57
53
instead. (This is useful for editing all files with text conflicts.)
59
55
Use bzr resolve when you have fixed a problem.
64
help='List paths of files with text conflicts.'),
66
_see_also = ['resolve', 'conflict-types']
59
takes_options = [Option('text', help='list text conflicts by pathname')]
68
def run(self, text=False, directory=u'.'):
69
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]
70
64
for conflict in wt.conflicts():
72
66
if conflict.typestring != 'text conflict':
74
68
self.outf.write(conflict.path + '\n')
76
self.outf.write(unicode(conflict) + '\n')
79
resolve_action_registry = registry.Registry()
82
resolve_action_registry.register(
83
'done', 'done', 'Marks the conflict as resolved' )
84
resolve_action_registry.register(
85
'take-this', 'take_this',
86
'Resolve the conflict preserving the version in the working tree' )
87
resolve_action_registry.register(
88
'take-other', 'take_other',
89
'Resolve the conflict taking the merged version into account' )
90
resolve_action_registry.default_key = 'done'
92
class ResolveActionOption(option.RegistryOption):
95
super(ResolveActionOption, self).__init__(
96
'action', 'How to resolve the conflict.',
98
registry=resolve_action_registry)
70
self.outf.write(str(conflict) + '\n')
101
73
class cmd_resolve(commands.Command):
102
__doc__ = """Mark a conflict as resolved.
74
"""Mark a conflict as resolved.
104
76
Merge will do its best to combine the changes in two branches, but there
105
77
are some kinds of problems only a human can fix. When it encounters those,
107
79
before you should commit.
109
81
Once you have fixed a problem, use "bzr resolve" to automatically mark
110
text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
82
text conflicts as fixed, resolve FILE to mark a specific conflict as
111
83
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
85
See also bzr conflicts.
113
87
aliases = ['resolved']
114
88
takes_args = ['file*']
117
option.Option('all', help='Resolve all conflicts in this tree.'),
118
ResolveActionOption(),
120
_see_also = ['conflicts']
121
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
124
raise errors.BzrCommandError(gettext("If --all is specified,"
125
" no FILE may be provided"))
126
if directory is None:
128
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]
132
tree, file_list = workingtree.WorkingTree.open_containing_paths(
133
file_list, directory)
134
if file_list is None:
136
# FIXME: There is a special case here related to the option
137
# handling that could be clearer and easier to discover by
138
# providing an --auto action (bug #344013 and #383396) and
139
# make it mandatory instead of implicit and active only
140
# when no file_list is provided -- vila 091229
99
tree, file_list = builtins.tree_files(file_list)
146
100
if file_list is None:
147
101
un_resolved, resolved = tree.auto_resolve()
148
102
if len(un_resolved) > 0:
149
trace.note(ngettext('%d conflict auto-resolved.',
150
'%d conflicts auto-resolved.', len(resolved)),
152
trace.note(gettext('Remaining conflicts:'))
103
trace.note('%d conflict(s) auto-resolved.', len(resolved))
104
trace.note('Remaining conflicts:')
153
105
for conflict in un_resolved:
154
trace.note(unicode(conflict))
157
trace.note(gettext('All conflicts resolved.'))
109
trace.note('All conflicts resolved.')
160
# FIXME: This can never occur but the block above needs some
161
# refactoring to transfer tree.auto_resolve() to
162
# conflict.auto(tree) --vila 091242
165
before, after = resolve(tree, file_list, action=action)
166
trace.note(ngettext('{0} conflict resolved, {1} remaining',
167
'{0} conflicts resolved, {1} remaining',
168
before-after).format(before - after, after))
171
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
173
"""Resolve some or all of the conflicts in a working tree.
175
:param paths: If None, resolve all conflicts. Otherwise, select only
177
:param recursive: If True, then elements of paths which are directories
178
have all their children resolved, etc. When invoked as part of
179
recursive commands like revert, this should be True. For commands
180
or applications wishing finer-grained control, like the resolve
181
command, this should be False.
182
:param ignore_misses: If False, warnings will be printed if the supplied
183
paths do not have conflicts.
184
:param action: How the conflict should be resolved,
112
resolve(tree, file_list)
115
def resolve(tree, paths=None, ignore_misses=False):
186
116
tree.lock_tree_write()
187
nb_conflicts_after = None
189
118
tree_conflicts = tree.conflicts()
190
nb_conflicts_before = len(tree_conflicts)
191
119
if paths is None:
192
120
new_conflicts = ConflictList()
193
to_process = tree_conflicts
121
selected_conflicts = tree_conflicts
195
new_conflicts, to_process = tree_conflicts.select_conflicts(
196
tree, paths, ignore_misses, recursive)
197
for conflict in to_process:
199
conflict._do(action, tree)
200
conflict.cleanup(tree)
201
except NotImplementedError:
202
new_conflicts.append(conflict)
123
new_conflicts, selected_conflicts = \
124
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
204
nb_conflicts_after = len(new_conflicts)
205
126
tree.set_conflicts(new_conflicts)
206
127
except errors.UnsupportedOperation:
129
selected_conflicts.remove_files(tree)
210
if nb_conflicts_after is None:
211
nb_conflicts_after = nb_conflicts_before
212
return nb_conflicts_before, nb_conflicts_after
215
134
def restore(filename):
216
"""Restore a conflicted file to the state it was in before merging.
218
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.
220
139
conflicted = False
291
210
"""Generator of stanzas"""
292
211
for conflict in self:
293
212
yield conflict.as_stanza()
295
214
def to_strings(self):
296
215
"""Generate strings for the provided conflicts"""
297
216
for conflict in self:
298
yield unicode(conflict)
300
219
def remove_files(self, tree):
301
220
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
302
221
for conflict in self:
303
222
if not conflict.has_files:
305
conflict.cleanup(tree)
224
for suffix in CONFLICT_SUFFIXES:
226
osutils.delete_any(tree.abspath(conflict.path+suffix))
228
if e.errno != errno.ENOENT:
307
def select_conflicts(self, tree, paths, ignore_misses=False,
231
def select_conflicts(self, tree, paths, ignore_misses=False):
309
232
"""Select the conflicts associated with paths in a tree.
311
234
File-ids are also used for this.
312
235
:return: a pair of ConflictLists: (not_selected, selected)
417
334
return None, conflict.typestring
419
def _do(self, action, tree):
420
"""Apply the specified action to the conflict.
422
:param action: The method name to call.
424
:param tree: The tree passed as a parameter to the method.
426
meth = getattr(self, 'action_%s' % action, None)
428
raise NotImplementedError(self.__class__.__name__ + '.' + action)
431
def associated_filenames(self):
432
"""The names of the files generated to help resolve the conflict."""
433
raise NotImplementedError(self.associated_filenames)
435
def cleanup(self, tree):
436
for fname in self.associated_filenames():
438
osutils.delete_any(tree.abspath(fname))
440
if e.errno != errno.ENOENT:
443
def action_done(self, tree):
444
"""Mark the conflict as solved once it has been handled."""
445
# This method does nothing but simplifies the design of upper levels.
448
def action_take_this(self, tree):
449
raise NotImplementedError(self.action_take_this)
451
def action_take_other(self, tree):
452
raise NotImplementedError(self.action_take_other)
454
def _resolve_with_cleanups(self, tree, *args, **kwargs):
455
tt = transform.TreeTransform(tree)
456
op = cleanup.OperationWithCleanups(self._resolve)
457
op.add_cleanup(tt.finalize)
458
op.run_simple(tt, *args, **kwargs)
461
337
class PathConflict(Conflict):
462
338
"""A conflict was encountered merging file paths"""
477
352
s.add('conflict_path', self.conflict_path)
480
def associated_filenames(self):
481
# No additional files have been generated here
484
def _resolve(self, tt, file_id, path, winner):
485
"""Resolve the conflict.
487
:param tt: The TreeTransform where the conflict is resolved.
488
:param file_id: The retained file id.
489
:param path: The retained path.
490
:param winner: 'this' or 'other' indicates which side is the winner.
492
path_to_create = None
494
if self.path == '<deleted>':
495
return # Nothing to do
496
if self.conflict_path == '<deleted>':
497
path_to_create = self.path
498
revid = tt._tree.get_parent_ids()[0]
499
elif winner == 'other':
500
if self.conflict_path == '<deleted>':
501
return # Nothing to do
502
if self.path == '<deleted>':
503
path_to_create = self.conflict_path
504
# FIXME: If there are more than two parents we may need to
505
# iterate. Taking the last parent is the safer bet in the mean
506
# time. -- vila 20100309
507
revid = tt._tree.get_parent_ids()[-1]
510
raise AssertionError('bad winner: %r' % (winner,))
511
if path_to_create is not None:
512
tid = tt.trans_id_tree_path(path_to_create)
513
transform.create_from_tree(
514
tt, tid, self._revision_tree(tt._tree, revid), file_id)
515
tt.version_file(file_id, tid)
517
tid = tt.trans_id_file_id(file_id)
518
# Adjust the path for the retained file id
519
parent_tid = tt.get_tree_parent(tid)
520
tt.adjust_path(osutils.basename(path), parent_tid, tid)
523
def _revision_tree(self, tree, revid):
524
return tree.branch.repository.revision_tree(revid)
526
def _infer_file_id(self, tree):
527
# Prior to bug #531967, file_id wasn't always set, there may still be
528
# conflict files in the wild so we need to cope with them
529
# Establish which path we should use to find back the file-id
531
for p in (self.path, self.conflict_path):
533
# special hard-coded path
536
possible_paths.append(p)
537
# Search the file-id in the parents with any path available
539
for revid in tree.get_parent_ids():
540
revtree = self._revision_tree(tree, revid)
541
for p in possible_paths:
542
file_id = revtree.path2id(p)
543
if file_id is not None:
544
return revtree, file_id
547
def action_take_this(self, tree):
548
if self.file_id is not None:
549
self._resolve_with_cleanups(tree, self.file_id, self.path,
552
# Prior to bug #531967 we need to find back the file_id and restore
553
# the content from there
554
revtree, file_id = self._infer_file_id(tree)
555
tree.revert([revtree.id2path(file_id)],
556
old_tree=revtree, backups=False)
558
def action_take_other(self, tree):
559
if self.file_id is not None:
560
self._resolve_with_cleanups(tree, self.file_id,
564
# Prior to bug #531967 we need to find back the file_id and restore
565
# the content from there
566
revtree, file_id = self._infer_file_id(tree)
567
tree.revert([revtree.id2path(file_id)],
568
old_tree=revtree, backups=False)
571
356
class ContentsConflict(PathConflict):
572
"""The files are of different types (or both binary), or not present"""
357
"""The files are of different types, or not present"""
578
363
format = 'Contents conflict in %(path)s'
580
def associated_filenames(self):
581
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
583
def _resolve(self, tt, suffix_to_remove):
584
"""Resolve the conflict.
586
:param tt: The TreeTransform where the conflict is resolved.
587
:param suffix_to_remove: Either 'THIS' or 'OTHER'
589
The resolution is symmetric: when taking THIS, OTHER is deleted and
590
item.THIS is renamed into item and vice-versa.
593
# Delete 'item.THIS' or 'item.OTHER' depending on
596
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
597
except errors.NoSuchFile:
598
# There are valid cases where 'item.suffix_to_remove' either
599
# never existed or was already deleted (including the case
600
# where the user deleted it)
603
this_path = tt._tree.id2path(self.file_id)
604
except errors.NoSuchId:
605
# The file is not present anymore. This may happen if the user
606
# deleted the file either manually or when resolving a conflict on
607
# the parent. We may raise some exception to indicate that the
608
# conflict doesn't exist anymore and as such doesn't need to be
609
# resolved ? -- vila 20110615
612
this_tid = tt.trans_id_tree_path(this_path)
613
if this_tid is not None:
614
# Rename 'item.suffix_to_remove' (note that if
615
# 'item.suffix_to_remove' has been deleted, this is a no-op)
616
parent_tid = tt.get_tree_parent(this_tid)
617
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
620
def action_take_this(self, tree):
621
self._resolve_with_cleanups(tree, 'OTHER')
623
def action_take_other(self, tree):
624
self._resolve_with_cleanups(tree, 'THIS')
627
# TODO: There should be a base revid attribute to better inform the user about
628
# how the conflicts were generated.
629
class TextConflict(Conflict):
366
class TextConflict(PathConflict):
630
367
"""The merge algorithm could not resolve all differences encountered."""
636
373
format = 'Text conflict in %(path)s'
638
rformat = '%(class)s(%(path)r, %(file_id)r)'
640
def associated_filenames(self):
641
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
643
def _resolve(self, tt, winner_suffix):
644
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
646
:param tt: The TreeTransform where the conflict is resolved.
647
:param winner_suffix: Either 'THIS' or 'OTHER'
649
The resolution is symmetric, when taking THIS, item.THIS is renamed
650
into item and vice-versa. This takes one of the files as a whole
651
ignoring every difference that could have been merged cleanly.
653
# To avoid useless copies, we switch item and item.winner_suffix, only
654
# item will exist after the conflict has been resolved anyway.
655
item_tid = tt.trans_id_file_id(self.file_id)
656
item_parent_tid = tt.get_tree_parent(item_tid)
657
winner_path = self.path + '.' + winner_suffix
658
winner_tid = tt.trans_id_tree_path(winner_path)
659
winner_parent_tid = tt.get_tree_parent(winner_tid)
660
# Switch the paths to preserve the content
661
tt.adjust_path(osutils.basename(self.path),
662
winner_parent_tid, winner_tid)
663
tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
664
# Associate the file_id to the right content
665
tt.unversion_file(item_tid)
666
tt.version_file(self.file_id, winner_tid)
669
def action_take_this(self, tree):
670
self._resolve_with_cleanups(tree, 'THIS')
672
def action_take_other(self, tree):
673
self._resolve_with_cleanups(tree, 'OTHER')
676
376
class HandledConflict(Conflict):
677
377
"""A path problem that has been provisionally resolved.
743
439
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
745
def action_take_this(self, tree):
746
tree.remove([self.conflict_path], force=True, keep_files=False)
747
tree.rename_one(self.path, self.conflict_path)
749
def action_take_other(self, tree):
750
tree.remove([self.path], force=True, keep_files=False)
753
442
class ParentLoop(HandledPathConflict):
754
443
"""An attempt to create an infinitely-looping directory structure.
755
444
This is rare, but can be produced like so:
764
453
typestring = 'parent loop'
766
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
768
def action_take_this(self, tree):
769
# just acccept bzr proposal
772
def action_take_other(self, tree):
773
tt = transform.TreeTransform(tree)
775
p_tid = tt.trans_id_file_id(self.file_id)
776
parent_tid = tt.get_tree_parent(p_tid)
777
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
778
cparent_tid = tt.get_tree_parent(cp_tid)
779
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
780
tt.adjust_path(osutils.basename(self.conflict_path),
455
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
787
458
class UnversionedParent(HandledConflict):
788
"""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.
789
460
Typically, the result of a merge where one tree unversioned the directory
790
461
and the other added a versioned file to it.
795
466
format = 'Conflict because %(path)s is not versioned, but has versioned'\
796
467
' children. %(action)s.'
798
# FIXME: We silently do nothing to make tests pass, but most probably the
799
# conflict shouldn't exist (the long story is that the conflict is
800
# generated with another one that can be resolved properly) -- vila 091224
801
def action_take_this(self, tree):
804
def action_take_other(self, tree):
808
470
class MissingParent(HandledConflict):
809
471
"""An attempt to add files to a directory that is not present.
810
472
Typically, the result of a merge where THIS deleted the directory and
811
473
the OTHER added a file to it.
812
See also: DeletingParent (same situation, THIS and OTHER reversed)
474
See also: DeletingParent (same situation, reversed THIS and OTHER)
815
477
typestring = 'missing parent'
817
479
format = 'Conflict adding files to %(path)s. %(action)s.'
819
def action_take_this(self, tree):
820
tree.remove([self.path], force=True, keep_files=False)
822
def action_take_other(self, tree):
823
# just acccept bzr proposal
827
482
class DeletingParent(HandledConflict):
828
483
"""An attempt to add files to a directory that is not present.
835
490
format = "Conflict: can't delete %(path)s because it is not empty. "\
838
# FIXME: It's a bit strange that the default action is not coherent with
839
# MissingParent from the *user* pov.
841
def action_take_this(self, tree):
842
# just acccept bzr proposal
845
def action_take_other(self, tree):
846
tree.remove([self.path], force=True, keep_files=False)
849
class NonDirectoryParent(HandledConflict):
850
"""An attempt to add files to a directory that is not a directory or
851
an attempt to change the kind of a directory with files.
854
typestring = 'non-directory parent'
856
format = "Conflict: %(path)s is not a directory, but has files in it."\
859
# FIXME: .OTHER should be used instead of .new when the conflict is created
861
def action_take_this(self, tree):
862
# FIXME: we should preserve that path when the conflict is generated !
863
if self.path.endswith('.new'):
864
conflict_path = self.path[:-(len('.new'))]
865
tree.remove([self.path], force=True, keep_files=False)
866
tree.add(conflict_path)
868
raise NotImplementedError(self.action_take_this)
870
def action_take_other(self, tree):
871
# FIXME: we should preserve that path when the conflict is generated !
872
if self.path.endswith('.new'):
873
conflict_path = self.path[:-(len('.new'))]
874
tree.remove([conflict_path], force=True, keep_files=False)
875
tree.rename_one(self.path, conflict_path)
877
raise NotImplementedError(self.action_take_other)