28
28
from bzrlib import (
37
from bzrlib.i18n import gettext, ngettext
37
from bzrlib.option import Option
40
46
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
43
49
class cmd_conflicts(commands.Command):
44
"""List files with conflicts.
50
__doc__ = """List files with conflicts.
46
52
Merge will do its best to combine the changes in two branches, but there
47
53
are some kinds of problems only a human can fix. When it encounters those,
48
54
it will mark a conflict. A conflict means that you need to fix something,
49
before you should commit.
55
before you can commit.
51
57
Conflicts normally are listed as short, human-readable messages. If --text
52
58
is supplied, the pathnames of files with text conflicts are listed,
53
59
instead. (This is useful for editing all files with text conflicts.)
55
61
Use bzr resolve when you have fixed a problem.
61
help='List paths of files with text conflicts.'),
66
help='List paths of files with text conflicts.'),
68
_see_also = ['resolve', 'conflict-types']
64
def run(self, text=False):
65
from bzrlib.workingtree import WorkingTree
66
wt = WorkingTree.open_containing(u'.')[0]
70
def run(self, text=False, directory=u'.'):
71
wt = workingtree.WorkingTree.open_containing(directory)[0]
67
72
for conflict in wt.conflicts():
69
74
if conflict.typestring != 'text conflict':
71
76
self.outf.write(conflict.path + '\n')
73
self.outf.write(str(conflict) + '\n')
78
self.outf.write(unicode(conflict) + '\n')
81
resolve_action_registry = registry.Registry()
84
resolve_action_registry.register(
85
'done', 'done', 'Marks the conflict as resolved.')
86
resolve_action_registry.register(
87
'take-this', 'take_this',
88
'Resolve the conflict preserving the version in the working tree.')
89
resolve_action_registry.register(
90
'take-other', 'take_other',
91
'Resolve the conflict taking the merged version into account.')
92
resolve_action_registry.default_key = 'done'
94
class ResolveActionOption(option.RegistryOption):
97
super(ResolveActionOption, self).__init__(
98
'action', 'How to resolve the conflict.',
100
registry=resolve_action_registry)
76
103
class cmd_resolve(commands.Command):
77
"""Mark a conflict as resolved.
104
__doc__ = """Mark a conflict as resolved.
79
106
Merge will do its best to combine the changes in two branches, but there
80
107
are some kinds of problems only a human can fix. When it encounters those,
81
108
it will mark a conflict. A conflict means that you need to fix something,
82
before you should commit.
109
before you can commit.
84
111
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
112
text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
86
113
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
88
See also bzr conflicts.
90
115
aliases = ['resolved']
91
116
takes_args = ['file*']
93
Option('all', help='Resolve all conflicts in this tree.'),
119
option.Option('all', help='Resolve all conflicts in this tree.'),
120
ResolveActionOption(),
95
def run(self, file_list=None, all=False):
96
from bzrlib.workingtree import WorkingTree
122
_see_also = ['conflicts']
123
def run(self, file_list=None, all=False, action=None, directory=None):
99
raise errors.BzrCommandError("If --all is specified,"
100
" no FILE may be provided")
101
tree = WorkingTree.open_containing('.')[0]
126
raise errors.BzrCommandError(gettext("If --all is specified,"
127
" no FILE may be provided"))
128
if directory is None:
130
tree = workingtree.WorkingTree.open_containing(directory)[0]
104
tree, file_list = builtins.tree_files(file_list)
134
tree, file_list = workingtree.WorkingTree.open_containing_paths(
135
file_list, directory)
136
if file_list is None:
138
# FIXME: There is a special case here related to the option
139
# handling that could be clearer and easier to discover by
140
# providing an --auto action (bug #344013 and #383396) and
141
# make it mandatory instead of implicit and active only
142
# when no file_list is provided -- vila 091229
105
148
if file_list is None:
106
149
un_resolved, resolved = tree.auto_resolve()
107
150
if len(un_resolved) > 0:
108
trace.note('%d conflict(s) auto-resolved.', len(resolved))
109
trace.note('Remaining conflicts:')
151
trace.note(ngettext('%d conflict auto-resolved.',
152
'%d conflicts auto-resolved.', len(resolved)),
154
trace.note(gettext('Remaining conflicts:'))
110
155
for conflict in un_resolved:
156
trace.note(unicode(conflict))
114
trace.note('All conflicts resolved.')
159
trace.note(gettext('All conflicts resolved.'))
117
resolve(tree, file_list)
120
def resolve(tree, paths=None, ignore_misses=False):
162
# FIXME: This can never occur but the block above needs some
163
# refactoring to transfer tree.auto_resolve() to
164
# conflict.auto(tree) --vila 091242
167
before, after = resolve(tree, file_list, action=action)
168
trace.note(ngettext('{0} conflict resolved, {1} remaining',
169
'{0} conflicts resolved, {1} remaining',
170
before-after).format(before - after, after))
173
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
175
"""Resolve some or all of the conflicts in a working tree.
177
:param paths: If None, resolve all conflicts. Otherwise, select only
179
:param recursive: If True, then elements of paths which are directories
180
have all their children resolved, etc. When invoked as part of
181
recursive commands like revert, this should be True. For commands
182
or applications wishing finer-grained control, like the resolve
183
command, this should be False.
184
:param ignore_misses: If False, warnings will be printed if the supplied
185
paths do not have conflicts.
186
:param action: How the conflict should be resolved,
121
188
tree.lock_tree_write()
189
nb_conflicts_after = None
123
191
tree_conflicts = tree.conflicts()
192
nb_conflicts_before = len(tree_conflicts)
124
193
if paths is None:
125
194
new_conflicts = ConflictList()
126
selected_conflicts = tree_conflicts
195
to_process = tree_conflicts
128
new_conflicts, selected_conflicts = \
129
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
197
new_conflicts, to_process = tree_conflicts.select_conflicts(
198
tree, paths, ignore_misses, recursive)
199
for conflict in to_process:
201
conflict._do(action, tree)
202
conflict.cleanup(tree)
203
except NotImplementedError:
204
new_conflicts.append(conflict)
206
nb_conflicts_after = len(new_conflicts)
131
207
tree.set_conflicts(new_conflicts)
132
208
except errors.UnsupportedOperation:
134
selected_conflicts.remove_files(tree)
212
if nb_conflicts_after is None:
213
nb_conflicts_after = nb_conflicts_before
214
return nb_conflicts_before, nb_conflicts_after
139
217
def restore(filename):
141
Restore a conflicted file to the state it was in before merging.
142
Only text restoration supported at present.
218
"""Restore a conflicted file to the state it was in before merging.
220
Only text restoration is supported at present.
144
222
conflicted = False
345
419
return None, conflict.typestring
421
def _do(self, action, tree):
422
"""Apply the specified action to the conflict.
424
:param action: The method name to call.
426
:param tree: The tree passed as a parameter to the method.
428
meth = getattr(self, 'action_%s' % action, None)
430
raise NotImplementedError(self.__class__.__name__ + '.' + action)
433
def associated_filenames(self):
434
"""The names of the files generated to help resolve the conflict."""
435
raise NotImplementedError(self.associated_filenames)
437
def cleanup(self, tree):
438
for fname in self.associated_filenames():
440
osutils.delete_any(tree.abspath(fname))
442
if e.errno != errno.ENOENT:
445
def action_done(self, tree):
446
"""Mark the conflict as solved once it has been handled."""
447
# This method does nothing but simplifies the design of upper levels.
450
def action_take_this(self, tree):
451
raise NotImplementedError(self.action_take_this)
453
def action_take_other(self, tree):
454
raise NotImplementedError(self.action_take_other)
456
def _resolve_with_cleanups(self, tree, *args, **kwargs):
457
tt = transform.TreeTransform(tree)
458
op = cleanup.OperationWithCleanups(self._resolve)
459
op.add_cleanup(tt.finalize)
460
op.run_simple(tt, *args, **kwargs)
348
463
class PathConflict(Conflict):
349
464
"""A conflict was encountered merging file paths"""
363
479
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
transform.create_from_tree(
516
tt, tid, self._revision_tree(tt._tree, revid), file_id)
517
tt.version_file(file_id, tid)
519
tid = tt.trans_id_file_id(file_id)
520
# Adjust the path for the retained file id
521
parent_tid = tt.get_tree_parent(tid)
522
tt.adjust_path(osutils.basename(path), parent_tid, tid)
525
def _revision_tree(self, tree, revid):
526
return tree.branch.repository.revision_tree(revid)
528
def _infer_file_id(self, tree):
529
# Prior to bug #531967, file_id wasn't always set, there may still be
530
# conflict files in the wild so we need to cope with them
531
# Establish which path we should use to find back the file-id
533
for p in (self.path, self.conflict_path):
535
# special hard-coded path
538
possible_paths.append(p)
539
# Search the file-id in the parents with any path available
541
for revid in tree.get_parent_ids():
542
revtree = self._revision_tree(tree, revid)
543
for p in possible_paths:
544
file_id = revtree.path2id(p)
545
if file_id is not None:
546
return revtree, file_id
549
def action_take_this(self, tree):
550
if self.file_id is not None:
551
self._resolve_with_cleanups(tree, self.file_id, self.path,
554
# Prior to bug #531967 we need to find back the file_id and restore
555
# the content from there
556
revtree, file_id = self._infer_file_id(tree)
557
tree.revert([revtree.id2path(file_id)],
558
old_tree=revtree, backups=False)
560
def action_take_other(self, tree):
561
if self.file_id is not None:
562
self._resolve_with_cleanups(tree, self.file_id,
566
# Prior to bug #531967 we need to find back the file_id and restore
567
# the content from there
568
revtree, file_id = self._infer_file_id(tree)
569
tree.revert([revtree.id2path(file_id)],
570
old_tree=revtree, backups=False)
367
573
class ContentsConflict(PathConflict):
368
"""The files are of different types, or not present"""
574
"""The files are of different types (or both binary), or not present"""
374
580
format = 'Contents conflict in %(path)s'
377
class TextConflict(PathConflict):
582
def associated_filenames(self):
583
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
585
def _resolve(self, tt, suffix_to_remove):
586
"""Resolve the conflict.
588
:param tt: The TreeTransform where the conflict is resolved.
589
:param suffix_to_remove: Either 'THIS' or 'OTHER'
591
The resolution is symmetric: when taking THIS, OTHER is deleted and
592
item.THIS is renamed into item and vice-versa.
595
# Delete 'item.THIS' or 'item.OTHER' depending on
598
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
599
except errors.NoSuchFile:
600
# There are valid cases where 'item.suffix_to_remove' either
601
# never existed or was already deleted (including the case
602
# where the user deleted it)
605
this_path = tt._tree.id2path(self.file_id)
606
except errors.NoSuchId:
607
# The file is not present anymore. This may happen if the user
608
# deleted the file either manually or when resolving a conflict on
609
# the parent. We may raise some exception to indicate that the
610
# conflict doesn't exist anymore and as such doesn't need to be
611
# resolved ? -- vila 20110615
614
this_tid = tt.trans_id_tree_path(this_path)
615
if this_tid is not None:
616
# Rename 'item.suffix_to_remove' (note that if
617
# 'item.suffix_to_remove' has been deleted, this is a no-op)
618
parent_tid = tt.get_tree_parent(this_tid)
619
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
622
def action_take_this(self, tree):
623
self._resolve_with_cleanups(tree, 'OTHER')
625
def action_take_other(self, tree):
626
self._resolve_with_cleanups(tree, 'THIS')
629
# TODO: There should be a base revid attribute to better inform the user about
630
# how the conflicts were generated.
631
class TextConflict(Conflict):
378
632
"""The merge algorithm could not resolve all differences encountered."""
384
638
format = 'Text conflict in %(path)s'
640
rformat = '%(class)s(%(path)r, %(file_id)r)'
642
def associated_filenames(self):
643
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
645
def _resolve(self, tt, winner_suffix):
646
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
648
:param tt: The TreeTransform where the conflict is resolved.
649
:param winner_suffix: Either 'THIS' or 'OTHER'
651
The resolution is symmetric, when taking THIS, item.THIS is renamed
652
into item and vice-versa. This takes one of the files as a whole
653
ignoring every difference that could have been merged cleanly.
655
# To avoid useless copies, we switch item and item.winner_suffix, only
656
# item will exist after the conflict has been resolved anyway.
657
item_tid = tt.trans_id_file_id(self.file_id)
658
item_parent_tid = tt.get_tree_parent(item_tid)
659
winner_path = self.path + '.' + winner_suffix
660
winner_tid = tt.trans_id_tree_path(winner_path)
661
winner_parent_tid = tt.get_tree_parent(winner_tid)
662
# Switch the paths to preserve the content
663
tt.adjust_path(osutils.basename(self.path),
664
winner_parent_tid, winner_tid)
665
tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
666
# Associate the file_id to the right content
667
tt.unversion_file(item_tid)
668
tt.version_file(self.file_id, winner_tid)
671
def action_take_this(self, tree):
672
self._resolve_with_cleanups(tree, 'THIS')
674
def action_take_other(self, tree):
675
self._resolve_with_cleanups(tree, 'OTHER')
387
678
class HandledConflict(Conflict):
388
679
"""A path problem that has been provisionally resolved.
450
745
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
747
def action_take_this(self, tree):
748
tree.remove([self.conflict_path], force=True, keep_files=False)
749
tree.rename_one(self.path, self.conflict_path)
751
def action_take_other(self, tree):
752
tree.remove([self.path], force=True, keep_files=False)
453
755
class ParentLoop(HandledPathConflict):
454
756
"""An attempt to create an infinitely-looping directory structure.
455
757
This is rare, but can be produced like so:
464
766
typestring = 'parent loop'
466
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
768
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
770
def action_take_this(self, tree):
771
# just acccept bzr proposal
774
def action_take_other(self, tree):
775
tt = transform.TreeTransform(tree)
777
p_tid = tt.trans_id_file_id(self.file_id)
778
parent_tid = tt.get_tree_parent(p_tid)
779
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
780
cparent_tid = tt.get_tree_parent(cp_tid)
781
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
782
tt.adjust_path(osutils.basename(self.conflict_path),
469
789
class UnversionedParent(HandledConflict):
470
"""An attempt to version an file whose parent directory is not versioned.
790
"""An attempt to version a file whose parent directory is not versioned.
471
791
Typically, the result of a merge where one tree unversioned the directory
472
792
and the other added a versioned file to it.
501
837
format = "Conflict: can't delete %(path)s because it is not empty. "\
840
# FIXME: It's a bit strange that the default action is not coherent with
841
# MissingParent from the *user* pov.
843
def action_take_this(self, tree):
844
# just acccept bzr proposal
847
def action_take_other(self, tree):
848
tree.remove([self.path], force=True, keep_files=False)
851
class NonDirectoryParent(HandledConflict):
852
"""An attempt to add files to a directory that is not a directory or
853
an attempt to change the kind of a directory with files.
856
typestring = 'non-directory parent'
858
format = "Conflict: %(path)s is not a directory, but has files in it."\
861
# FIXME: .OTHER should be used instead of .new when the conflict is created
863
def action_take_this(self, tree):
864
# FIXME: we should preserve that path when the conflict is generated !
865
if self.path.endswith('.new'):
866
conflict_path = self.path[:-(len('.new'))]
867
tree.remove([self.path], force=True, keep_files=False)
868
tree.add(conflict_path)
870
raise NotImplementedError(self.action_take_this)
872
def action_take_other(self, tree):
873
# FIXME: we should preserve that path when the conflict is generated !
874
if self.path.endswith('.new'):
875
conflict_path = self.path[:-(len('.new'))]
876
tree.remove([conflict_path], force=True, keep_files=False)
877
tree.rename_one(self.path, conflict_path)
879
raise NotImplementedError(self.action_take_other)