1
# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 Canonical Ltd
1
# Copyright (C) 2005 by Aaron Bentley
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
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: 'bzr 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
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
35
from bzrlib.i18n import gettext, ngettext
26
from bzrlib.commands import register_command
27
from bzrlib.errors import BzrCommandError, NotConflicted, UnsupportedOperation
28
from bzrlib.option import Option
29
from bzrlib.osutils import rename, delete_any
30
from bzrlib.rio import Stanza
44
33
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
47
class cmd_conflicts(commands.Command):
48
__doc__ = """List files with conflicts.
36
class cmd_conflicts(bzrlib.commands.Command):
37
"""List files with conflicts.
50
39
Merge will do its best to combine the changes in two branches, but there
51
40
are some kinds of problems only a human can fix. When it encounters those,
52
41
it will mark a conflict. A conflict means that you need to fix something,
53
42
before you should commit.
55
Conflicts normally are listed as short, human-readable messages. If --text
56
is supplied, the pathnames of files with text conflicts are listed,
57
instead. (This is useful for editing all files with text conflicts.)
59
44
Use bzr resolve when you have fixed a problem.
46
(conflicts are determined by the presence of .BASE .TREE, and .OTHER
64
help='List paths of files with text conflicts.'),
66
_see_also = ['resolve', 'conflict-types']
68
def run(self, text=False, directory=u'.'):
69
wt = workingtree.WorkingTree.open_containing(directory)[0]
52
from bzrlib.workingtree import WorkingTree
53
wt = WorkingTree.open_containing(u'.')[0]
70
54
for conflict in wt.conflicts():
72
if conflict.typestring != 'text conflict':
74
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)
101
class cmd_resolve(commands.Command):
102
__doc__ = """Mark a conflict as resolved.
58
class cmd_resolve(bzrlib.commands.Command):
59
"""Mark a conflict as resolved.
104
61
Merge will do its best to combine the changes in two branches, but there
105
62
are some kinds of problems only a human can fix. When it encounters those,
106
63
it will mark a conflict. A conflict means that you need to fix something,
107
64
before you should commit.
109
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
111
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
66
Once you have fixed a problem, use "bzr resolve FILE.." to mark
67
individual files as fixed, or "bzr resolve --all" to mark all conflicts as
70
See also bzr conflicts.
113
72
aliases = ['resolved']
114
73
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):
74
takes_options = [Option('all', help='Resolve all conflicts in this tree')]
75
def run(self, file_list=None, all=False):
76
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]
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
146
if file_list is None:
147
un_resolved, resolved = tree.auto_resolve()
148
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:'))
153
for conflict in un_resolved:
154
trace.note(unicode(conflict))
157
trace.note(gettext('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,
186
tree.lock_tree_write()
187
nb_conflicts_after = None
79
raise BzrCommandError("If --all is specified, no FILE may be provided")
80
tree = WorkingTree.open_containing('.')[0]
84
raise BzrCommandError("command 'resolve' needs one or more FILE, or --all")
85
tree = WorkingTree.open_containing(file_list[0])[0]
86
to_resolve = [tree.relpath(p) for p in file_list]
87
resolve(tree, to_resolve)
90
def resolve(tree, paths=None, ignore_misses=False):
189
93
tree_conflicts = tree.conflicts()
190
nb_conflicts_before = len(tree_conflicts)
192
95
new_conflicts = ConflictList()
193
to_process = tree_conflicts
96
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)
98
new_conflicts, selected_conflicts = \
99
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
204
nb_conflicts_after = len(new_conflicts)
205
101
tree.set_conflicts(new_conflicts)
206
except errors.UnsupportedOperation:
102
except UnsupportedOperation:
104
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
109
def restore(filename):
216
"""Restore a conflicted file to the state it was in before merging.
218
Only text restoration is supported at present.
111
Restore a conflicted file to the state it was in before merging.
112
Only text restoration supported at present.
220
114
conflicted = False
222
osutils.rename(filename + ".THIS", filename)
116
rename(filename + ".THIS", filename)
223
117
conflicted = True
224
118
except OSError, e:
225
119
if e.errno != errno.ENOENT:
477
320
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
324
class ContentsConflict(PathConflict):
572
"""The files are of different types (or both binary), or not present"""
325
"""The files are of different types, or not present"""
578
331
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):
334
class TextConflict(PathConflict):
630
335
"""The merge algorithm could not resolve all differences encountered."""
636
341
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
344
class HandledConflict(Conflict):
677
345
"""A path problem that has been provisionally resolved.
743
404
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
407
class ParentLoop(HandledPathConflict):
754
408
"""An attempt to create an infinitely-looping directory structure.
755
409
This is rare, but can be produced like so:
764
418
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),
420
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
787
423
class UnversionedParent(HandledConflict):
788
"""An attempt to version a file whose parent directory is not versioned.
424
"""An attempt to version an file whose parent directory is not versioned.
789
425
Typically, the result of a merge where one tree unversioned the directory
790
426
and the other added a versioned file to it.
793
429
typestring = 'unversioned parent'
795
format = 'Conflict because %(path)s is not versioned, but has versioned'\
796
' 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):
431
format = 'Conflict adding versioned files to %(path)s. %(action)s.'
808
434
class MissingParent(HandledConflict):
809
435
"""An attempt to add files to a directory that is not present.
810
Typically, the result of a merge where THIS deleted the directory and
811
the OTHER added a file to it.
812
See also: DeletingParent (same situation, THIS and OTHER reversed)
436
Typically, the result of a merge where one tree deleted the directory and
437
the other added a file to it.
815
440
typestring = 'missing parent'
817
442
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
class DeletingParent(HandledConflict):
828
"""An attempt to add files to a directory that is not present.
829
Typically, the result of a merge where one OTHER deleted the directory and
830
the THIS added a file to it.
833
typestring = 'deleting parent'
835
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)