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: 'brz 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
20
from __future__ import absolute_import
22
# TODO: bzr revert should resolve; even when reverting the whole tree
23
# or particular directories
25
from .lazy_import import lazy_import
26
lazy_import(globals(), """
36
from breezy.i18n import gettext, ngettext
45
from .sixish import text_type
48
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
51
class cmd_conflicts(commands.Command):
52
__doc__ = """List files with conflicts.
54
Merge will do its best to combine the changes in two branches, but there
55
are some kinds of problems only a human can fix. When it encounters those,
56
it will mark a conflict. A conflict means that you need to fix something,
57
before you can commit.
59
Conflicts normally are listed as short, human-readable messages. If --text
60
is supplied, the pathnames of files with text conflicts are listed,
61
instead. (This is useful for editing all files with text conflicts.)
63
Use brz resolve when you have fixed a problem.
29
from bzrlib.branch import Branch
30
from bzrlib.errors import BzrCommandError, NotConflicted
31
from bzrlib.commands import register_command
32
from bzrlib.workingtree import CONFLICT_SUFFIXES
34
class cmd_conflicts(bzrlib.commands.Command):
35
"""List files with conflicts.
36
(conflicts are determined by the presence of .BASE .TREE, and .OTHER
68
help='List paths of files with text conflicts.'),
70
_see_also = ['resolve', 'conflict-types']
72
def run(self, text=False, directory=u'.'):
73
wt = workingtree.WorkingTree.open_containing(directory)[0]
74
for conflict in wt.conflicts():
76
if conflict.typestring != 'text conflict':
78
self.outf.write(conflict.path + '\n')
80
self.outf.write(text_type(conflict) + '\n')
83
resolve_action_registry = registry.Registry()
86
resolve_action_registry.register(
87
'auto', 'auto', 'Detect whether conflict has been resolved by user.')
88
resolve_action_registry.register(
89
'done', 'done', 'Marks the conflict as resolved.')
90
resolve_action_registry.register(
91
'take-this', 'take_this',
92
'Resolve the conflict preserving the version in the working tree.')
93
resolve_action_registry.register(
94
'take-other', 'take_other',
95
'Resolve the conflict taking the merged version into account.')
96
resolve_action_registry.default_key = 'done'
99
class ResolveActionOption(option.RegistryOption):
102
super(ResolveActionOption, self).__init__(
103
'action', 'How to resolve the conflict.',
105
registry=resolve_action_registry)
108
class cmd_resolve(commands.Command):
109
__doc__ = """Mark a conflict as resolved.
111
Merge will do its best to combine the changes in two branches, but there
112
are some kinds of problems only a human can fix. When it encounters those,
113
it will mark a conflict. A conflict means that you need to fix something,
114
before you can commit.
116
Once you have fixed a problem, use "brz resolve" to automatically mark
117
text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
118
resolved, or "brz resolve --all" to mark all conflicts as resolved.
40
for path in Branch.open_containing('.')[0].working_tree().iter_conflicts():
43
class cmd_resolve(bzrlib.commands.Command):
44
"""Mark a conflict as resolved.
120
46
aliases = ['resolved']
121
47
takes_args = ['file*']
124
option.Option('all', help='Resolve all conflicts in this tree.'),
125
ResolveActionOption(),
127
_see_also = ['conflicts']
129
def run(self, file_list=None, all=False, action=None, directory=None):
132
raise errors.BzrCommandError(gettext("If --all is specified,"
133
" no FILE may be provided"))
134
if directory is None:
136
tree = workingtree.WorkingTree.open_containing(directory)[0]
48
takes_options = ['all']
49
def run(self, file_list=None, all=False):
52
raise BzrCommandError(
53
"command 'resolve' needs one or more FILE, or --all")
54
tree = Branch.open_containing('.')[0].working_tree()
55
file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
140
tree, file_list = workingtree.WorkingTree.open_containing_paths(
141
file_list, directory)
143
if file_list is None:
58
raise BzrCommandError(
59
"If --all is specified, no FILE may be provided")
60
for filename in file_list:
62
for suffix in CONFLICT_SUFFIXES:
64
os.unlink(filename+suffix)
66
if e.errno != errno.ENOENT:
70
if failures == len(CONFLICT_SUFFIXES):
71
if not os.path.exists(filename):
72
print "%s does not exist" % filename
147
before, after = resolve(tree, file_list, action=action)
148
# GZ 2012-07-27: Should unify UI below now that auto is less magical.
149
if action == 'auto' and file_list is None:
152
ngettext('%d conflict auto-resolved.',
153
'%d conflicts auto-resolved.', before - after),
155
trace.note(gettext('Remaining conflicts:'))
156
for conflict in tree.conflicts():
157
trace.note(text_type(conflict))
160
trace.note(gettext('All conflicts resolved.'))
163
trace.note(ngettext('{0} conflict resolved, {1} remaining',
164
'{0} conflicts resolved, {1} remaining',
165
before - after).format(before - after, after))
168
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
170
"""Resolve some or all of the conflicts in a working tree.
172
:param paths: If None, resolve all conflicts. Otherwise, select only
174
:param recursive: If True, then elements of paths which are directories
175
have all their children resolved, etc. When invoked as part of
176
recursive commands like revert, this should be True. For commands
177
or applications wishing finer-grained control, like the resolve
178
command, this should be False.
179
:param ignore_misses: If False, warnings will be printed if the supplied
180
paths do not have conflicts.
181
:param action: How the conflict should be resolved,
183
nb_conflicts_after = None
184
with tree.lock_tree_write():
185
tree_conflicts = tree.conflicts()
186
nb_conflicts_before = len(tree_conflicts)
188
new_conflicts = ConflictList()
189
to_process = 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)
200
nb_conflicts_after = len(new_conflicts)
201
tree.set_conflicts(new_conflicts)
202
except errors.UnsupportedOperation:
204
if nb_conflicts_after is None:
205
nb_conflicts_after = nb_conflicts_before
206
return nb_conflicts_before, nb_conflicts_after
74
print "%s is not conflicted" % filename
209
76
def restore(filename):
210
"""Restore a conflicted file to the state it was in before merging.
212
Only text restoration is supported at present.
78
Restore a conflicted file to the state it was in before merging.
79
Only text restoration supported at present.
214
81
conflicted = False
216
osutils.rename(filename + ".THIS", filename)
83
os.rename(filename + ".THIS", filename)
219
86
if e.errno != errno.ENOENT:
222
89
os.unlink(filename + ".BASE")
225
92
if e.errno != errno.ENOENT:
228
95
os.unlink(filename + ".OTHER")
231
98
if e.errno != errno.ENOENT:
233
100
if not conflicted:
234
raise errors.NotConflicted(filename)
237
class ConflictList(object):
238
"""List of conflicts.
240
Typically obtained from WorkingTree.conflicts()
242
Can be instantiated from stanzas or from Conflict subclasses.
245
def __init__(self, conflicts=None):
246
object.__init__(self)
247
if conflicts is None:
250
self.__list = conflicts
253
return len(self.__list) == 0
256
return len(self.__list)
259
return iter(self.__list)
261
def __getitem__(self, key):
262
return self.__list[key]
264
def append(self, conflict):
265
return self.__list.append(conflict)
267
def __eq__(self, other_list):
268
return list(self) == list(other_list)
270
def __ne__(self, other_list):
271
return not (self == other_list)
274
return "ConflictList(%r)" % self.__list
277
def from_stanzas(stanzas):
278
"""Produce a new ConflictList from an iterable of stanzas"""
279
conflicts = ConflictList()
280
for stanza in stanzas:
281
conflicts.append(Conflict.factory(**stanza.as_dict()))
284
def to_stanzas(self):
285
"""Generator of stanzas"""
286
for conflict in self:
287
yield conflict.as_stanza()
289
def to_strings(self):
290
"""Generate strings for the provided conflicts"""
291
for conflict in self:
292
yield text_type(conflict)
294
def remove_files(self, tree):
295
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
296
for conflict in self:
297
if not conflict.has_files:
299
conflict.cleanup(tree)
301
def select_conflicts(self, tree, paths, ignore_misses=False,
303
"""Select the conflicts associated with paths in a tree.
305
File-ids are also used for this.
306
:return: a pair of ConflictLists: (not_selected, selected)
308
path_set = set(paths)
310
selected_paths = set()
311
new_conflicts = ConflictList()
312
selected_conflicts = ConflictList()
314
file_id = tree.path2id(path)
315
if file_id is not None:
318
for conflict in self:
320
for key in ('path', 'conflict_path'):
321
cpath = getattr(conflict, key, None)
324
if cpath in path_set:
326
selected_paths.add(cpath)
328
if osutils.is_inside_any(path_set, cpath):
330
selected_paths.add(cpath)
332
for key in ('file_id', 'conflict_file_id'):
333
cfile_id = getattr(conflict, key, None)
337
cpath = ids[cfile_id]
341
selected_paths.add(cpath)
343
selected_conflicts.append(conflict)
345
new_conflicts.append(conflict)
346
if ignore_misses is not True:
347
for path in [p for p in paths if p not in selected_paths]:
348
if not os.path.exists(tree.abspath(path)):
349
print("%s does not exist" % path)
351
print("%s is not conflicted" % path)
352
return new_conflicts, selected_conflicts
355
class Conflict(object):
356
"""Base class for all types of conflict"""
358
# FIXME: cleanup should take care of that ? -- vila 091229
361
def __init__(self, path, file_id=None):
363
# the factory blindly transfers the Stanza values to __init__ and
364
# Stanza is purely a Unicode api.
365
if isinstance(file_id, text_type):
366
file_id = cache_utf8.encode(file_id)
367
self.file_id = osutils.safe_file_id(file_id)
370
s = rio.Stanza(type=self.typestring, path=self.path)
371
if self.file_id is not None:
372
# Stanza requires Unicode apis
373
s.add('file_id', self.file_id.decode('utf8'))
377
return [type(self), self.path, self.file_id]
379
def __cmp__(self, other):
380
if getattr(other, "_cmp_list", None) is None:
383
y = other._cmp_list()
384
return (x > y) - (x < y)
387
return hash((type(self), self.path, self.file_id))
389
def __eq__(self, other):
390
return self.__cmp__(other) == 0
392
def __ne__(self, other):
393
return not self.__eq__(other)
395
def __unicode__(self):
396
return self.describe()
399
return self.describe()
402
return self.format % self.__dict__
405
rdict = dict(self.__dict__)
406
rdict['class'] = self.__class__.__name__
407
return self.rformat % rdict
410
def factory(type, **kwargs):
412
return ctype[type](**kwargs)
415
def sort_key(conflict):
416
if conflict.path is not None:
417
return conflict.path, conflict.typestring
418
elif getattr(conflict, "conflict_path", None) is not None:
419
return conflict.conflict_path, conflict.typestring
421
return None, conflict.typestring
423
def _do(self, action, tree):
424
"""Apply the specified action to the conflict.
426
:param action: The method name to call.
428
:param tree: The tree passed as a parameter to the method.
430
meth = getattr(self, 'action_%s' % action, None)
432
raise NotImplementedError(self.__class__.__name__ + '.' + action)
435
def associated_filenames(self):
436
"""The names of the files generated to help resolve the conflict."""
437
raise NotImplementedError(self.associated_filenames)
439
def cleanup(self, tree):
440
for fname in self.associated_filenames():
442
osutils.delete_any(tree.abspath(fname))
444
if e.errno != errno.ENOENT:
447
def action_auto(self, tree):
448
raise NotImplementedError(self.action_auto)
450
def action_done(self, tree):
451
"""Mark the conflict as solved once it has been handled."""
452
# This method does nothing but simplifies the design of upper levels.
455
def action_take_this(self, tree):
456
raise NotImplementedError(self.action_take_this)
458
def action_take_other(self, tree):
459
raise NotImplementedError(self.action_take_other)
461
def _resolve_with_cleanups(self, tree, *args, **kwargs):
462
with tree.get_transform() as tt:
463
self._resolve(tt, *args, **kwargs)
466
class PathConflict(Conflict):
467
"""A conflict was encountered merging file paths"""
469
typestring = 'path conflict'
471
format = 'Path conflict: %(path)s / %(conflict_path)s'
473
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
475
def __init__(self, path, conflict_path=None, file_id=None):
476
Conflict.__init__(self, path, file_id)
477
self.conflict_path = conflict_path
480
s = Conflict.as_stanza(self)
481
if self.conflict_path is not None:
482
s.add('conflict_path', self.conflict_path)
485
def associated_filenames(self):
486
# No additional files have been generated here
489
def _resolve(self, tt, file_id, path, winner):
490
"""Resolve the conflict.
492
:param tt: The TreeTransform where the conflict is resolved.
493
:param file_id: The retained file id.
494
:param path: The retained path.
495
:param winner: 'this' or 'other' indicates which side is the winner.
497
path_to_create = None
499
if self.path == '<deleted>':
500
return # Nothing to do
501
if self.conflict_path == '<deleted>':
502
path_to_create = self.path
503
revid = tt._tree.get_parent_ids()[0]
504
elif winner == 'other':
505
if self.conflict_path == '<deleted>':
506
return # Nothing to do
507
if self.path == '<deleted>':
508
path_to_create = self.conflict_path
509
# FIXME: If there are more than two parents we may need to
510
# iterate. Taking the last parent is the safer bet in the mean
511
# time. -- vila 20100309
512
revid = tt._tree.get_parent_ids()[-1]
515
raise AssertionError('bad winner: %r' % (winner,))
516
if path_to_create is not None:
517
tid = tt.trans_id_tree_path(path_to_create)
518
tree = self._revision_tree(tt._tree, revid)
519
transform.create_from_tree(
520
tt, tid, tree, tree.id2path(file_id))
521
tt.version_file(file_id, tid)
523
tid = tt.trans_id_file_id(file_id)
524
# Adjust the path for the retained file id
525
parent_tid = tt.get_tree_parent(tid)
526
tt.adjust_path(osutils.basename(path), parent_tid, tid)
529
def _revision_tree(self, tree, revid):
530
return tree.branch.repository.revision_tree(revid)
532
def _infer_file_id(self, tree):
533
# Prior to bug #531967, file_id wasn't always set, there may still be
534
# conflict files in the wild so we need to cope with them
535
# Establish which path we should use to find back the file-id
537
for p in (self.path, self.conflict_path):
539
# special hard-coded path
542
possible_paths.append(p)
543
# Search the file-id in the parents with any path available
545
for revid in tree.get_parent_ids():
546
revtree = self._revision_tree(tree, revid)
547
for p in possible_paths:
548
file_id = revtree.path2id(p)
549
if file_id is not None:
550
return revtree, file_id
553
def action_take_this(self, tree):
554
if self.file_id is not None:
555
self._resolve_with_cleanups(tree, self.file_id, self.path,
558
# Prior to bug #531967 we need to find back the file_id and restore
559
# the content from there
560
revtree, file_id = self._infer_file_id(tree)
561
tree.revert([revtree.id2path(file_id)],
562
old_tree=revtree, backups=False)
564
def action_take_other(self, tree):
565
if self.file_id is not None:
566
self._resolve_with_cleanups(tree, self.file_id,
570
# Prior to bug #531967 we need to find back the file_id and restore
571
# the content from there
572
revtree, file_id = self._infer_file_id(tree)
573
tree.revert([revtree.id2path(file_id)],
574
old_tree=revtree, backups=False)
577
class ContentsConflict(PathConflict):
578
"""The files are of different types (or both binary), or not present"""
582
typestring = 'contents conflict'
584
format = 'Contents conflict in %(path)s'
586
def associated_filenames(self):
587
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
589
def _resolve(self, tt, suffix_to_remove):
590
"""Resolve the conflict.
592
:param tt: The TreeTransform where the conflict is resolved.
593
:param suffix_to_remove: Either 'THIS' or 'OTHER'
595
The resolution is symmetric: when taking THIS, OTHER is deleted and
596
item.THIS is renamed into item and vice-versa.
599
# Delete 'item.THIS' or 'item.OTHER' depending on
602
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
603
except errors.NoSuchFile:
604
# There are valid cases where 'item.suffix_to_remove' either
605
# never existed or was already deleted (including the case
606
# where the user deleted it)
609
this_path = tt._tree.id2path(self.file_id)
610
except errors.NoSuchId:
611
# The file is not present anymore. This may happen if the user
612
# deleted the file either manually or when resolving a conflict on
613
# the parent. We may raise some exception to indicate that the
614
# conflict doesn't exist anymore and as such doesn't need to be
615
# resolved ? -- vila 20110615
618
this_tid = tt.trans_id_tree_path(this_path)
619
if this_tid is not None:
620
# Rename 'item.suffix_to_remove' (note that if
621
# 'item.suffix_to_remove' has been deleted, this is a no-op)
622
parent_tid = tt.get_tree_parent(this_tid)
623
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
626
def action_take_this(self, tree):
627
self._resolve_with_cleanups(tree, 'OTHER')
629
def action_take_other(self, tree):
630
self._resolve_with_cleanups(tree, 'THIS')
633
# TODO: There should be a base revid attribute to better inform the user about
634
# how the conflicts were generated.
635
class TextConflict(Conflict):
636
"""The merge algorithm could not resolve all differences encountered."""
640
typestring = 'text conflict'
642
format = 'Text conflict in %(path)s'
644
rformat = '%(class)s(%(path)r, %(file_id)r)'
646
_conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
648
def associated_filenames(self):
649
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
651
def _resolve(self, tt, winner_suffix):
652
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
654
:param tt: The TreeTransform where the conflict is resolved.
655
:param winner_suffix: Either 'THIS' or 'OTHER'
657
The resolution is symmetric, when taking THIS, item.THIS is renamed
658
into item and vice-versa. This takes one of the files as a whole
659
ignoring every difference that could have been merged cleanly.
661
# To avoid useless copies, we switch item and item.winner_suffix, only
662
# item will exist after the conflict has been resolved anyway.
663
item_tid = tt.trans_id_file_id(self.file_id)
664
item_parent_tid = tt.get_tree_parent(item_tid)
665
winner_path = self.path + '.' + winner_suffix
666
winner_tid = tt.trans_id_tree_path(winner_path)
667
winner_parent_tid = tt.get_tree_parent(winner_tid)
668
# Switch the paths to preserve the content
669
tt.adjust_path(osutils.basename(self.path),
670
winner_parent_tid, winner_tid)
671
tt.adjust_path(osutils.basename(winner_path),
672
item_parent_tid, item_tid)
673
# Associate the file_id to the right content
674
tt.unversion_file(item_tid)
675
tt.version_file(self.file_id, winner_tid)
678
def action_auto(self, tree):
679
# GZ 2012-07-27: Using NotImplementedError to signal that a conflict
680
# can't be auto resolved does not seem ideal.
682
kind = tree.kind(self.path)
683
except errors.NoSuchFile:
686
raise NotImplementedError("Conflict is not a file")
687
conflict_markers_in_line = self._conflict_re.search
688
# GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
689
with tree.get_file(self.path) as f:
691
if conflict_markers_in_line(line):
692
raise NotImplementedError("Conflict markers present")
694
def action_take_this(self, tree):
695
self._resolve_with_cleanups(tree, 'THIS')
697
def action_take_other(self, tree):
698
self._resolve_with_cleanups(tree, 'OTHER')
701
class HandledConflict(Conflict):
702
"""A path problem that has been provisionally resolved.
703
This is intended to be a base class.
706
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
708
def __init__(self, action, path, file_id=None):
709
Conflict.__init__(self, path, file_id)
713
return Conflict._cmp_list(self) + [self.action]
716
s = Conflict.as_stanza(self)
717
s.add('action', self.action)
720
def associated_filenames(self):
721
# Nothing has been generated here
725
class HandledPathConflict(HandledConflict):
726
"""A provisionally-resolved path problem involving two paths.
727
This is intended to be a base class.
730
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
731
" %(file_id)r, %(conflict_file_id)r)"
733
def __init__(self, action, path, conflict_path, file_id=None,
734
conflict_file_id=None):
735
HandledConflict.__init__(self, action, path, file_id)
736
self.conflict_path = conflict_path
737
# the factory blindly transfers the Stanza values to __init__,
738
# so they can be unicode.
739
if isinstance(conflict_file_id, text_type):
740
conflict_file_id = cache_utf8.encode(conflict_file_id)
741
self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
744
return HandledConflict._cmp_list(self) + [self.conflict_path,
745
self.conflict_file_id]
748
s = HandledConflict.as_stanza(self)
749
s.add('conflict_path', self.conflict_path)
750
if self.conflict_file_id is not None:
751
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
756
class DuplicateID(HandledPathConflict):
757
"""Two files want the same file_id."""
759
typestring = 'duplicate id'
761
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
764
class DuplicateEntry(HandledPathConflict):
765
"""Two directory entries want to have the same name."""
767
typestring = 'duplicate'
769
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
771
def action_take_this(self, tree):
772
tree.remove([self.conflict_path], force=True, keep_files=False)
773
tree.rename_one(self.path, self.conflict_path)
775
def action_take_other(self, tree):
776
tree.remove([self.path], force=True, keep_files=False)
779
class ParentLoop(HandledPathConflict):
780
"""An attempt to create an infinitely-looping directory structure.
781
This is rare, but can be produced like so:
790
typestring = 'parent loop'
792
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
794
def action_take_this(self, tree):
795
# just acccept brz proposal
798
def action_take_other(self, tree):
799
with tree.get_transform() as tt:
800
p_tid = tt.trans_id_file_id(self.file_id)
801
parent_tid = tt.get_tree_parent(p_tid)
802
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
803
cparent_tid = tt.get_tree_parent(cp_tid)
804
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
805
tt.adjust_path(osutils.basename(self.conflict_path),
810
class UnversionedParent(HandledConflict):
811
"""An attempt to version a file whose parent directory is not versioned.
812
Typically, the result of a merge where one tree unversioned the directory
813
and the other added a versioned file to it.
816
typestring = 'unversioned parent'
818
format = 'Conflict because %(path)s is not versioned, but has versioned'\
819
' children. %(action)s.'
821
# FIXME: We silently do nothing to make tests pass, but most probably the
822
# conflict shouldn't exist (the long story is that the conflict is
823
# generated with another one that can be resolved properly) -- vila 091224
824
def action_take_this(self, tree):
827
def action_take_other(self, tree):
831
class MissingParent(HandledConflict):
832
"""An attempt to add files to a directory that is not present.
833
Typically, the result of a merge where THIS deleted the directory and
834
the OTHER added a file to it.
835
See also: DeletingParent (same situation, THIS and OTHER reversed)
838
typestring = 'missing parent'
840
format = 'Conflict adding files to %(path)s. %(action)s.'
842
def action_take_this(self, tree):
843
tree.remove([self.path], force=True, keep_files=False)
845
def action_take_other(self, tree):
846
# just acccept brz proposal
850
class DeletingParent(HandledConflict):
851
"""An attempt to add files to a directory that is not present.
852
Typically, the result of a merge where one OTHER deleted the directory and
853
the THIS added a file to it.
856
typestring = 'deleting parent'
858
format = "Conflict: can't delete %(path)s because it is not empty. "\
861
# FIXME: It's a bit strange that the default action is not coherent with
862
# MissingParent from the *user* pov.
864
def action_take_this(self, tree):
865
# just acccept brz proposal
868
def action_take_other(self, tree):
869
tree.remove([self.path], force=True, keep_files=False)
872
class NonDirectoryParent(HandledConflict):
873
"""An attempt to add files to a directory that is not a directory or
874
an attempt to change the kind of a directory with files.
877
typestring = 'non-directory parent'
879
format = "Conflict: %(path)s is not a directory, but has files in it."\
882
# FIXME: .OTHER should be used instead of .new when the conflict is created
884
def action_take_this(self, tree):
885
# FIXME: we should preserve that path when the conflict is generated !
886
if self.path.endswith('.new'):
887
conflict_path = self.path[:-(len('.new'))]
888
tree.remove([self.path], force=True, keep_files=False)
889
tree.add(conflict_path)
891
raise NotImplementedError(self.action_take_this)
893
def action_take_other(self, tree):
894
# FIXME: we should preserve that path when the conflict is generated !
895
if self.path.endswith('.new'):
896
conflict_path = self.path[:-(len('.new'))]
897
tree.remove([conflict_path], force=True, keep_files=False)
898
tree.rename_one(self.path, conflict_path)
900
raise NotImplementedError(self.action_take_other)
906
def register_types(*conflict_types):
907
"""Register a Conflict subclass for serialization purposes"""
909
for conflict_type in conflict_types:
910
ctype[conflict_type.typestring] = conflict_type
913
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
914
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
915
DeletingParent, NonDirectoryParent)
101
raise NotConflicted(filename)