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
# 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
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
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.'),
67
_see_also = ['resolve', 'conflict-types']
59
takes_options = [Option('text', help='list text conflicts by pathname')]
69
def run(self, text=False, directory=u'.'):
70
wt = workingtree.WorkingTree.open_containing(directory)[0]
61
def run(self, text=False):
62
from bzrlib.workingtree import WorkingTree
63
wt = WorkingTree.open_containing(u'.')[0]
71
64
for conflict in wt.conflicts():
73
66
if conflict.typestring != 'text conflict':
77
70
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
73
class cmd_resolve(commands.Command):
106
__doc__ = """Mark a conflict as resolved.
74
"""Mark a conflict as resolved.
108
76
Merge will do its best to combine the changes in two branches, but there
109
77
are some kinds of problems only a human can fix. When it encounters those,
110
78
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.
79
before you should commit.
81
Once you have fixed a problem, use "bzr resolve" to automatically mark
82
text conflicts as fixed, resolve FILE to mark a specific conflict as
83
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
85
See also bzr conflicts.
117
87
aliases = ['resolved']
118
88
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):
89
takes_options = [Option('all', help='Resolve all conflicts in this tree')]
90
def run(self, file_list=None, all=False):
91
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]
94
raise errors.BzrCommandError("If --all is specified,"
95
" no FILE may be provided")
96
tree = WorkingTree.open_containing('.')[0]
137
tree, file_list = workingtree.WorkingTree.open_containing_paths(
138
file_list, directory)
140
if file_list is None:
99
tree, file_list = builtins.tree_files(file_list)
100
if file_list is None:
101
un_resolved, resolved = tree.auto_resolve()
102
if len(un_resolved) > 0:
103
trace.note('%d conflict(s) auto-resolved.', len(resolved))
104
trace.note('Remaining conflicts:')
105
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))
109
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,
167
"""Resolve some or all of the conflicts in a working tree.
169
:param paths: If None, resolve all conflicts. Otherwise, select only
171
:param recursive: If True, then elements of paths which are directories
172
have all their children resolved, etc. When invoked as part of
173
recursive commands like revert, this should be True. For commands
174
or applications wishing finer-grained control, like the resolve
175
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,
180
nb_conflicts_after = None
181
with tree.lock_tree_write():
112
resolve(tree, file_list)
115
def resolve(tree, paths=None, ignore_misses=False):
116
tree.lock_tree_write()
182
118
tree_conflicts = tree.conflicts()
183
nb_conflicts_before = len(tree_conflicts)
184
119
if paths is None:
185
120
new_conflicts = ConflictList()
186
to_process = tree_conflicts
121
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)
123
new_conflicts, selected_conflicts = \
124
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
197
nb_conflicts_after = len(new_conflicts)
198
126
tree.set_conflicts(new_conflicts)
199
127
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
129
selected_conflicts.remove_files(tree)
206
134
def restore(filename):
207
"""Restore a conflicted file to the state it was in before merging.
209
Only text restoration is supported at present.
136
Restore a conflicted file to the state it was in before merging.
137
Only text restoration supported at present.
211
139
conflicted = False
213
141
osutils.rename(filename + ".THIS", filename)
214
142
conflicted = True
216
144
if e.errno != errno.ENOENT:
219
147
os.unlink(filename + ".BASE")
220
148
conflicted = True
222
150
if e.errno != errno.ENOENT:
225
153
os.unlink(filename + ".OTHER")
226
154
conflicted = True
228
156
if e.errno != errno.ENOENT:
230
158
if not conflicted:
418
334
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
337
class PathConflict(Conflict):
464
338
"""A conflict was encountered merging file paths"""
479
352
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
356
class ContentsConflict(PathConflict):
575
"""The files are of different types (or both binary), or not present"""
357
"""The files are of different types, or not present"""
581
363
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):
366
class TextConflict(PathConflict):
633
367
"""The merge algorithm could not resolve all differences encountered."""
639
373
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
376
class HandledConflict(Conflict):
699
377
"""A path problem that has been provisionally resolved.
766
439
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
442
class ParentLoop(HandledPathConflict):
777
443
"""An attempt to create an infinitely-looping directory structure.
778
444
This is rare, but can be produced like so:
787
453
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),
455
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
807
458
class UnversionedParent(HandledConflict):
808
"""An attempt to version a file whose parent directory is not versioned.
459
"""An attempt to version an file whose parent directory is not versioned.
809
460
Typically, the result of a merge where one tree unversioned the directory
810
461
and the other added a versioned file to it.
815
466
format = 'Conflict because %(path)s is not versioned, but has versioned'\
816
467
' 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
470
class MissingParent(HandledConflict):
829
471
"""An attempt to add files to a directory that is not present.
830
472
Typically, the result of a merge where THIS deleted the directory and
831
473
the OTHER added a file to it.
832
See also: DeletingParent (same situation, THIS and OTHER reversed)
474
See also: DeletingParent (same situation, reversed THIS and OTHER)
835
477
typestring = 'missing parent'
837
479
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
482
class DeletingParent(HandledConflict):
848
483
"""An attempt to add files to a directory that is not present.
855
490
format = "Conflict: can't delete %(path)s because it is not empty. "\
858
# FIXME: It's a bit strange that the default action is not coherent with
859
# MissingParent from the *user* pov.
861
def action_take_this(self, tree):
862
# just acccept brz proposal
865
def action_take_other(self, tree):
866
tree.remove([self.path], force=True, keep_files=False)
869
class NonDirectoryParent(HandledConflict):
870
"""An attempt to add files to a directory that is not a directory or
871
an attempt to change the kind of a directory with files.
874
typestring = 'non-directory parent'
876
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)