1
# Copyright (C) 2005 Aaron Bentley, Canonical Ltd
1
# Copyright (C) 2005 by Aaron Bentley
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
# TODO: Move this into builtins
19
# TODO: 'bzr resolve' should accept a directory name and work from that
18
from bzrlib.branch import Branch
19
from bzrlib.errors import BzrCommandError
20
from bzrlib.commands import register_command
21
from bzrlib.workingtree import CONFLICT_SUFFIXES
24
from bzrlib.lazy_import import lazy_import
25
lazy_import(globals(), """
37
from bzrlib.option import Option
40
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
43
class cmd_conflicts(commands.Command):
25
class cmd_conflicts(bzrlib.commands.Command):
44
26
"""List files with conflicts.
46
Merge will do its best to combine the changes in two branches, but there
47
are some kinds of problems only a human can fix. When it encounters those,
48
it will mark a conflict. A conflict means that you need to fix something,
49
before you should commit.
51
Conflicts normally are listed as short, human-readable messages. If --text
52
is supplied, the pathnames of files with text conflicts are listed,
53
instead. (This is useful for editing all files with text conflicts.)
55
Use bzr resolve when you have fixed a problem.
27
(conflicts are determined by the presence of .BASE .TREE, and .OTHER
61
help='List paths of files with text conflicts.'),
64
def run(self, text=False):
65
from bzrlib.workingtree import WorkingTree
66
wt = WorkingTree.open_containing(u'.')[0]
67
for conflict in wt.conflicts():
69
if conflict.typestring != 'text conflict':
71
self.outf.write(conflict.path + '\n')
73
self.outf.write(str(conflict) + '\n')
76
class cmd_resolve(commands.Command):
31
for path in Branch.open_containing('.').working_tree().iter_conflicts():
34
register_command(cmd_conflicts)
36
class cmd_resolve(bzrlib.commands.Command):
77
37
"""Mark a conflict as resolved.
79
Merge will do its best to combine the changes in two branches, but there
80
are some kinds of problems only a human can fix. When it encounters those,
81
it will mark a conflict. A conflict means that you need to fix something,
82
before you should commit.
84
Once you have fixed a problem, use "bzr resolve" to automatically mark
85
text conflicts as fixed, resolve FILE to mark a specific conflict as
86
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
88
See also bzr conflicts.
90
aliases = ['resolved']
91
39
takes_args = ['file*']
93
Option('all', help='Resolve all conflicts in this tree.'),
40
takes_options = ['all']
95
41
def run(self, file_list=None, all=False):
96
from bzrlib.workingtree import WorkingTree
99
raise errors.BzrCommandError("If --all is specified,"
100
" no FILE may be provided")
101
tree = WorkingTree.open_containing('.')[0]
104
tree, file_list = builtins.tree_files(file_list)
105
if file_list is None:
106
un_resolved, resolved = tree.auto_resolve()
107
if len(un_resolved) > 0:
108
trace.note('%d conflict(s) auto-resolved.', len(resolved))
109
trace.note('Remaining conflicts:')
110
for conflict in un_resolved:
114
trace.note('All conflicts resolved.')
117
resolve(tree, file_list)
120
def resolve(tree, paths=None, ignore_misses=False):
121
tree.lock_tree_write()
123
tree_conflicts = tree.conflicts()
125
new_conflicts = ConflictList()
126
selected_conflicts = tree_conflicts
128
new_conflicts, selected_conflicts = \
129
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
131
tree.set_conflicts(new_conflicts)
132
except errors.UnsupportedOperation:
134
selected_conflicts.remove_files(tree)
139
def restore(filename):
141
Restore a conflicted file to the state it was in before merging.
142
Only text restoration supported at present.
146
osutils.rename(filename + ".THIS", filename)
149
if e.errno != errno.ENOENT:
152
os.unlink(filename + ".BASE")
155
if e.errno != errno.ENOENT:
158
os.unlink(filename + ".OTHER")
161
if e.errno != errno.ENOENT:
164
raise errors.NotConflicted(filename)
167
class ConflictList(object):
168
"""List of conflicts.
170
Typically obtained from WorkingTree.conflicts()
172
Can be instantiated from stanzas or from Conflict subclasses.
175
def __init__(self, conflicts=None):
176
object.__init__(self)
177
if conflicts is None:
180
self.__list = conflicts
183
return len(self.__list) == 0
186
return len(self.__list)
189
return iter(self.__list)
191
def __getitem__(self, key):
192
return self.__list[key]
194
def append(self, conflict):
195
return self.__list.append(conflict)
197
def __eq__(self, other_list):
198
return list(self) == list(other_list)
200
def __ne__(self, other_list):
201
return not (self == other_list)
204
return "ConflictList(%r)" % self.__list
207
def from_stanzas(stanzas):
208
"""Produce a new ConflictList from an iterable of stanzas"""
209
conflicts = ConflictList()
210
for stanza in stanzas:
211
conflicts.append(Conflict.factory(**stanza.as_dict()))
214
def to_stanzas(self):
215
"""Generator of stanzas"""
216
for conflict in self:
217
yield conflict.as_stanza()
219
def to_strings(self):
220
"""Generate strings for the provided conflicts"""
221
for conflict in self:
224
def remove_files(self, tree):
225
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
226
for conflict in self:
227
if not conflict.has_files:
44
raise BzrCommandError(
45
"command 'resolve' needs one or more FILE, or --all")
46
tree = Branch.open_containing('.').working_tree()
47
file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
50
raise BzrCommandError(
51
"If --all is specified, no FILE may be provided")
52
for filename in file_list:
229
54
for suffix in CONFLICT_SUFFIXES:
231
osutils.delete_any(tree.abspath(conflict.path+suffix))
56
os.unlink(filename+suffix)
232
57
except OSError, e:
233
58
if e.errno != errno.ENOENT:
236
def select_conflicts(self, tree, paths, ignore_misses=False):
237
"""Select the conflicts associated with paths in a tree.
239
File-ids are also used for this.
240
:return: a pair of ConflictLists: (not_selected, selected)
242
path_set = set(paths)
244
selected_paths = set()
245
new_conflicts = ConflictList()
246
selected_conflicts = ConflictList()
248
file_id = tree.path2id(path)
249
if file_id is not None:
252
for conflict in self:
254
for key in ('path', 'conflict_path'):
255
cpath = getattr(conflict, key, None)
258
if cpath in path_set:
260
selected_paths.add(cpath)
261
for key in ('file_id', 'conflict_file_id'):
262
cfile_id = getattr(conflict, key, None)
266
cpath = ids[cfile_id]
270
selected_paths.add(cpath)
272
selected_conflicts.append(conflict)
274
new_conflicts.append(conflict)
275
if ignore_misses is not True:
276
for path in [p for p in paths if p not in selected_paths]:
277
if not os.path.exists(tree.abspath(path)):
278
print "%s does not exist" % path
62
if failures == len(CONFLICT_SUFFIXES):
63
if not os.path.exists(filename):
64
print "%s does not exist" % filename
280
print "%s is not conflicted" % path
281
return new_conflicts, selected_conflicts
284
class Conflict(object):
285
"""Base class for all types of conflict"""
289
def __init__(self, path, file_id=None):
291
# warn turned off, because the factory blindly transfers the Stanza
292
# values to __init__ and Stanza is purely a Unicode api.
293
self.file_id = osutils.safe_file_id(file_id, warn=False)
296
s = rio.Stanza(type=self.typestring, path=self.path)
297
if self.file_id is not None:
298
# Stanza requires Unicode apis
299
s.add('file_id', self.file_id.decode('utf8'))
303
return [type(self), self.path, self.file_id]
305
def __cmp__(self, other):
306
if getattr(other, "_cmp_list", None) is None:
308
return cmp(self._cmp_list(), other._cmp_list())
311
return hash((type(self), self.path, self.file_id))
313
def __eq__(self, other):
314
return self.__cmp__(other) == 0
316
def __ne__(self, other):
317
return not self.__eq__(other)
320
return self.format % self.__dict__
323
rdict = dict(self.__dict__)
324
rdict['class'] = self.__class__.__name__
325
return self.rformat % rdict
328
def factory(type, **kwargs):
330
return ctype[type](**kwargs)
333
def sort_key(conflict):
334
if conflict.path is not None:
335
return conflict.path, conflict.typestring
336
elif getattr(conflict, "conflict_path", None) is not None:
337
return conflict.conflict_path, conflict.typestring
339
return None, conflict.typestring
342
class PathConflict(Conflict):
343
"""A conflict was encountered merging file paths"""
345
typestring = 'path conflict'
347
format = 'Path conflict: %(path)s / %(conflict_path)s'
349
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
350
def __init__(self, path, conflict_path=None, file_id=None):
351
Conflict.__init__(self, path, file_id)
352
self.conflict_path = conflict_path
355
s = Conflict.as_stanza(self)
356
if self.conflict_path is not None:
357
s.add('conflict_path', self.conflict_path)
361
class ContentsConflict(PathConflict):
362
"""The files are of different types, or not present"""
366
typestring = 'contents conflict'
368
format = 'Contents conflict in %(path)s'
371
class TextConflict(PathConflict):
372
"""The merge algorithm could not resolve all differences encountered."""
376
typestring = 'text conflict'
378
format = 'Text conflict in %(path)s'
381
class HandledConflict(Conflict):
382
"""A path problem that has been provisionally resolved.
383
This is intended to be a base class.
386
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
388
def __init__(self, action, path, file_id=None):
389
Conflict.__init__(self, path, file_id)
393
return Conflict._cmp_list(self) + [self.action]
396
s = Conflict.as_stanza(self)
397
s.add('action', self.action)
401
class HandledPathConflict(HandledConflict):
402
"""A provisionally-resolved path problem involving two paths.
403
This is intended to be a base class.
406
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
407
" %(file_id)r, %(conflict_file_id)r)"
409
def __init__(self, action, path, conflict_path, file_id=None,
410
conflict_file_id=None):
411
HandledConflict.__init__(self, action, path, file_id)
412
self.conflict_path = conflict_path
413
# warn turned off, because the factory blindly transfers the Stanza
414
# values to __init__.
415
self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
419
return HandledConflict._cmp_list(self) + [self.conflict_path,
420
self.conflict_file_id]
423
s = HandledConflict.as_stanza(self)
424
s.add('conflict_path', self.conflict_path)
425
if self.conflict_file_id is not None:
426
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
431
class DuplicateID(HandledPathConflict):
432
"""Two files want the same file_id."""
434
typestring = 'duplicate id'
436
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
439
class DuplicateEntry(HandledPathConflict):
440
"""Two directory entries want to have the same name."""
442
typestring = 'duplicate'
444
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
447
class ParentLoop(HandledPathConflict):
448
"""An attempt to create an infinitely-looping directory structure.
449
This is rare, but can be produced like so:
458
typestring = 'parent loop'
460
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
463
class UnversionedParent(HandledConflict):
464
"""An attempt to version an file whose parent directory is not versioned.
465
Typically, the result of a merge where one tree unversioned the directory
466
and the other added a versioned file to it.
469
typestring = 'unversioned parent'
471
format = 'Conflict because %(path)s is not versioned, but has versioned'\
472
' children. %(action)s.'
475
class MissingParent(HandledConflict):
476
"""An attempt to add files to a directory that is not present.
477
Typically, the result of a merge where THIS deleted the directory and
478
the OTHER added a file to it.
479
See also: DeletingParent (same situation, reversed THIS and OTHER)
482
typestring = 'missing parent'
484
format = 'Conflict adding files to %(path)s. %(action)s.'
487
class DeletingParent(HandledConflict):
488
"""An attempt to add files to a directory that is not present.
489
Typically, the result of a merge where one OTHER deleted the directory and
490
the THIS added a file to it.
493
typestring = 'deleting parent'
495
format = "Conflict: can't delete %(path)s because it is not empty. "\
502
def register_types(*conflict_types):
503
"""Register a Conflict subclass for serialization purposes"""
505
for conflict_type in conflict_types:
506
ctype[conflict_type.typestring] = conflict_type
509
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
510
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
66
print "%s is not conflicted" % filename
68
register_command(cmd_resolve)