14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# TODO: 'brz resolve' should accept a directory name and work from that
17
# TODO: Move this into builtins
19
# TODO: 'bzr resolve' should accept a directory name and work from that
23
from .lazy_import import lazy_import
24
from bzrlib.lazy_import import lazy_import
24
25
lazy_import(globals(), """
34
from breezy.i18n import gettext, ngettext
37
from bzrlib.option import Option
45
40
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
48
43
class cmd_conflicts(commands.Command):
49
__doc__ = """List files with conflicts.
44
"""List files with conflicts.
51
46
Merge will do its best to combine the changes in two branches, but there
52
47
are some kinds of problems only a human can fix. When it encounters those,
53
48
it will mark a conflict. A conflict means that you need to fix something,
54
before you can commit.
49
before you should commit.
56
51
Conflicts normally are listed as short, human-readable messages. If --text
57
52
is supplied, the pathnames of files with text conflicts are listed,
58
53
instead. (This is useful for editing all files with text conflicts.)
60
Use brz resolve when you have fixed a problem.
55
Use bzr resolve when you have fixed a problem.
65
help='List paths of files with text conflicts.'),
61
help='List paths of files with text conflicts.'),
67
_see_also = ['resolve', 'conflict-types']
69
def run(self, text=False, directory=u'.'):
70
wt = workingtree.WorkingTree.open_containing(directory)[0]
64
def run(self, text=False):
65
from bzrlib.workingtree import WorkingTree
66
wt = WorkingTree.open_containing(u'.')[0]
71
67
for conflict in wt.conflicts():
73
69
if conflict.typestring != 'text conflict':
77
73
self.outf.write(str(conflict) + '\n')
80
resolve_action_registry = registry.Registry()
83
resolve_action_registry.register(
84
'auto', 'auto', 'Detect whether conflict has been resolved by user.')
85
resolve_action_registry.register(
86
'done', 'done', 'Marks the conflict as resolved.')
87
resolve_action_registry.register(
88
'take-this', 'take_this',
89
'Resolve the conflict preserving the version in the working tree.')
90
resolve_action_registry.register(
91
'take-other', 'take_other',
92
'Resolve the conflict taking the merged version into account.')
93
resolve_action_registry.default_key = 'done'
96
class ResolveActionOption(option.RegistryOption):
99
super(ResolveActionOption, self).__init__(
100
'action', 'How to resolve the conflict.',
102
registry=resolve_action_registry)
105
76
class cmd_resolve(commands.Command):
106
__doc__ = """Mark a conflict as resolved.
77
"""Mark a conflict as resolved.
108
79
Merge will do its best to combine the changes in two branches, but there
109
80
are some kinds of problems only a human can fix. When it encounters those,
110
81
it will mark a conflict. A conflict means that you need to fix something,
111
before you can commit.
113
Once you have fixed a problem, use "brz resolve" to automatically mark
114
text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
115
resolved, or "brz resolve --all" to mark all conflicts as resolved.
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.
117
90
aliases = ['resolved']
118
91
takes_args = ['file*']
121
option.Option('all', help='Resolve all conflicts in this tree.'),
122
ResolveActionOption(),
124
_see_also = ['conflicts']
126
def run(self, file_list=None, all=False, action=None, directory=None):
93
Option('all', help='Resolve all conflicts in this tree.'),
95
def run(self, file_list=None, all=False):
96
from bzrlib.workingtree import WorkingTree
129
raise errors.CommandError(gettext("If --all is specified,"
130
" no FILE may be provided"))
131
if directory is None:
133
tree = workingtree.WorkingTree.open_containing(directory)[0]
99
raise errors.BzrCommandError("If --all is specified,"
100
" no FILE may be provided")
101
tree = WorkingTree.open_containing('.')[0]
137
tree, file_list = workingtree.WorkingTree.open_containing_paths(
138
file_list, directory)
140
if file_list is None:
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:
144
before, after = resolve(tree, file_list, action=action)
145
# GZ 2012-07-27: Should unify UI below now that auto is less magical.
146
if action == 'auto' and file_list is None:
149
ngettext('%d conflict auto-resolved.',
150
'%d conflicts auto-resolved.', before - after),
152
trace.note(gettext('Remaining conflicts:'))
153
for conflict in tree.conflicts():
154
trace.note(str(conflict))
114
trace.note('All conflicts resolved.')
157
trace.note(gettext('All conflicts resolved.'))
160
trace.note(ngettext('{0} conflict resolved, {1} remaining',
161
'{0} conflicts resolved, {1} remaining',
162
before - after).format(before - after, after))
165
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
117
resolve(tree, file_list)
120
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
167
121
"""Resolve some or all of the conflicts in a working tree.
169
123
:param paths: If None, resolve all conflicts. Otherwise, select only
173
127
recursive commands like revert, this should be True. For commands
174
128
or applications wishing finer-grained control, like the resolve
175
129
command, this should be False.
176
:param ignore_misses: If False, warnings will be printed if the supplied
177
paths do not have conflicts.
178
:param action: How the conflict should be resolved,
130
:ignore_misses: If False, warnings will be printed if the supplied paths
131
do not have conflicts.
180
nb_conflicts_after = None
181
with tree.lock_tree_write():
133
tree.lock_tree_write()
182
135
tree_conflicts = tree.conflicts()
183
nb_conflicts_before = len(tree_conflicts)
184
136
if paths is None:
185
137
new_conflicts = ConflictList()
186
to_process = tree_conflicts
138
selected_conflicts = tree_conflicts
188
new_conflicts, to_process = tree_conflicts.select_conflicts(
189
tree, paths, ignore_misses, recursive)
190
for conflict in to_process:
192
conflict._do(action, tree)
193
conflict.cleanup(tree)
194
except NotImplementedError:
195
new_conflicts.append(conflict)
140
new_conflicts, selected_conflicts = \
141
tree_conflicts.select_conflicts(tree, paths, ignore_misses,
197
nb_conflicts_after = len(new_conflicts)
198
144
tree.set_conflicts(new_conflicts)
199
145
except errors.UnsupportedOperation:
201
if nb_conflicts_after is None:
202
nb_conflicts_after = nb_conflicts_before
203
return nb_conflicts_before, nb_conflicts_after
147
selected_conflicts.remove_files(tree)
206
152
def restore(filename):
207
"""Restore a conflicted file to the state it was in before merging.
209
Only text restoration is supported at present.
154
Restore a conflicted file to the state it was in before merging.
155
Only text restoration supported at present.
211
157
conflicted = False
213
159
osutils.rename(filename + ".THIS", filename)
214
160
conflicted = True
216
162
if e.errno != errno.ENOENT:
219
165
os.unlink(filename + ".BASE")
220
166
conflicted = True
222
168
if e.errno != errno.ENOENT:
225
171
os.unlink(filename + ".OTHER")
226
172
conflicted = True
228
174
if e.errno != errno.ENOENT:
230
176
if not conflicted:
343
294
if ignore_misses is not True:
344
295
for path in [p for p in paths if p not in selected_paths]:
345
296
if not os.path.exists(tree.abspath(path)):
346
print("%s does not exist" % path)
297
print "%s does not exist" % path
348
print("%s is not conflicted" % path)
299
print "%s is not conflicted" % path
349
300
return new_conflicts, selected_conflicts
352
303
class Conflict(object):
353
304
"""Base class for all types of conflict"""
355
# FIXME: cleanup should take care of that ? -- vila 091229
356
306
has_files = False
358
308
def __init__(self, path, file_id=None):
360
# the factory blindly transfers the Stanza values to __init__ and
361
# Stanza is purely a Unicode api.
362
if isinstance(file_id, str):
363
file_id = cache_utf8.encode(file_id)
364
self.file_id = file_id
310
# warn turned off, because the factory blindly transfers the Stanza
311
# values to __init__ and Stanza is purely a Unicode api.
312
self.file_id = osutils.safe_file_id(file_id, warn=False)
366
314
def as_stanza(self):
367
315
s = rio.Stanza(type=self.typestring, path=self.path)
418
358
return None, conflict.typestring
420
def _do(self, action, tree):
421
"""Apply the specified action to the conflict.
423
:param action: The method name to call.
425
:param tree: The tree passed as a parameter to the method.
427
meth = getattr(self, 'action_%s' % action, None)
429
raise NotImplementedError(self.__class__.__name__ + '.' + action)
432
def associated_filenames(self):
433
"""The names of the files generated to help resolve the conflict."""
434
raise NotImplementedError(self.associated_filenames)
436
def cleanup(self, tree):
437
for fname in self.associated_filenames():
439
osutils.delete_any(tree.abspath(fname))
441
if e.errno != errno.ENOENT:
444
def action_auto(self, tree):
445
raise NotImplementedError(self.action_auto)
447
def action_done(self, tree):
448
"""Mark the conflict as solved once it has been handled."""
449
# This method does nothing but simplifies the design of upper levels.
452
def action_take_this(self, tree):
453
raise NotImplementedError(self.action_take_this)
455
def action_take_other(self, tree):
456
raise NotImplementedError(self.action_take_other)
458
def _resolve_with_cleanups(self, tree, *args, **kwargs):
459
with tree.transform() as tt:
460
self._resolve(tt, *args, **kwargs)
463
361
class PathConflict(Conflict):
464
362
"""A conflict was encountered merging file paths"""
479
376
s.add('conflict_path', self.conflict_path)
482
def associated_filenames(self):
483
# No additional files have been generated here
486
def _resolve(self, tt, file_id, path, winner):
487
"""Resolve the conflict.
489
:param tt: The TreeTransform where the conflict is resolved.
490
:param file_id: The retained file id.
491
:param path: The retained path.
492
:param winner: 'this' or 'other' indicates which side is the winner.
494
path_to_create = None
496
if self.path == '<deleted>':
497
return # Nothing to do
498
if self.conflict_path == '<deleted>':
499
path_to_create = self.path
500
revid = tt._tree.get_parent_ids()[0]
501
elif winner == 'other':
502
if self.conflict_path == '<deleted>':
503
return # Nothing to do
504
if self.path == '<deleted>':
505
path_to_create = self.conflict_path
506
# FIXME: If there are more than two parents we may need to
507
# iterate. Taking the last parent is the safer bet in the mean
508
# time. -- vila 20100309
509
revid = tt._tree.get_parent_ids()[-1]
512
raise AssertionError('bad winner: %r' % (winner,))
513
if path_to_create is not None:
514
tid = tt.trans_id_tree_path(path_to_create)
515
tree = self._revision_tree(tt._tree, revid)
516
transform.create_from_tree(
517
tt, tid, tree, tree.id2path(file_id))
518
tt.version_file(tid, file_id=file_id)
520
tid = tt.trans_id_file_id(file_id)
521
# Adjust the path for the retained file id
522
parent_tid = tt.get_tree_parent(tid)
523
tt.adjust_path(osutils.basename(path), parent_tid, tid)
526
def _revision_tree(self, tree, revid):
527
return tree.branch.repository.revision_tree(revid)
529
def _infer_file_id(self, tree):
530
# Prior to bug #531967, file_id wasn't always set, there may still be
531
# conflict files in the wild so we need to cope with them
532
# Establish which path we should use to find back the file-id
534
for p in (self.path, self.conflict_path):
536
# special hard-coded path
539
possible_paths.append(p)
540
# Search the file-id in the parents with any path available
542
for revid in tree.get_parent_ids():
543
revtree = self._revision_tree(tree, revid)
544
for p in possible_paths:
545
file_id = revtree.path2id(p)
546
if file_id is not None:
547
return revtree, file_id
550
def action_take_this(self, tree):
551
if self.file_id is not None:
552
self._resolve_with_cleanups(tree, self.file_id, self.path,
555
# Prior to bug #531967 we need to find back the file_id and restore
556
# the content from there
557
revtree, file_id = self._infer_file_id(tree)
558
tree.revert([revtree.id2path(file_id)],
559
old_tree=revtree, backups=False)
561
def action_take_other(self, tree):
562
if self.file_id is not None:
563
self._resolve_with_cleanups(tree, self.file_id,
567
# Prior to bug #531967 we need to find back the file_id and restore
568
# the content from there
569
revtree, file_id = self._infer_file_id(tree)
570
tree.revert([revtree.id2path(file_id)],
571
old_tree=revtree, backups=False)
574
380
class ContentsConflict(PathConflict):
575
"""The files are of different types (or both binary), or not present"""
381
"""The files are of different types, or not present"""
581
387
format = 'Contents conflict in %(path)s'
583
def associated_filenames(self):
584
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
586
def _resolve(self, tt, suffix_to_remove):
587
"""Resolve the conflict.
589
:param tt: The TreeTransform where the conflict is resolved.
590
:param suffix_to_remove: Either 'THIS' or 'OTHER'
592
The resolution is symmetric: when taking THIS, OTHER is deleted and
593
item.THIS is renamed into item and vice-versa.
596
# Delete 'item.THIS' or 'item.OTHER' depending on
599
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
600
except errors.NoSuchFile:
601
# There are valid cases where 'item.suffix_to_remove' either
602
# never existed or was already deleted (including the case
603
# where the user deleted it)
606
this_path = tt._tree.id2path(self.file_id)
607
except errors.NoSuchId:
608
# The file is not present anymore. This may happen if the user
609
# deleted the file either manually or when resolving a conflict on
610
# the parent. We may raise some exception to indicate that the
611
# conflict doesn't exist anymore and as such doesn't need to be
612
# resolved ? -- vila 20110615
615
this_tid = tt.trans_id_tree_path(this_path)
616
if this_tid is not None:
617
# Rename 'item.suffix_to_remove' (note that if
618
# 'item.suffix_to_remove' has been deleted, this is a no-op)
619
parent_tid = tt.get_tree_parent(this_tid)
620
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
623
def action_take_this(self, tree):
624
self._resolve_with_cleanups(tree, 'OTHER')
626
def action_take_other(self, tree):
627
self._resolve_with_cleanups(tree, 'THIS')
630
# TODO: There should be a base revid attribute to better inform the user about
631
# how the conflicts were generated.
632
class TextConflict(Conflict):
390
class TextConflict(PathConflict):
633
391
"""The merge algorithm could not resolve all differences encountered."""
639
397
format = 'Text conflict in %(path)s'
641
rformat = '%(class)s(%(path)r, %(file_id)r)'
643
_conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
645
def associated_filenames(self):
646
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
648
def _resolve(self, tt, winner_suffix):
649
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
651
:param tt: The TreeTransform where the conflict is resolved.
652
:param winner_suffix: Either 'THIS' or 'OTHER'
654
The resolution is symmetric, when taking THIS, item.THIS is renamed
655
into item and vice-versa. This takes one of the files as a whole
656
ignoring every difference that could have been merged cleanly.
658
# To avoid useless copies, we switch item and item.winner_suffix, only
659
# item will exist after the conflict has been resolved anyway.
660
item_tid = tt.trans_id_file_id(self.file_id)
661
item_parent_tid = tt.get_tree_parent(item_tid)
662
winner_path = self.path + '.' + winner_suffix
663
winner_tid = tt.trans_id_tree_path(winner_path)
664
winner_parent_tid = tt.get_tree_parent(winner_tid)
665
# Switch the paths to preserve the content
666
tt.adjust_path(osutils.basename(self.path),
667
winner_parent_tid, winner_tid)
668
tt.adjust_path(osutils.basename(winner_path),
669
item_parent_tid, item_tid)
670
# Associate the file_id to the right content
671
tt.unversion_file(item_tid)
672
tt.version_file(winner_tid, file_id=self.file_id)
675
def action_auto(self, tree):
676
# GZ 2012-07-27: Using NotImplementedError to signal that a conflict
677
# can't be auto resolved does not seem ideal.
679
kind = tree.kind(self.path)
680
except errors.NoSuchFile:
683
raise NotImplementedError("Conflict is not a file")
684
conflict_markers_in_line = self._conflict_re.search
685
# GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
686
with tree.get_file(self.path) as f:
688
if conflict_markers_in_line(line):
689
raise NotImplementedError("Conflict markers present")
691
def action_take_this(self, tree):
692
self._resolve_with_cleanups(tree, 'THIS')
694
def action_take_other(self, tree):
695
self._resolve_with_cleanups(tree, 'OTHER')
698
400
class HandledConflict(Conflict):
699
401
"""A path problem that has been provisionally resolved.
766
463
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
768
def action_take_this(self, tree):
769
tree.remove([self.conflict_path], force=True, keep_files=False)
770
tree.rename_one(self.path, self.conflict_path)
772
def action_take_other(self, tree):
773
tree.remove([self.path], force=True, keep_files=False)
776
466
class ParentLoop(HandledPathConflict):
777
467
"""An attempt to create an infinitely-looping directory structure.
778
468
This is rare, but can be produced like so:
787
477
typestring = 'parent loop'
789
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
791
def action_take_this(self, tree):
792
# just acccept brz proposal
795
def action_take_other(self, tree):
796
with tree.transform() as tt:
797
p_tid = tt.trans_id_file_id(self.file_id)
798
parent_tid = tt.get_tree_parent(p_tid)
799
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
800
cparent_tid = tt.get_tree_parent(cp_tid)
801
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
802
tt.adjust_path(osutils.basename(self.conflict_path),
479
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
807
482
class UnversionedParent(HandledConflict):
808
"""An attempt to version a file whose parent directory is not versioned.
483
"""An attempt to version an file whose parent directory is not versioned.
809
484
Typically, the result of a merge where one tree unversioned the directory
810
485
and the other added a versioned file to it.
815
490
format = 'Conflict because %(path)s is not versioned, but has versioned'\
816
491
' children. %(action)s.'
818
# FIXME: We silently do nothing to make tests pass, but most probably the
819
# conflict shouldn't exist (the long story is that the conflict is
820
# generated with another one that can be resolved properly) -- vila 091224
821
def action_take_this(self, tree):
824
def action_take_other(self, tree):
828
494
class MissingParent(HandledConflict):
829
495
"""An attempt to add files to a directory that is not present.
830
496
Typically, the result of a merge where THIS deleted the directory and
831
497
the OTHER added a file to it.
832
See also: DeletingParent (same situation, THIS and OTHER reversed)
498
See also: DeletingParent (same situation, reversed THIS and OTHER)
835
501
typestring = 'missing parent'
837
503
format = 'Conflict adding files to %(path)s. %(action)s.'
839
def action_take_this(self, tree):
840
tree.remove([self.path], force=True, keep_files=False)
842
def action_take_other(self, tree):
843
# just acccept brz proposal
847
506
class DeletingParent(HandledConflict):
848
507
"""An attempt to add files to a directory that is not present.
876
525
format = "Conflict: %(path)s is not a directory, but has files in it."\
879
# FIXME: .OTHER should be used instead of .new when the conflict is created
881
def action_take_this(self, tree):
882
# FIXME: we should preserve that path when the conflict is generated !
883
if self.path.endswith('.new'):
884
conflict_path = self.path[:-(len('.new'))]
885
tree.remove([self.path], force=True, keep_files=False)
886
tree.add(conflict_path)
888
raise NotImplementedError(self.action_take_this)
890
def action_take_other(self, tree):
891
# FIXME: we should preserve that path when the conflict is generated !
892
if self.path.endswith('.new'):
893
conflict_path = self.path[:-(len('.new'))]
894
tree.remove([conflict_path], force=True, keep_files=False)
895
tree.rename_one(self.path, conflict_path)
897
raise NotImplementedError(self.action_take_other)