58
55
Use bzr resolve when you have fixed a problem.
63
help='List paths of files with text conflicts.'),
59
help='List paths of files with text conflicts.'),
65
_see_also = ['resolve', 'conflict-types']
61
_see_also = ['resolve']
67
def run(self, text=False, directory=u'.'):
68
wt = workingtree.WorkingTree.open_containing(directory)[0]
63
def run(self, text=False):
64
from bzrlib.workingtree import WorkingTree
65
wt = WorkingTree.open_containing(u'.')[0]
69
66
for conflict in wt.conflicts():
71
68
if conflict.typestring != 'text conflict':
73
70
self.outf.write(conflict.path + '\n')
75
self.outf.write(unicode(conflict) + '\n')
78
resolve_action_registry = registry.Registry()
81
resolve_action_registry.register(
82
'done', 'done', 'Marks the conflict as resolved' )
83
resolve_action_registry.register(
84
'take-this', 'take_this',
85
'Resolve the conflict preserving the version in the working tree' )
86
resolve_action_registry.register(
87
'take-other', 'take_other',
88
'Resolve the conflict taking the merged version into account' )
89
resolve_action_registry.default_key = 'done'
91
class ResolveActionOption(option.RegistryOption):
94
super(ResolveActionOption, self).__init__(
95
'action', 'How to resolve the conflict.',
97
registry=resolve_action_registry)
72
self.outf.write(str(conflict) + '\n')
100
75
class cmd_resolve(commands.Command):
101
__doc__ = """Mark a conflict as resolved.
76
"""Mark a conflict as resolved.
103
78
Merge will do its best to combine the changes in two branches, but there
104
79
are some kinds of problems only a human can fix. When it encounters those,
112
87
aliases = ['resolved']
113
88
takes_args = ['file*']
116
option.Option('all', help='Resolve all conflicts in this tree.'),
117
ResolveActionOption(),
90
Option('all', help='Resolve all conflicts in this tree.'),
119
92
_see_also = ['conflicts']
120
def run(self, file_list=None, all=False, action=None, directory=None):
93
def run(self, file_list=None, all=False):
94
from bzrlib.workingtree import WorkingTree
123
97
raise errors.BzrCommandError("If --all is specified,"
124
98
" no FILE may be provided")
125
if directory is None:
127
tree = workingtree.WorkingTree.open_containing(directory)[0]
99
tree = WorkingTree.open_containing('.')[0]
131
tree, file_list = workingtree.WorkingTree.open_containing_paths(
132
file_list, directory)
133
if file_list is None:
135
# FIXME: There is a special case here related to the option
136
# handling that could be clearer and easier to discover by
137
# providing an --auto action (bug #344013 and #383396) and
138
# make it mandatory instead of implicit and active only
139
# when no file_list is provided -- vila 091229
102
tree, file_list = builtins.tree_files(file_list)
145
103
if file_list is None:
146
104
un_resolved, resolved = tree.auto_resolve()
147
105
if len(un_resolved) > 0:
148
106
trace.note('%d conflict(s) auto-resolved.', len(resolved))
149
107
trace.note('Remaining conflicts:')
150
108
for conflict in un_resolved:
151
trace.note(unicode(conflict))
154
112
trace.note('All conflicts resolved.')
157
# FIXME: This can never occur but the block above needs some
158
# refactoring to transfer tree.auto_resolve() to
159
# conflict.auto(tree) --vila 091242
162
before, after = resolve(tree, file_list, action=action)
163
trace.note('%d conflict(s) resolved, %d remaining'
164
% (before - after, after))
167
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
115
resolve(tree, file_list)
118
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
169
119
"""Resolve some or all of the conflicts in a working tree.
171
121
:param paths: If None, resolve all conflicts. Otherwise, select only
175
125
recursive commands like revert, this should be True. For commands
176
126
or applications wishing finer-grained control, like the resolve
177
127
command, this should be False.
178
:param ignore_misses: If False, warnings will be printed if the supplied
179
paths do not have conflicts.
180
:param action: How the conflict should be resolved,
128
:ignore_misses: If False, warnings will be printed if the supplied paths
129
do not have conflicts.
182
131
tree.lock_tree_write()
183
nb_conflicts_after = None
185
133
tree_conflicts = tree.conflicts()
186
nb_conflicts_before = len(tree_conflicts)
187
134
if paths is None:
188
135
new_conflicts = ConflictList()
189
to_process = tree_conflicts
136
selected_conflicts = tree_conflicts
191
new_conflicts, to_process = tree_conflicts.select_conflicts(
192
tree, paths, ignore_misses, recursive)
193
for conflict in to_process:
195
conflict._do(action, tree)
196
conflict.cleanup(tree)
197
except NotImplementedError:
198
new_conflicts.append(conflict)
138
new_conflicts, selected_conflicts = \
139
tree_conflicts.select_conflicts(tree, paths, ignore_misses,
200
nb_conflicts_after = len(new_conflicts)
201
142
tree.set_conflicts(new_conflicts)
202
143
except errors.UnsupportedOperation:
145
selected_conflicts.remove_files(tree)
206
if nb_conflicts_after is None:
207
nb_conflicts_after = nb_conflicts_before
208
return nb_conflicts_before, nb_conflicts_after
211
150
def restore(filename):
212
"""Restore a conflicted file to the state it was in before merging.
214
Only text restoration is supported at present.
152
Restore a conflicted file to the state it was in before merging.
153
Only text restoration supported at present.
216
155
conflicted = False
413
356
return None, conflict.typestring
415
def _do(self, action, tree):
416
"""Apply the specified action to the conflict.
418
:param action: The method name to call.
420
:param tree: The tree passed as a parameter to the method.
422
meth = getattr(self, 'action_%s' % action, None)
424
raise NotImplementedError(self.__class__.__name__ + '.' + action)
427
def associated_filenames(self):
428
"""The names of the files generated to help resolve the conflict."""
429
raise NotImplementedError(self.associated_filenames)
431
def cleanup(self, tree):
432
for fname in self.associated_filenames():
434
osutils.delete_any(tree.abspath(fname))
436
if e.errno != errno.ENOENT:
439
def action_done(self, tree):
440
"""Mark the conflict as solved once it has been handled."""
441
# This method does nothing but simplifies the design of upper levels.
444
def action_take_this(self, tree):
445
raise NotImplementedError(self.action_take_this)
447
def action_take_other(self, tree):
448
raise NotImplementedError(self.action_take_other)
450
def _resolve_with_cleanups(self, tree, *args, **kwargs):
451
tt = transform.TreeTransform(tree)
452
op = cleanup.OperationWithCleanups(self._resolve)
453
op.add_cleanup(tt.finalize)
454
op.run_simple(tt, *args, **kwargs)
457
359
class PathConflict(Conflict):
458
360
"""A conflict was encountered merging file paths"""
473
374
s.add('conflict_path', self.conflict_path)
476
def associated_filenames(self):
477
# No additional files have been generated here
480
def _resolve(self, tt, file_id, path, winner):
481
"""Resolve the conflict.
483
:param tt: The TreeTransform where the conflict is resolved.
484
:param file_id: The retained file id.
485
:param path: The retained path.
486
:param winner: 'this' or 'other' indicates which side is the winner.
488
path_to_create = None
490
if self.path == '<deleted>':
491
return # Nothing to do
492
if self.conflict_path == '<deleted>':
493
path_to_create = self.path
494
revid = tt._tree.get_parent_ids()[0]
495
elif winner == 'other':
496
if self.conflict_path == '<deleted>':
497
return # Nothing to do
498
if self.path == '<deleted>':
499
path_to_create = self.conflict_path
500
# FIXME: If there are more than two parents we may need to
501
# iterate. Taking the last parent is the safer bet in the mean
502
# time. -- vila 20100309
503
revid = tt._tree.get_parent_ids()[-1]
506
raise AssertionError('bad winner: %r' % (winner,))
507
if path_to_create is not None:
508
tid = tt.trans_id_tree_path(path_to_create)
509
transform.create_from_tree(
510
tt, tid, self._revision_tree(tt._tree, revid), file_id)
511
tt.version_file(file_id, tid)
513
tid = tt.trans_id_file_id(file_id)
514
# Adjust the path for the retained file id
515
parent_tid = tt.get_tree_parent(tid)
516
tt.adjust_path(osutils.basename(path), parent_tid, tid)
519
def _revision_tree(self, tree, revid):
520
return tree.branch.repository.revision_tree(revid)
522
def _infer_file_id(self, tree):
523
# Prior to bug #531967, file_id wasn't always set, there may still be
524
# conflict files in the wild so we need to cope with them
525
# Establish which path we should use to find back the file-id
527
for p in (self.path, self.conflict_path):
529
# special hard-coded path
532
possible_paths.append(p)
533
# Search the file-id in the parents with any path available
535
for revid in tree.get_parent_ids():
536
revtree = self._revision_tree(tree, revid)
537
for p in possible_paths:
538
file_id = revtree.path2id(p)
539
if file_id is not None:
540
return revtree, file_id
543
def action_take_this(self, tree):
544
if self.file_id is not None:
545
self._resolve_with_cleanups(tree, self.file_id, self.path,
548
# Prior to bug #531967 we need to find back the file_id and restore
549
# the content from there
550
revtree, file_id = self._infer_file_id(tree)
551
tree.revert([revtree.id2path(file_id)],
552
old_tree=revtree, backups=False)
554
def action_take_other(self, tree):
555
if self.file_id is not None:
556
self._resolve_with_cleanups(tree, self.file_id,
560
# Prior to bug #531967 we need to find back the file_id and restore
561
# the content from there
562
revtree, file_id = self._infer_file_id(tree)
563
tree.revert([revtree.id2path(file_id)],
564
old_tree=revtree, backups=False)
567
378
class ContentsConflict(PathConflict):
568
"""The files are of different types (or both binary), or not present"""
379
"""The files are of different types, or not present"""
574
385
format = 'Contents conflict in %(path)s'
576
def associated_filenames(self):
577
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
579
def _resolve(self, tt, suffix_to_remove):
580
"""Resolve the conflict.
582
:param tt: The TreeTransform where the conflict is resolved.
583
:param suffix_to_remove: Either 'THIS' or 'OTHER'
585
The resolution is symmetric: when taking THIS, OTHER is deleted and
586
item.THIS is renamed into item and vice-versa.
589
# Delete 'item.THIS' or 'item.OTHER' depending on
592
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
593
except errors.NoSuchFile:
594
# There are valid cases where 'item.suffix_to_remove' either
595
# never existed or was already deleted (including the case
596
# where the user deleted it)
599
this_path = tt._tree.id2path(self.file_id)
600
except errors.NoSuchId:
601
# The file is not present anymore. This may happen if the user
602
# deleted the file either manually or when resolving a conflict on
603
# the parent. We may raise some exception to indicate that the
604
# conflict doesn't exist anymore and as such doesn't need to be
605
# resolved ? -- vila 20110615
608
this_tid = tt.trans_id_tree_path(this_path)
609
if this_tid is not None:
610
# Rename 'item.suffix_to_remove' (note that if
611
# 'item.suffix_to_remove' has been deleted, this is a no-op)
612
parent_tid = tt.get_tree_parent(this_tid)
613
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
616
def action_take_this(self, tree):
617
self._resolve_with_cleanups(tree, 'OTHER')
619
def action_take_other(self, tree):
620
self._resolve_with_cleanups(tree, 'THIS')
623
# TODO: There should be a base revid attribute to better inform the user about
624
# how the conflicts were generated.
625
class TextConflict(Conflict):
388
class TextConflict(PathConflict):
626
389
"""The merge algorithm could not resolve all differences encountered."""
632
395
format = 'Text conflict in %(path)s'
634
rformat = '%(class)s(%(path)r, %(file_id)r)'
636
def associated_filenames(self):
637
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
639
def _resolve(self, tt, winner_suffix):
640
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
642
:param tt: The TreeTransform where the conflict is resolved.
643
:param winner_suffix: Either 'THIS' or 'OTHER'
645
The resolution is symmetric, when taking THIS, item.THIS is renamed
646
into item and vice-versa. This takes one of the files as a whole
647
ignoring every difference that could have been merged cleanly.
649
# To avoid useless copies, we switch item and item.winner_suffix, only
650
# item will exist after the conflict has been resolved anyway.
651
item_tid = tt.trans_id_file_id(self.file_id)
652
item_parent_tid = tt.get_tree_parent(item_tid)
653
winner_path = self.path + '.' + winner_suffix
654
winner_tid = tt.trans_id_tree_path(winner_path)
655
winner_parent_tid = tt.get_tree_parent(winner_tid)
656
# Switch the paths to preserve the content
657
tt.adjust_path(osutils.basename(self.path),
658
winner_parent_tid, winner_tid)
659
tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
660
# Associate the file_id to the right content
661
tt.unversion_file(item_tid)
662
tt.version_file(self.file_id, winner_tid)
665
def action_take_this(self, tree):
666
self._resolve_with_cleanups(tree, 'THIS')
668
def action_take_other(self, tree):
669
self._resolve_with_cleanups(tree, 'OTHER')
672
398
class HandledConflict(Conflict):
673
399
"""A path problem that has been provisionally resolved.
739
461
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
741
def action_take_this(self, tree):
742
tree.remove([self.conflict_path], force=True, keep_files=False)
743
tree.rename_one(self.path, self.conflict_path)
745
def action_take_other(self, tree):
746
tree.remove([self.path], force=True, keep_files=False)
749
464
class ParentLoop(HandledPathConflict):
750
465
"""An attempt to create an infinitely-looping directory structure.
751
466
This is rare, but can be produced like so:
760
475
typestring = 'parent loop'
762
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
764
def action_take_this(self, tree):
765
# just acccept bzr proposal
768
def action_take_other(self, tree):
769
tt = transform.TreeTransform(tree)
771
p_tid = tt.trans_id_file_id(self.file_id)
772
parent_tid = tt.get_tree_parent(p_tid)
773
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
774
cparent_tid = tt.get_tree_parent(cp_tid)
775
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
776
tt.adjust_path(osutils.basename(self.conflict_path),
477
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
783
480
class UnversionedParent(HandledConflict):
784
"""An attempt to version a file whose parent directory is not versioned.
481
"""An attempt to version an file whose parent directory is not versioned.
785
482
Typically, the result of a merge where one tree unversioned the directory
786
483
and the other added a versioned file to it.
791
488
format = 'Conflict because %(path)s is not versioned, but has versioned'\
792
489
' children. %(action)s.'
794
# FIXME: We silently do nothing to make tests pass, but most probably the
795
# conflict shouldn't exist (the long story is that the conflict is
796
# generated with another one that can be resolved properly) -- vila 091224
797
def action_take_this(self, tree):
800
def action_take_other(self, tree):
804
492
class MissingParent(HandledConflict):
805
493
"""An attempt to add files to a directory that is not present.
806
494
Typically, the result of a merge where THIS deleted the directory and
807
495
the OTHER added a file to it.
808
See also: DeletingParent (same situation, THIS and OTHER reversed)
496
See also: DeletingParent (same situation, reversed THIS and OTHER)
811
499
typestring = 'missing parent'
813
501
format = 'Conflict adding files to %(path)s. %(action)s.'
815
def action_take_this(self, tree):
816
tree.remove([self.path], force=True, keep_files=False)
818
def action_take_other(self, tree):
819
# just acccept bzr proposal
823
504
class DeletingParent(HandledConflict):
824
505
"""An attempt to add files to a directory that is not present.
831
512
format = "Conflict: can't delete %(path)s because it is not empty. "\
834
# FIXME: It's a bit strange that the default action is not coherent with
835
# MissingParent from the *user* pov.
837
def action_take_this(self, tree):
838
# just acccept bzr proposal
841
def action_take_other(self, tree):
842
tree.remove([self.path], force=True, keep_files=False)
845
516
class NonDirectoryParent(HandledConflict):
846
"""An attempt to add files to a directory that is not a directory or
517
"""An attempt to add files to a directory that is not a director or
847
518
an attempt to change the kind of a directory with files.
852
523
format = "Conflict: %(path)s is not a directory, but has files in it."\
855
# FIXME: .OTHER should be used instead of .new when the conflict is created
857
def action_take_this(self, tree):
858
# FIXME: we should preserve that path when the conflict is generated !
859
if self.path.endswith('.new'):
860
conflict_path = self.path[:-(len('.new'))]
861
tree.remove([self.path], force=True, keep_files=False)
862
tree.add(conflict_path)
864
raise NotImplementedError(self.action_take_this)
866
def action_take_other(self, tree):
867
# FIXME: we should preserve that path when the conflict is generated !
868
if self.path.endswith('.new'):
869
conflict_path = self.path[:-(len('.new'))]
870
tree.remove([conflict_path], force=True, keep_files=False)
871
tree.rename_one(self.path, conflict_path)
873
raise NotImplementedError(self.action_take_other)