19
19
# TODO: 'bzr resolve' should accept a directory name and work from that
22
# TODO: bzr revert should resolve; even when reverting the whole tree
23
# or particular directories
29
from bzrlib.branch import Branch
30
from bzrlib.errors import BzrCommandError
26
31
from bzrlib.commands import register_command
27
from bzrlib.errors import BzrCommandError, NotConflicted, UnsupportedOperation
28
from bzrlib.option import Option
29
from bzrlib.osutils import rename, delete_any
30
from bzrlib.rio import Stanza
33
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
32
from bzrlib.workingtree import CONFLICT_SUFFIXES
36
34
class cmd_conflicts(bzrlib.commands.Command):
37
35
"""List files with conflicts.
39
Merge will do its best to combine the changes in two branches, but there
40
are some kinds of problems only a human can fix. When it encounters those,
41
it will mark a conflict. A conflict means that you need to fix something,
42
before you should commit.
44
Use bzr resolve when you have fixed a problem.
46
36
(conflicts are determined by the presence of .BASE .TREE, and .OTHER
52
from bzrlib.workingtree import WorkingTree
53
wt = WorkingTree.open_containing(u'.')[0]
54
for conflict in wt.conflicts():
40
for path in Branch.open_containing('.').working_tree().iter_conflicts():
58
43
class cmd_resolve(bzrlib.commands.Command):
59
44
"""Mark a conflict as resolved.
61
Merge will do its best to combine the changes in two branches, but there
62
are some kinds of problems only a human can fix. When it encounters those,
63
it will mark a conflict. A conflict means that you need to fix something,
64
before you should commit.
66
Once you have fixed a problem, use "bzr resolve FILE.." to mark
67
individual files as fixed, or "bzr resolve --all" to mark all conflicts as
70
See also bzr conflicts.
72
aliases = ['resolved']
73
46
takes_args = ['file*']
74
takes_options = [Option('all', help='Resolve all conflicts in this tree')]
47
takes_options = ['all']
75
48
def run(self, file_list=None, all=False):
76
from bzrlib.workingtree import WorkingTree
79
raise BzrCommandError("If --all is specified, no FILE may be provided")
80
tree = WorkingTree.open_containing('.')[0]
84
raise BzrCommandError("command 'resolve' needs one or more FILE, or --all")
85
tree = WorkingTree.open_containing(file_list[0])[0]
86
to_resolve = [tree.relpath(p) for p in file_list]
87
resolve(tree, to_resolve)
90
def resolve(tree, paths=None, ignore_misses=False):
93
tree_conflicts = tree.conflicts()
95
new_conflicts = ConflictList()
96
selected_conflicts = tree_conflicts
98
new_conflicts, selected_conflicts = \
99
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
101
tree.set_conflicts(new_conflicts)
102
except UnsupportedOperation:
104
selected_conflicts.remove_files(tree)
109
def restore(filename):
111
Restore a conflicted file to the state it was in before merging.
112
Only text restoration supported at present.
116
rename(filename + ".THIS", filename)
119
if e.errno != errno.ENOENT:
122
os.unlink(filename + ".BASE")
125
if e.errno != errno.ENOENT:
128
os.unlink(filename + ".OTHER")
131
if e.errno != errno.ENOENT:
134
raise NotConflicted(filename)
137
class ConflictList(object):
138
"""List of conflicts.
140
Typically obtained from WorkingTree.conflicts()
142
Can be instantiated from stanzas or from Conflict subclasses.
145
def __init__(self, conflicts=None):
146
object.__init__(self)
147
if conflicts is None:
150
self.__list = conflicts
153
return len(self.__list) == 0
156
return len(self.__list)
159
return iter(self.__list)
161
def __getitem__(self, key):
162
return self.__list[key]
164
def append(self, conflict):
165
return self.__list.append(conflict)
167
def __eq__(self, other_list):
168
return list(self) == list(other_list)
170
def __ne__(self, other_list):
171
return not (self == other_list)
174
return "ConflictList(%r)" % self.__list
177
def from_stanzas(stanzas):
178
"""Produce a new ConflictList from an iterable of stanzas"""
179
conflicts = ConflictList()
180
for stanza in stanzas:
181
conflicts.append(Conflict.factory(**stanza.as_dict()))
184
def to_stanzas(self):
185
"""Generator of stanzas"""
186
for conflict in self:
187
yield conflict.as_stanza()
189
def to_strings(self):
190
"""Generate strings for the provided conflicts"""
191
for conflict in self:
194
def remove_files(self, tree):
195
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
196
for conflict in self:
197
if not conflict.has_files:
51
raise BzrCommandError(
52
"command 'resolve' needs one or more FILE, or --all")
53
tree = Branch.open_containing('.').working_tree()
54
file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
57
raise BzrCommandError(
58
"If --all is specified, no FILE may be provided")
59
for filename in file_list:
199
61
for suffix in CONFLICT_SUFFIXES:
201
delete_any(tree.abspath(conflict.path+suffix))
63
os.unlink(filename+suffix)
202
64
except OSError, e:
203
65
if e.errno != errno.ENOENT:
206
def select_conflicts(self, tree, paths, ignore_misses=False):
207
"""Select the conflicts associated with paths in a tree.
209
File-ids are also used for this.
211
path_set = set(paths)
213
selected_paths = set()
214
new_conflicts = ConflictList()
215
selected_conflicts = ConflictList()
217
file_id = tree.path2id(path)
218
if file_id is not None:
221
for conflict in self:
223
for key in ('path', 'conflict_path'):
224
cpath = getattr(conflict, key, None)
227
if cpath in path_set:
229
selected_paths.add(cpath)
230
for key in ('file_id', 'conflict_file_id'):
231
cfile_id = getattr(conflict, key, None)
235
cpath = ids[cfile_id]
239
selected_paths.add(cpath)
241
selected_conflicts.append(conflict)
243
new_conflicts.append(conflict)
244
if ignore_misses is not True:
245
for path in [p for p in paths if p not in selected_paths]:
246
if not os.path.exists(tree.abspath(path)):
247
print "%s does not exist" % path
69
if failures == len(CONFLICT_SUFFIXES):
70
if not os.path.exists(filename):
71
print "%s does not exist" % filename
249
print "%s is not conflicted" % path
250
return new_conflicts, selected_conflicts
253
class Conflict(object):
254
"""Base class for all types of conflict"""
258
def __init__(self, path, file_id=None):
260
self.file_id = file_id
263
s = Stanza(type=self.typestring, path=self.path)
264
if self.file_id is not None:
265
s.add('file_id', self.file_id)
269
return [type(self), self.path, self.file_id]
271
def __cmp__(self, other):
272
if getattr(other, "_cmp_list", None) is None:
274
return cmp(self._cmp_list(), other._cmp_list())
276
def __eq__(self, other):
277
return self.__cmp__(other) == 0
279
def __ne__(self, other):
280
return not self.__eq__(other)
283
return self.format % self.__dict__
286
rdict = dict(self.__dict__)
287
rdict['class'] = self.__class__.__name__
288
return self.rformat % rdict
291
def factory(type, **kwargs):
293
return ctype[type](**kwargs)
296
def sort_key(conflict):
297
if conflict.path is not None:
298
return conflict.path, conflict.typestring
299
elif getattr(conflict, "conflict_path", None) is not None:
300
return conflict.conflict_path, conflict.typestring
302
return None, conflict.typestring
305
class PathConflict(Conflict):
306
"""A conflict was encountered merging file paths"""
308
typestring = 'path conflict'
310
format = 'Path conflict: %(path)s / %(conflict_path)s'
312
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
313
def __init__(self, path, conflict_path=None, file_id=None):
314
Conflict.__init__(self, path, file_id)
315
self.conflict_path = conflict_path
318
s = Conflict.as_stanza(self)
319
if self.conflict_path is not None:
320
s.add('conflict_path', self.conflict_path)
324
class ContentsConflict(PathConflict):
325
"""The files are of different types, or not present"""
329
typestring = 'contents conflict'
331
format = 'Contents conflict in %(path)s'
334
class TextConflict(PathConflict):
335
"""The merge algorithm could not resolve all differences encountered."""
339
typestring = 'text conflict'
341
format = 'Text conflict in %(path)s'
344
class HandledConflict(Conflict):
345
"""A path problem that has been provisionally resolved.
346
This is intended to be a base class.
349
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
351
def __init__(self, action, path, file_id=None):
352
Conflict.__init__(self, path, file_id)
356
return Conflict._cmp_list(self) + [self.action]
359
s = Conflict.as_stanza(self)
360
s.add('action', self.action)
364
class HandledPathConflict(HandledConflict):
365
"""A provisionally-resolved path problem involving two paths.
366
This is intended to be a base class.
369
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
370
" %(file_id)r, %(conflict_file_id)r)"
372
def __init__(self, action, path, conflict_path, file_id=None,
373
conflict_file_id=None):
374
HandledConflict.__init__(self, action, path, file_id)
375
self.conflict_path = conflict_path
376
self.conflict_file_id = conflict_file_id
379
return HandledConflict._cmp_list(self) + [self.conflict_path,
380
self.conflict_file_id]
383
s = HandledConflict.as_stanza(self)
384
s.add('conflict_path', self.conflict_path)
385
if self.conflict_file_id is not None:
386
s.add('conflict_file_id', self.conflict_file_id)
391
class DuplicateID(HandledPathConflict):
392
"""Two files want the same file_id."""
394
typestring = 'duplicate id'
396
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
399
class DuplicateEntry(HandledPathConflict):
400
"""Two directory entries want to have the same name."""
402
typestring = 'duplicate'
404
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
407
class ParentLoop(HandledPathConflict):
408
"""An attempt to create an infinitely-looping directory structure.
409
This is rare, but can be produced like so:
418
typestring = 'parent loop'
420
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
423
class UnversionedParent(HandledConflict):
424
"""An attempt to version an file whose parent directory is not versioned.
425
Typically, the result of a merge where one tree unversioned the directory
426
and the other added a versioned file to it.
429
typestring = 'unversioned parent'
431
format = 'Conflict adding versioned files to %(path)s. %(action)s.'
434
class MissingParent(HandledConflict):
435
"""An attempt to add files to a directory that is not present.
436
Typically, the result of a merge where one tree deleted the directory and
437
the other added a file to it.
440
typestring = 'missing parent'
442
format = 'Conflict adding files to %(path)s. %(action)s.'
449
def register_types(*conflict_types):
450
"""Register a Conflict subclass for serialization purposes"""
452
for conflict_type in conflict_types:
453
ctype[conflict_type.typestring] = conflict_type
456
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
457
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)
73
print "%s is not conflicted" % filename