53
56
instead. (This is useful for editing all files with text conflicts.)
55
58
Use bzr resolve when you have fixed a problem.
61
help='List paths of files with text conflicts.'),
63
help='List paths of files with text conflicts.'),
65
_see_also = ['resolve', 'conflict-types']
64
def run(self, text=False):
65
from bzrlib.workingtree import WorkingTree
66
wt = WorkingTree.open_containing(u'.')[0]
67
def run(self, text=False, directory=u'.'):
68
wt = workingtree.WorkingTree.open_containing(directory)[0]
67
69
for conflict in wt.conflicts():
69
71
if conflict.typestring != 'text conflict':
73
75
self.outf.write(str(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)
76
100
class cmd_resolve(commands.Command):
77
"""Mark a conflict as resolved.
101
__doc__ = """Mark a conflict as resolved.
79
103
Merge will do its best to combine the changes in two branches, but there
80
104
are some kinds of problems only a human can fix. When it encounters those,
82
106
before you should commit.
84
108
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
109
text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
86
110
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
88
See also bzr conflicts.
90
112
aliases = ['resolved']
91
113
takes_args = ['file*']
93
Option('all', help='Resolve all conflicts in this tree.'),
116
option.Option('all', help='Resolve all conflicts in this tree.'),
117
ResolveActionOption(),
95
def run(self, file_list=None, all=False):
96
from bzrlib.workingtree import WorkingTree
119
_see_also = ['conflicts']
120
def run(self, file_list=None, all=False, action=None, directory=None):
99
123
raise errors.BzrCommandError("If --all is specified,"
100
124
" no FILE may be provided")
101
tree = WorkingTree.open_containing('.')[0]
125
if directory is None:
127
tree = workingtree.WorkingTree.open_containing(directory)[0]
104
tree, file_list = builtins.tree_files(file_list)
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
105
145
if file_list is None:
106
146
un_resolved, resolved = tree.auto_resolve()
107
147
if len(un_resolved) > 0:
114
154
trace.note('All conflicts resolved.')
117
resolve(tree, file_list)
120
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
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,
121
169
"""Resolve some or all of the conflicts in a working tree.
123
171
:param paths: If None, resolve all conflicts. Otherwise, select only
127
175
recursive commands like revert, this should be True. For commands
128
176
or applications wishing finer-grained control, like the resolve
129
177
command, this should be False.
130
:ignore_misses: If False, warnings will be printed if the supplied paths
131
do not have conflicts.
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,
133
182
tree.lock_tree_write()
183
nb_conflicts_after = None
135
185
tree_conflicts = tree.conflicts()
186
nb_conflicts_before = len(tree_conflicts)
136
187
if paths is None:
137
188
new_conflicts = ConflictList()
138
selected_conflicts = tree_conflicts
189
to_process = tree_conflicts
140
new_conflicts, selected_conflicts = \
141
tree_conflicts.select_conflicts(tree, paths, ignore_misses,
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)
200
nb_conflicts_after = len(new_conflicts)
144
201
tree.set_conflicts(new_conflicts)
145
202
except errors.UnsupportedOperation:
147
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
152
211
def restore(filename):
154
Restore a conflicted file to the state it was in before merging.
155
Only text restoration supported at present.
212
"""Restore a conflicted file to the state it was in before merging.
214
Only text restoration is supported at present.
157
216
conflicted = False
358
413
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)
361
457
class PathConflict(Conflict):
362
458
"""A conflict was encountered merging file paths"""
376
473
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, tt.trans_id_tree_path(path_to_create),
511
self._revision_tree(tt._tree, revid), file_id)
512
tt.version_file(file_id, tid)
514
# Adjust the path for the retained file id
515
tid = tt.trans_id_file_id(file_id)
516
parent_tid = tt.get_tree_parent(tid)
517
tt.adjust_path(path, parent_tid, tid)
520
def _revision_tree(self, tree, revid):
521
return tree.branch.repository.revision_tree(revid)
523
def _infer_file_id(self, tree):
524
# Prior to bug #531967, file_id wasn't always set, there may still be
525
# conflict files in the wild so we need to cope with them
526
# Establish which path we should use to find back the file-id
528
for p in (self.path, self.conflict_path):
530
# special hard-coded path
533
possible_paths.append(p)
534
# Search the file-id in the parents with any path available
536
for revid in tree.get_parent_ids():
537
revtree = self._revision_tree(tree, revid)
538
for p in possible_paths:
539
file_id = revtree.path2id(p)
540
if file_id is not None:
541
return revtree, file_id
544
def action_take_this(self, tree):
545
if self.file_id is not None:
546
self._resolve_with_cleanups(tree, self.file_id, self.path,
549
# Prior to bug #531967 we need to find back the file_id and restore
550
# the content from there
551
revtree, file_id = self._infer_file_id(tree)
552
tree.revert([revtree.id2path(file_id)],
553
old_tree=revtree, backups=False)
555
def action_take_other(self, tree):
556
if self.file_id is not None:
557
self._resolve_with_cleanups(tree, self.file_id,
561
# Prior to bug #531967 we need to find back the file_id and restore
562
# the content from there
563
revtree, file_id = self._infer_file_id(tree)
564
tree.revert([revtree.id2path(file_id)],
565
old_tree=revtree, backups=False)
380
568
class ContentsConflict(PathConflict):
381
"""The files are of different types, or not present"""
569
"""The files are of different types (or both binary), or not present"""
387
575
format = 'Contents conflict in %(path)s'
390
class TextConflict(PathConflict):
577
def associated_filenames(self):
578
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
580
def _resolve(self, tt, suffix_to_remove):
581
"""Resolve the conflict.
583
:param tt: The TreeTransform where the conflict is resolved.
584
:param suffix_to_remove: Either 'THIS' or 'OTHER'
586
The resolution is symmetric, when taking THIS, OTHER is deleted and
587
item.THIS is renamed into item and vice-versa.
590
# Delete 'item.THIS' or 'item.OTHER' depending on
593
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
594
except errors.NoSuchFile:
595
# There are valid cases where 'item.suffix_to_remove' either
596
# never existed or was already deleted (including the case
597
# where the user deleted it)
599
# Rename 'item.suffix_to_remove' (note that if
600
# 'item.suffix_to_remove' has been deleted, this is a no-op)
601
this_tid = tt.trans_id_file_id(self.file_id)
602
parent_tid = tt.get_tree_parent(this_tid)
603
tt.adjust_path(self.path, parent_tid, this_tid)
606
def action_take_this(self, tree):
607
self._resolve_with_cleanups(tree, 'OTHER')
609
def action_take_other(self, tree):
610
self._resolve_with_cleanups(tree, 'THIS')
613
# TODO: There should be a base revid attribute to better inform the user about
614
# how the conflicts were generated.
615
class TextConflict(Conflict):
391
616
"""The merge algorithm could not resolve all differences encountered."""
463
697
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
699
def action_take_this(self, tree):
700
tree.remove([self.conflict_path], force=True, keep_files=False)
701
tree.rename_one(self.path, self.conflict_path)
703
def action_take_other(self, tree):
704
tree.remove([self.path], force=True, keep_files=False)
466
707
class ParentLoop(HandledPathConflict):
467
708
"""An attempt to create an infinitely-looping directory structure.
468
709
This is rare, but can be produced like so:
477
718
typestring = 'parent loop'
479
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
720
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
722
def action_take_this(self, tree):
723
# just acccept bzr proposal
726
def action_take_other(self, tree):
727
# FIXME: We shouldn't have to manipulate so many paths here (and there
728
# is probably a bug or two...)
729
base_path = osutils.basename(self.path)
730
conflict_base_path = osutils.basename(self.conflict_path)
731
tt = transform.TreeTransform(tree)
733
p_tid = tt.trans_id_file_id(self.file_id)
734
parent_tid = tt.get_tree_parent(p_tid)
735
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
736
cparent_tid = tt.get_tree_parent(cp_tid)
737
tt.adjust_path(base_path, cparent_tid, cp_tid)
738
tt.adjust_path(conflict_base_path, parent_tid, p_tid)
482
744
class UnversionedParent(HandledConflict):
483
"""An attempt to version an file whose parent directory is not versioned.
745
"""An attempt to version a file whose parent directory is not versioned.
484
746
Typically, the result of a merge where one tree unversioned the directory
485
747
and the other added a versioned file to it.
490
752
format = 'Conflict because %(path)s is not versioned, but has versioned'\
491
753
' children. %(action)s.'
755
# FIXME: We silently do nothing to make tests pass, but most probably the
756
# conflict shouldn't exist (the long story is that the conflict is
757
# generated with another one that can be resolved properly) -- vila 091224
758
def action_take_this(self, tree):
761
def action_take_other(self, tree):
494
765
class MissingParent(HandledConflict):
495
766
"""An attempt to add files to a directory that is not present.
496
767
Typically, the result of a merge where THIS deleted the directory and
497
768
the OTHER added a file to it.
498
See also: DeletingParent (same situation, reversed THIS and OTHER)
769
See also: DeletingParent (same situation, THIS and OTHER reversed)
501
772
typestring = 'missing parent'
503
774
format = 'Conflict adding files to %(path)s. %(action)s.'
776
def action_take_this(self, tree):
777
tree.remove([self.path], force=True, keep_files=False)
779
def action_take_other(self, tree):
780
# just acccept bzr proposal
506
784
class DeletingParent(HandledConflict):
507
785
"""An attempt to add files to a directory that is not present.
514
792
format = "Conflict: can't delete %(path)s because it is not empty. "\
795
# FIXME: It's a bit strange that the default action is not coherent with
796
# MissingParent from the *user* pov.
798
def action_take_this(self, tree):
799
# just acccept bzr proposal
802
def action_take_other(self, tree):
803
tree.remove([self.path], force=True, keep_files=False)
518
806
class NonDirectoryParent(HandledConflict):
519
"""An attempt to add files to a directory that is not a director or
807
"""An attempt to add files to a directory that is not a directory or
520
808
an attempt to change the kind of a directory with files.
525
813
format = "Conflict: %(path)s is not a directory, but has files in it."\
816
# FIXME: .OTHER should be used instead of .new when the conflict is created
818
def action_take_this(self, tree):
819
# FIXME: we should preserve that path when the conflict is generated !
820
if self.path.endswith('.new'):
821
conflict_path = self.path[:-(len('.new'))]
822
tree.remove([self.path], force=True, keep_files=False)
823
tree.add(conflict_path)
825
raise NotImplementedError(self.action_take_this)
827
def action_take_other(self, tree):
828
# FIXME: we should preserve that path when the conflict is generated !
829
if self.path.endswith('.new'):
830
conflict_path = self.path[:-(len('.new'))]
831
tree.remove([conflict_path], force=True, keep_files=False)
832
tree.rename_one(self.path, conflict_path)
834
raise NotImplementedError(self.action_take_other)