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., 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
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# TODO: 'bzr resolve' should accept a directory name and work from that
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
resolve(tree, file_list, action=action)
165
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
121
167
"""Resolve some or all of the conflicts in a working tree.
123
169
:param paths: If None, resolve all conflicts. Otherwise, select only
127
173
recursive commands like revert, this should be True. For commands
128
174
or applications wishing finer-grained control, like the resolve
129
175
command, this should be False.
130
:ignore_misses: If False, warnings will be printed if the supplied paths
131
do not have conflicts.
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,
133
180
tree.lock_tree_write()
135
182
tree_conflicts = tree.conflicts()
136
183
if paths is None:
137
184
new_conflicts = ConflictList()
138
selected_conflicts = tree_conflicts
185
to_process = tree_conflicts
140
new_conflicts, selected_conflicts = \
141
tree_conflicts.select_conflicts(tree, paths, ignore_misses,
187
new_conflicts, to_process = tree_conflicts.select_conflicts(
188
tree, paths, ignore_misses, recursive)
189
for conflict in to_process:
191
conflict._do(action, tree)
192
conflict.cleanup(tree)
193
except NotImplementedError:
194
new_conflicts.append(conflict)
144
196
tree.set_conflicts(new_conflicts)
145
197
except errors.UnsupportedOperation:
147
selected_conflicts.remove_files(tree)
152
203
def restore(filename):
154
Restore a conflicted file to the state it was in before merging.
155
Only text restoration supported at present.
204
"""Restore a conflicted file to the state it was in before merging.
206
Only text restoration is supported at present.
157
208
conflicted = False
358
405
return None, conflict.typestring
407
def _do(self, action, tree):
408
"""Apply the specified action to the conflict.
410
:param action: The method name to call.
412
:param tree: The tree passed as a parameter to the method.
414
meth = getattr(self, 'action_%s' % action, None)
416
raise NotImplementedError(self.__class__.__name__ + '.' + action)
419
def associated_filenames(self):
420
"""The names of the files generated to help resolve the conflict."""
421
raise NotImplementedError(self.associated_filenames)
423
def cleanup(self, tree):
424
for fname in self.associated_filenames():
426
osutils.delete_any(tree.abspath(fname))
428
if e.errno != errno.ENOENT:
431
def action_done(self, tree):
432
"""Mark the conflict as solved once it has been handled."""
433
# This method does nothing but simplifies the design of upper levels.
436
def action_take_this(self, tree):
437
raise NotImplementedError(self.action_take_this)
439
def action_take_other(self, tree):
440
raise NotImplementedError(self.action_take_other)
442
def _resolve_with_cleanups(self, tree, *args, **kwargs):
443
tt = transform.TreeTransform(tree)
444
op = cleanup.OperationWithCleanups(self._resolve)
445
op.add_cleanup(tt.finalize)
446
op.run_simple(tt, *args, **kwargs)
361
449
class PathConflict(Conflict):
362
450
"""A conflict was encountered merging file paths"""
376
465
s.add('conflict_path', self.conflict_path)
468
def associated_filenames(self):
469
# No additional files have been generated here
472
def _resolve(self, tt, file_id, path, winner):
473
"""Resolve the conflict.
475
:param tt: The TreeTransform where the conflict is resolved.
476
:param file_id: The retained file id.
477
:param path: The retained path.
478
:param winner: 'this' or 'other' indicates which side is the winner.
480
path_to_create = None
482
if self.path == '<deleted>':
483
return # Nothing to do
484
if self.conflict_path == '<deleted>':
485
path_to_create = self.path
486
revid = tt._tree.get_parent_ids()[0]
487
elif winner == 'other':
488
if self.conflict_path == '<deleted>':
489
return # Nothing to do
490
if self.path == '<deleted>':
491
path_to_create = self.conflict_path
492
# FIXME: If there are more than two parents we may need to
493
# iterate. Taking the last parent is the safer bet in the mean
494
# time. -- vila 20100309
495
revid = tt._tree.get_parent_ids()[-1]
498
raise AssertionError('bad winner: %r' % (winner,))
499
if path_to_create is not None:
500
tid = tt.trans_id_tree_path(path_to_create)
501
transform.create_from_tree(
502
tt, tt.trans_id_tree_path(path_to_create),
503
self._revision_tree(tt._tree, revid), file_id)
504
tt.version_file(file_id, tid)
506
# Adjust the path for the retained file id
507
tid = tt.trans_id_file_id(file_id)
508
parent_tid = tt.get_tree_parent(tid)
509
tt.adjust_path(path, parent_tid, tid)
512
def _revision_tree(self, tree, revid):
513
return tree.branch.repository.revision_tree(revid)
515
def _infer_file_id(self, tree):
516
# Prior to bug #531967, file_id wasn't always set, there may still be
517
# conflict files in the wild so we need to cope with them
518
# Establish which path we should use to find back the file-id
520
for p in (self.path, self.conflict_path):
522
# special hard-coded path
525
possible_paths.append(p)
526
# Search the file-id in the parents with any path available
528
for revid in tree.get_parent_ids():
529
revtree = self._revision_tree(tree, revid)
530
for p in possible_paths:
531
file_id = revtree.path2id(p)
532
if file_id is not None:
533
return revtree, file_id
536
def action_take_this(self, tree):
537
if self.file_id is not None:
538
self._resolve_with_cleanups(tree, self.file_id, self.path,
541
# Prior to bug #531967 we need to find back the file_id and restore
542
# the content from there
543
revtree, file_id = self._infer_file_id(tree)
544
tree.revert([revtree.id2path(file_id)],
545
old_tree=revtree, backups=False)
547
def action_take_other(self, tree):
548
if self.file_id is not None:
549
self._resolve_with_cleanups(tree, self.file_id,
553
# Prior to bug #531967 we need to find back the file_id and restore
554
# the content from there
555
revtree, file_id = self._infer_file_id(tree)
556
tree.revert([revtree.id2path(file_id)],
557
old_tree=revtree, backups=False)
380
560
class ContentsConflict(PathConflict):
381
"""The files are of different types, or not present"""
561
"""The files are of different types (or both binary), or not present"""
387
567
format = 'Contents conflict in %(path)s'
569
def associated_filenames(self):
570
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
572
def _resolve(self, tt, suffix_to_remove):
573
"""Resolve the conflict.
575
:param tt: The TreeTransform where the conflict is resolved.
576
:param suffix_to_remove: Either 'THIS' or 'OTHER'
578
The resolution is symmetric, when taking THIS, OTHER is deleted and
579
item.THIS is renamed into item and vice-versa.
582
# Delete 'item.THIS' or 'item.OTHER' depending on
585
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
586
except errors.NoSuchFile:
587
# There are valid cases where 'item.suffix_to_remove' either
588
# never existed or was already deleted (including the case
589
# where the user deleted it)
591
# Rename 'item.suffix_to_remove' (note that if
592
# 'item.suffix_to_remove' has been deleted, this is a no-op)
593
this_tid = tt.trans_id_file_id(self.file_id)
594
parent_tid = tt.get_tree_parent(this_tid)
595
tt.adjust_path(self.path, parent_tid, this_tid)
598
def action_take_this(self, tree):
599
self._resolve_with_cleanups(tree, 'OTHER')
601
def action_take_other(self, tree):
602
self._resolve_with_cleanups(tree, 'THIS')
605
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
606
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
608
# TODO: There should be a base revid attribute to better inform the user about
609
# how the conflicts were generated.
390
610
class TextConflict(PathConflict):
391
611
"""The merge algorithm could not resolve all differences encountered."""
428
655
def __init__(self, action, path, conflict_path, file_id=None,
429
656
conflict_file_id=None):
430
657
HandledConflict.__init__(self, action, path, file_id)
431
self.conflict_path = conflict_path
658
self.conflict_path = conflict_path
432
659
# warn turned off, because the factory blindly transfers the Stanza
433
660
# values to __init__.
434
661
self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
437
664
def _cmp_list(self):
438
return HandledConflict._cmp_list(self) + [self.conflict_path,
665
return HandledConflict._cmp_list(self) + [self.conflict_path,
439
666
self.conflict_file_id]
441
668
def as_stanza(self):
463
690
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
692
def action_take_this(self, tree):
693
tree.remove([self.conflict_path], force=True, keep_files=False)
694
tree.rename_one(self.path, self.conflict_path)
696
def action_take_other(self, tree):
697
tree.remove([self.path], force=True, keep_files=False)
466
700
class ParentLoop(HandledPathConflict):
467
701
"""An attempt to create an infinitely-looping directory structure.
468
702
This is rare, but can be produced like so:
477
711
typestring = 'parent loop'
479
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
713
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
715
def action_take_this(self, tree):
716
# just acccept bzr proposal
719
def action_take_other(self, tree):
720
# FIXME: We shouldn't have to manipulate so many paths here (and there
721
# is probably a bug or two...)
722
base_path = osutils.basename(self.path)
723
conflict_base_path = osutils.basename(self.conflict_path)
724
tt = transform.TreeTransform(tree)
726
p_tid = tt.trans_id_file_id(self.file_id)
727
parent_tid = tt.get_tree_parent(p_tid)
728
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
729
cparent_tid = tt.get_tree_parent(cp_tid)
730
tt.adjust_path(base_path, cparent_tid, cp_tid)
731
tt.adjust_path(conflict_base_path, parent_tid, p_tid)
482
737
class UnversionedParent(HandledConflict):
483
"""An attempt to version an file whose parent directory is not versioned.
738
"""An attempt to version a file whose parent directory is not versioned.
484
739
Typically, the result of a merge where one tree unversioned the directory
485
740
and the other added a versioned file to it.
490
745
format = 'Conflict because %(path)s is not versioned, but has versioned'\
491
746
' children. %(action)s.'
748
# FIXME: We silently do nothing to make tests pass, but most probably the
749
# conflict shouldn't exist (the long story is that the conflict is
750
# generated with another one that can be resolved properly) -- vila 091224
751
def action_take_this(self, tree):
754
def action_take_other(self, tree):
494
758
class MissingParent(HandledConflict):
495
759
"""An attempt to add files to a directory that is not present.
496
760
Typically, the result of a merge where THIS deleted the directory and
497
761
the OTHER added a file to it.
498
See also: DeletingParent (same situation, reversed THIS and OTHER)
762
See also: DeletingParent (same situation, THIS and OTHER reversed)
501
765
typestring = 'missing parent'
503
767
format = 'Conflict adding files to %(path)s. %(action)s.'
769
def action_take_this(self, tree):
770
tree.remove([self.path], force=True, keep_files=False)
772
def action_take_other(self, tree):
773
# just acccept bzr proposal
506
777
class DeletingParent(HandledConflict):
507
778
"""An attempt to add files to a directory that is not present.
514
785
format = "Conflict: can't delete %(path)s because it is not empty. "\
788
# FIXME: It's a bit strange that the default action is not coherent with
789
# MissingParent from the *user* pov.
791
def action_take_this(self, tree):
792
# just acccept bzr proposal
795
def action_take_other(self, tree):
796
tree.remove([self.path], force=True, keep_files=False)
518
799
class NonDirectoryParent(HandledConflict):
519
"""An attempt to add files to a directory that is not a director or
800
"""An attempt to add files to a directory that is not a directory or
520
801
an attempt to change the kind of a directory with files.
525
806
format = "Conflict: %(path)s is not a directory, but has files in it."\
809
# FIXME: .OTHER should be used instead of .new when the conflict is created
811
def action_take_this(self, tree):
812
# FIXME: we should preserve that path when the conflict is generated !
813
if self.path.endswith('.new'):
814
conflict_path = self.path[:-(len('.new'))]
815
tree.remove([self.path], force=True, keep_files=False)
816
tree.add(conflict_path)
818
raise NotImplementedError(self.action_take_this)
820
def action_take_other(self, tree):
821
# FIXME: we should preserve that path when the conflict is generated !
822
if self.path.endswith('.new'):
823
conflict_path = self.path[:-(len('.new'))]
824
tree.remove([conflict_path], force=True, keep_files=False)
825
tree.rename_one(self.path, conflict_path)
827
raise NotImplementedError(self.action_take_other)