1
# Copyright (C) 2005, 2007, 2009 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# TODO: 'bzr resolve' should accept a directory name and work from that
23
from bzrlib.lazy_import import lazy_import
24
lazy_import(globals(), """
43
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
46
class cmd_conflicts(commands.Command):
47
"""List files with conflicts.
49
Merge will do its best to combine the changes in two branches, but there
50
are some kinds of problems only a human can fix. When it encounters those,
51
it will mark a conflict. A conflict means that you need to fix something,
52
before you should commit.
54
Conflicts normally are listed as short, human-readable messages. If --text
55
is supplied, the pathnames of files with text conflicts are listed,
56
instead. (This is useful for editing all files with text conflicts.)
58
Use bzr resolve when you have fixed a problem.
62
help='List paths of files with text conflicts.'),
64
_see_also = ['resolve']
66
def run(self, text=False):
67
wt = workingtree.WorkingTree.open_containing(u'.')[0]
68
for conflict in wt.conflicts():
70
if conflict.typestring != 'text conflict':
72
self.outf.write(conflict.path + '\n')
74
self.outf.write(str(conflict) + '\n')
77
resolve_action_registry = registry.Registry()
80
resolve_action_registry.register(
81
'done', 'done', 'Marks the conflict as resolved' )
82
resolve_action_registry.register(
83
'keep-mine', 'keep_mine',
84
'Resolve the conflict preserving the version in the working tree' )
85
resolve_action_registry.register(
86
'take-their', 'take_their',
87
'Resolve the conflict taking the merged version into account' )
88
resolve_action_registry.default_key = 'done'
90
class ResolveActionOption(option.RegistryOption):
93
super(ResolveActionOption, self).__init__(
94
'action', 'How to resolve the conflict.',
96
registry=resolve_action_registry)
99
class cmd_resolve(commands.Command):
100
"""Mark a conflict as resolved.
102
Merge will do its best to combine the changes in two branches, but there
103
are some kinds of problems only a human can fix. When it encounters those,
104
it will mark a conflict. A conflict means that you need to fix something,
105
before you should commit.
107
Once you have fixed a problem, use "bzr resolve" to automatically mark
108
text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
109
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
111
aliases = ['resolved']
112
takes_args = ['file*']
114
option.Option('all', help='Resolve all conflicts in this tree.'),
115
ResolveActionOption(),
117
_see_also = ['conflicts']
118
def run(self, file_list=None, all=False, action=None):
121
raise errors.BzrCommandError("If --all is specified,"
122
" no FILE may be provided")
123
tree = workingtree.WorkingTree.open_containing('.')[0]
127
tree, file_list = builtins.tree_files(file_list)
128
if file_list is None:
130
# FIXME: There is a special case here related to the option
131
# handling that could be clearer and easier to discover by
132
# providing an --auto action (bug #344013 and #383396) and
133
# make it mandatory instead of implicit and active only
134
# when no file_list is provided -- vila 091229
140
if file_list is None:
141
un_resolved, resolved = tree.auto_resolve()
142
if len(un_resolved) > 0:
143
trace.note('%d conflict(s) auto-resolved.', len(resolved))
144
trace.note('Remaining conflicts:')
145
for conflict in un_resolved:
149
trace.note('All conflicts resolved.')
152
# FIXME: This can never occur but the block above needs some
153
# refactoring to transfer tree.auto_resolve() to
154
# conflict.auto(tree) --vila 091242
157
resolve(tree, file_list, action=action)
160
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
162
"""Resolve some or all of the conflicts in a working tree.
164
:param paths: If None, resolve all conflicts. Otherwise, select only
166
:param recursive: If True, then elements of paths which are directories
167
have all their children resolved, etc. When invoked as part of
168
recursive commands like revert, this should be True. For commands
169
or applications wishing finer-grained control, like the resolve
170
command, this should be False.
171
:param ignore_misses: If False, warnings will be printed if the supplied
172
paths do not have conflicts.
173
:param action: How the conflict should be resolved,
175
tree.lock_tree_write()
177
tree_conflicts = tree.conflicts()
179
new_conflicts = ConflictList()
180
to_process = tree_conflicts
182
new_conflicts, to_process = tree_conflicts.select_conflicts(
183
tree, paths, ignore_misses, recursive)
184
for conflict in to_process:
186
conflict._do(action, tree)
187
conflict.cleanup(tree)
188
except NotImplementedError:
189
new_conflicts.append(conflict)
191
tree.set_conflicts(new_conflicts)
192
except errors.UnsupportedOperation:
198
def restore(filename):
199
"""Restore a conflicted file to the state it was in before merging.
201
Only text restoration is supported at present.
205
osutils.rename(filename + ".THIS", filename)
208
if e.errno != errno.ENOENT:
211
os.unlink(filename + ".BASE")
214
if e.errno != errno.ENOENT:
217
os.unlink(filename + ".OTHER")
220
if e.errno != errno.ENOENT:
223
raise errors.NotConflicted(filename)
226
class ConflictList(object):
227
"""List of conflicts.
229
Typically obtained from WorkingTree.conflicts()
231
Can be instantiated from stanzas or from Conflict subclasses.
234
def __init__(self, conflicts=None):
235
object.__init__(self)
236
if conflicts is None:
239
self.__list = conflicts
242
return len(self.__list) == 0
245
return len(self.__list)
248
return iter(self.__list)
250
def __getitem__(self, key):
251
return self.__list[key]
253
def append(self, conflict):
254
return self.__list.append(conflict)
256
def __eq__(self, other_list):
257
return list(self) == list(other_list)
259
def __ne__(self, other_list):
260
return not (self == other_list)
263
return "ConflictList(%r)" % self.__list
266
def from_stanzas(stanzas):
267
"""Produce a new ConflictList from an iterable of stanzas"""
268
conflicts = ConflictList()
269
for stanza in stanzas:
270
conflicts.append(Conflict.factory(**stanza.as_dict()))
273
def to_stanzas(self):
274
"""Generator of stanzas"""
275
for conflict in self:
276
yield conflict.as_stanza()
278
def to_strings(self):
279
"""Generate strings for the provided conflicts"""
280
for conflict in self:
283
def remove_files(self, tree):
284
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
285
for conflict in self:
286
if not conflict.has_files:
288
conflict.cleanup(tree)
290
def select_conflicts(self, tree, paths, ignore_misses=False,
292
"""Select the conflicts associated with paths in a tree.
294
File-ids are also used for this.
295
:return: a pair of ConflictLists: (not_selected, selected)
297
path_set = set(paths)
299
selected_paths = set()
300
new_conflicts = ConflictList()
301
selected_conflicts = ConflictList()
303
file_id = tree.path2id(path)
304
if file_id is not None:
307
for conflict in self:
309
for key in ('path', 'conflict_path'):
310
cpath = getattr(conflict, key, None)
313
if cpath in path_set:
315
selected_paths.add(cpath)
317
if osutils.is_inside_any(path_set, cpath):
319
selected_paths.add(cpath)
321
for key in ('file_id', 'conflict_file_id'):
322
cfile_id = getattr(conflict, key, None)
326
cpath = ids[cfile_id]
330
selected_paths.add(cpath)
332
selected_conflicts.append(conflict)
334
new_conflicts.append(conflict)
335
if ignore_misses is not True:
336
for path in [p for p in paths if p not in selected_paths]:
337
if not os.path.exists(tree.abspath(path)):
338
print "%s does not exist" % path
340
print "%s is not conflicted" % path
341
return new_conflicts, selected_conflicts
344
class Conflict(object):
345
"""Base class for all types of conflict"""
347
# FIXME: cleanup should take care of that ? -- vila 091229
350
def __init__(self, path, file_id=None):
352
# warn turned off, because the factory blindly transfers the Stanza
353
# values to __init__ and Stanza is purely a Unicode api.
354
self.file_id = osutils.safe_file_id(file_id, warn=False)
357
s = rio.Stanza(type=self.typestring, path=self.path)
358
if self.file_id is not None:
359
# Stanza requires Unicode apis
360
s.add('file_id', self.file_id.decode('utf8'))
364
return [type(self), self.path, self.file_id]
366
def __cmp__(self, other):
367
if getattr(other, "_cmp_list", None) is None:
369
return cmp(self._cmp_list(), other._cmp_list())
372
return hash((type(self), self.path, self.file_id))
374
def __eq__(self, other):
375
return self.__cmp__(other) == 0
377
def __ne__(self, other):
378
return not self.__eq__(other)
381
return self.format % self.__dict__
384
rdict = dict(self.__dict__)
385
rdict['class'] = self.__class__.__name__
386
return self.rformat % rdict
389
def factory(type, **kwargs):
391
return ctype[type](**kwargs)
394
def sort_key(conflict):
395
if conflict.path is not None:
396
return conflict.path, conflict.typestring
397
elif getattr(conflict, "conflict_path", None) is not None:
398
return conflict.conflict_path, conflict.typestring
400
return None, conflict.typestring
402
def _do(self, action, tree):
403
"""Apply the specified action to the conflict.
405
:param action: The method name to call.
407
:param tree: The tree passed as a parameter to the method.
409
meth = getattr(self, action, None)
411
raise NotImplementedError(self.__class__.__name__ + '.' + action)
414
def cleanup(self, tree):
415
raise NotImplementedError(self.cleanup)
417
def done(self, tree):
418
"""Mark the conflict as solved once it has been handled."""
419
# This method does nothing but simplifies the design of upper levels.
422
def keep_mine(self, tree):
423
raise NotImplementedError(self.keep_mine)
425
def take_their(self, tree):
426
raise NotImplementedError(self.take_their)
429
class PathConflict(Conflict):
430
"""A conflict was encountered merging file paths"""
432
typestring = 'path conflict'
434
format = 'Path conflict: %(path)s / %(conflict_path)s'
436
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
438
def __init__(self, path, conflict_path=None, file_id=None):
439
Conflict.__init__(self, path, file_id)
440
self.conflict_path = conflict_path
443
s = Conflict.as_stanza(self)
444
if self.conflict_path is not None:
445
s.add('conflict_path', self.conflict_path)
448
def cleanup(self, tree):
449
# No additional files have been generated here
452
def keep_mine(self, tree):
453
tree.rename_one(self.conflict_path, self.path)
455
def take_their(self, tree):
456
# just acccept bzr proposal
460
class ContentsConflict(PathConflict):
461
"""The files are of different types, or not present"""
465
typestring = 'contents conflict'
467
format = 'Contents conflict in %(path)s'
469
def cleanup(self, tree):
470
for suffix in ('.BASE', '.OTHER'):
472
osutils.delete_any(tree.abspath(self.path + suffix))
474
if e.errno != errno.ENOENT:
477
# FIXME: I smell something weird here and it seems we should be able to be
478
# more coherent with some other conflict ? bzr *did* a choice there but
479
# neither keep_mine nor take_their reflect that... -- vila 091224
480
def keep_mine(self, tree):
481
tree.remove([self.path + '.OTHER'], force=True, keep_files=False)
483
def take_their(self, tree):
484
tree.remove([self.path], force=True, keep_files=False)
488
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
489
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
491
# TODO: There should be a base revid attribute to better inform the user about
492
# how the conflicts were generated.
493
class TextConflict(PathConflict):
494
"""The merge algorithm could not resolve all differences encountered."""
498
typestring = 'text conflict'
500
format = 'Text conflict in %(path)s'
502
def cleanup(self, tree):
503
for suffix in CONFLICT_SUFFIXES:
505
osutils.delete_any(tree.abspath(self.path+suffix))
507
if e.errno != errno.ENOENT:
511
class HandledConflict(Conflict):
512
"""A path problem that has been provisionally resolved.
513
This is intended to be a base class.
516
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
518
def __init__(self, action, path, file_id=None):
519
Conflict.__init__(self, path, file_id)
523
return Conflict._cmp_list(self) + [self.action]
526
s = Conflict.as_stanza(self)
527
s.add('action', self.action)
530
def cleanup(self, tree):
531
"""Nothing to cleanup."""
535
class HandledPathConflict(HandledConflict):
536
"""A provisionally-resolved path problem involving two paths.
537
This is intended to be a base class.
540
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
541
" %(file_id)r, %(conflict_file_id)r)"
543
def __init__(self, action, path, conflict_path, file_id=None,
544
conflict_file_id=None):
545
HandledConflict.__init__(self, action, path, file_id)
546
self.conflict_path = conflict_path
547
# warn turned off, because the factory blindly transfers the Stanza
548
# values to __init__.
549
self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
553
return HandledConflict._cmp_list(self) + [self.conflict_path,
554
self.conflict_file_id]
557
s = HandledConflict.as_stanza(self)
558
s.add('conflict_path', self.conflict_path)
559
if self.conflict_file_id is not None:
560
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
565
class DuplicateID(HandledPathConflict):
566
"""Two files want the same file_id."""
568
typestring = 'duplicate id'
570
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
573
class DuplicateEntry(HandledPathConflict):
574
"""Two directory entries want to have the same name."""
576
typestring = 'duplicate'
578
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
580
def keep_mine(self, tree):
581
tree.remove([self.conflict_path], force=True, keep_files=False)
582
tree.rename_one(self.path, self.conflict_path)
584
def take_their(self, tree):
585
tree.remove([self.path], force=True, keep_files=False)
588
class ParentLoop(HandledPathConflict):
589
"""An attempt to create an infinitely-looping directory structure.
590
This is rare, but can be produced like so:
599
typestring = 'parent loop'
601
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
603
def keep_mine(self, tree):
604
# just acccept bzr proposal
607
def take_their(self, tree):
608
# FIXME: We shouldn't have to manipulate so many paths here (and there
609
# is probably a bug or two...)
610
conflict_base_path = osutils.basename(self.conflict_path)
611
base_path = osutils.basename(self.path)
612
tree.rename_one(self.conflict_path, conflict_base_path)
613
tree.rename_one(self.path,
614
osutils.joinpath([conflict_base_path, base_path]))
617
class UnversionedParent(HandledConflict):
618
"""An attempt to version a file whose parent directory is not versioned.
619
Typically, the result of a merge where one tree unversioned the directory
620
and the other added a versioned file to it.
623
typestring = 'unversioned parent'
625
format = 'Conflict because %(path)s is not versioned, but has versioned'\
626
' children. %(action)s.'
628
# FIXME: We silently do nothing to make tests pass, but most probably the
629
# conflict shouldn't exist (the long story is that the conflict is
630
# generated with another one that can be resolved properly) -- vila 091224
631
def keep_mine(self, tree):
634
def take_their(self, tree):
638
class MissingParent(HandledConflict):
639
"""An attempt to add files to a directory that is not present.
640
Typically, the result of a merge where THIS deleted the directory and
641
the OTHER added a file to it.
642
See also: DeletingParent (same situation, THIS and OTHER reversed)
645
typestring = 'missing parent'
647
format = 'Conflict adding files to %(path)s. %(action)s.'
649
def keep_mine(self, tree):
650
tree.remove([self.path], force=True, keep_files=False)
652
def take_their(self, tree):
653
# just acccept bzr proposal
657
class DeletingParent(HandledConflict):
658
"""An attempt to add files to a directory that is not present.
659
Typically, the result of a merge where one OTHER deleted the directory and
660
the THIS added a file to it.
663
typestring = 'deleting parent'
665
format = "Conflict: can't delete %(path)s because it is not empty. "\
668
# FIXME: It's a bit strange that the default action is not coherent with
669
# MissingParent from the *user* pov.
671
def keep_mine(self, tree):
672
# just acccept bzr proposal
675
def take_their(self, tree):
676
tree.remove([self.path], force=True, keep_files=False)
679
class NonDirectoryParent(HandledConflict):
680
"""An attempt to add files to a directory that is not a directory or
681
an attempt to change the kind of a directory with files.
684
typestring = 'non-directory parent'
686
format = "Conflict: %(path)s is not a directory, but has files in it."\
689
def keep_mine(self, tree):
690
# FIXME: we should preserve that path when the conflict is generated !
691
if self.path.endswith('.new'):
692
conflict_path = self.path[:-(len('.new'))]
693
tree.remove([self.path], force=True, keep_files=False)
694
tree.add(conflict_path)
696
raise NotImplementedError(self.keep_mine)
698
def take_their(self, tree):
699
# FIXME: we should preserve that path when the conflict is generated !
700
if self.path.endswith('.new'):
701
conflict_path = self.path[:-(len('.new'))]
702
tree.remove([conflict_path], force=True, keep_files=False)
703
tree.rename_one(self.path, conflict_path)
705
raise NotImplementedError(self.take_their)
711
def register_types(*conflict_types):
712
"""Register a Conflict subclass for serialization purposes"""
714
for conflict_type in conflict_types:
715
ctype[conflict_type.typestring] = conflict_type
717
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
718
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
719
DeletingParent, NonDirectoryParent)