1
# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
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
20
from __future__ import absolute_import
24
from .lazy_import import lazy_import
25
lazy_import(globals(), """
37
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.
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
'done', 'done', 'Marks the conflict as resolved.')
88
resolve_action_registry.register(
89
'take-this', 'take_this',
90
'Resolve the conflict preserving the version in the working tree.')
91
resolve_action_registry.register(
92
'take-other', 'take_other',
93
'Resolve the conflict taking the merged version into account.')
94
resolve_action_registry.default_key = 'done'
96
class ResolveActionOption(option.RegistryOption):
99
super(ResolveActionOption, self).__init__(
100
'action', 'How to resolve the conflict.',
102
registry=resolve_action_registry)
105
class cmd_resolve(commands.Command):
106
__doc__ = """Mark a conflict as resolved.
108
Merge will do its best to combine the changes in two branches, but there
109
are some kinds of problems only a human can fix. When it encounters those,
110
it will mark a conflict. A conflict means that you need to fix something,
111
before you can commit.
113
Once you have fixed a problem, use "brz resolve" to automatically mark
114
text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
115
resolved, or "brz resolve --all" to mark all conflicts as resolved.
117
aliases = ['resolved']
118
takes_args = ['file*']
121
option.Option('all', help='Resolve all conflicts in this tree.'),
122
ResolveActionOption(),
124
_see_also = ['conflicts']
125
def run(self, file_list=None, all=False, action=None, directory=None):
128
raise errors.BzrCommandError(gettext("If --all is specified,"
129
" no FILE may be provided"))
130
if directory is None:
132
tree = workingtree.WorkingTree.open_containing(directory)[0]
136
tree, file_list = workingtree.WorkingTree.open_containing_paths(
137
file_list, directory)
138
if file_list is None:
140
# FIXME: There is a special case here related to the option
141
# handling that could be clearer and easier to discover by
142
# providing an --auto action (bug #344013 and #383396) and
143
# make it mandatory instead of implicit and active only
144
# when no file_list is provided -- vila 091229
150
if file_list is None:
151
un_resolved, resolved = tree.auto_resolve()
152
if len(un_resolved) > 0:
153
trace.note(ngettext('%d conflict auto-resolved.',
154
'%d conflicts auto-resolved.', len(resolved)),
156
trace.note(gettext('Remaining conflicts:'))
157
for conflict in un_resolved:
158
trace.note(text_type(conflict))
161
trace.note(gettext('All conflicts resolved.'))
164
# FIXME: This can never occur but the block above needs some
165
# refactoring to transfer tree.auto_resolve() to
166
# conflict.auto(tree) --vila 091242
169
before, after = resolve(tree, file_list, action=action)
170
trace.note(ngettext('{0} conflict resolved, {1} remaining',
171
'{0} conflicts resolved, {1} remaining',
172
before-after).format(before - after, after))
175
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
177
"""Resolve some or all of the conflicts in a working tree.
179
:param paths: If None, resolve all conflicts. Otherwise, select only
181
:param recursive: If True, then elements of paths which are directories
182
have all their children resolved, etc. When invoked as part of
183
recursive commands like revert, this should be True. For commands
184
or applications wishing finer-grained control, like the resolve
185
command, this should be False.
186
:param ignore_misses: If False, warnings will be printed if the supplied
187
paths do not have conflicts.
188
:param action: How the conflict should be resolved,
190
nb_conflicts_after = None
191
with tree.lock_tree_write():
192
tree_conflicts = tree.conflicts()
193
nb_conflicts_before = len(tree_conflicts)
195
new_conflicts = ConflictList()
196
to_process = tree_conflicts
198
new_conflicts, to_process = tree_conflicts.select_conflicts(
199
tree, paths, ignore_misses, recursive)
200
for conflict in to_process:
202
conflict._do(action, tree)
203
conflict.cleanup(tree)
204
except NotImplementedError:
205
new_conflicts.append(conflict)
207
nb_conflicts_after = len(new_conflicts)
208
tree.set_conflicts(new_conflicts)
209
except errors.UnsupportedOperation:
211
if nb_conflicts_after is None:
212
nb_conflicts_after = nb_conflicts_before
213
return nb_conflicts_before, nb_conflicts_after
216
def restore(filename):
217
"""Restore a conflicted file to the state it was in before merging.
219
Only text restoration is supported at present.
223
osutils.rename(filename + ".THIS", filename)
226
if e.errno != errno.ENOENT:
229
os.unlink(filename + ".BASE")
232
if e.errno != errno.ENOENT:
235
os.unlink(filename + ".OTHER")
238
if e.errno != errno.ENOENT:
241
raise errors.NotConflicted(filename)
244
class ConflictList(object):
245
"""List of conflicts.
247
Typically obtained from WorkingTree.conflicts()
249
Can be instantiated from stanzas or from Conflict subclasses.
252
def __init__(self, conflicts=None):
253
object.__init__(self)
254
if conflicts is None:
257
self.__list = conflicts
260
return len(self.__list) == 0
263
return len(self.__list)
266
return iter(self.__list)
268
def __getitem__(self, key):
269
return self.__list[key]
271
def append(self, conflict):
272
return self.__list.append(conflict)
274
def __eq__(self, other_list):
275
return list(self) == list(other_list)
277
def __ne__(self, other_list):
278
return not (self == other_list)
281
return "ConflictList(%r)" % self.__list
284
def from_stanzas(stanzas):
285
"""Produce a new ConflictList from an iterable of stanzas"""
286
conflicts = ConflictList()
287
for stanza in stanzas:
288
conflicts.append(Conflict.factory(**stanza.as_dict()))
291
def to_stanzas(self):
292
"""Generator of stanzas"""
293
for conflict in self:
294
yield conflict.as_stanza()
296
def to_strings(self):
297
"""Generate strings for the provided conflicts"""
298
for conflict in self:
299
yield text_type(conflict)
301
def remove_files(self, tree):
302
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
303
for conflict in self:
304
if not conflict.has_files:
306
conflict.cleanup(tree)
308
def select_conflicts(self, tree, paths, ignore_misses=False,
310
"""Select the conflicts associated with paths in a tree.
312
File-ids are also used for this.
313
:return: a pair of ConflictLists: (not_selected, selected)
315
path_set = set(paths)
317
selected_paths = set()
318
new_conflicts = ConflictList()
319
selected_conflicts = ConflictList()
321
file_id = tree.path2id(path)
322
if file_id is not None:
325
for conflict in self:
327
for key in ('path', 'conflict_path'):
328
cpath = getattr(conflict, key, None)
331
if cpath in path_set:
333
selected_paths.add(cpath)
335
if osutils.is_inside_any(path_set, cpath):
337
selected_paths.add(cpath)
339
for key in ('file_id', 'conflict_file_id'):
340
cfile_id = getattr(conflict, key, None)
344
cpath = ids[cfile_id]
348
selected_paths.add(cpath)
350
selected_conflicts.append(conflict)
352
new_conflicts.append(conflict)
353
if ignore_misses is not True:
354
for path in [p for p in paths if p not in selected_paths]:
355
if not os.path.exists(tree.abspath(path)):
356
print("%s does not exist" % path)
358
print("%s is not conflicted" % path)
359
return new_conflicts, selected_conflicts
362
class Conflict(object):
363
"""Base class for all types of conflict"""
365
# FIXME: cleanup should take care of that ? -- vila 091229
368
def __init__(self, path, file_id=None):
370
# the factory blindly transfers the Stanza values to __init__ and
371
# Stanza is purely a Unicode api.
372
if isinstance(file_id, text_type):
373
file_id = cache_utf8.encode(file_id)
374
self.file_id = osutils.safe_file_id(file_id)
377
s = rio.Stanza(type=self.typestring, path=self.path)
378
if self.file_id is not None:
379
# Stanza requires Unicode apis
380
s.add('file_id', self.file_id.decode('utf8'))
384
return [type(self), self.path, self.file_id]
386
def __cmp__(self, other):
387
if getattr(other, "_cmp_list", None) is None:
389
return cmp(self._cmp_list(), other._cmp_list())
392
return hash((type(self), self.path, self.file_id))
394
def __eq__(self, other):
395
return self.__cmp__(other) == 0
397
def __ne__(self, other):
398
return not self.__eq__(other)
400
def __unicode__(self):
401
return self.format % self.__dict__
404
rdict = dict(self.__dict__)
405
rdict['class'] = self.__class__.__name__
406
return self.rformat % rdict
409
def factory(type, **kwargs):
411
return ctype[type](**kwargs)
414
def sort_key(conflict):
415
if conflict.path is not None:
416
return conflict.path, conflict.typestring
417
elif getattr(conflict, "conflict_path", None) is not None:
418
return conflict.conflict_path, conflict.typestring
420
return None, conflict.typestring
422
def _do(self, action, tree):
423
"""Apply the specified action to the conflict.
425
:param action: The method name to call.
427
:param tree: The tree passed as a parameter to the method.
429
meth = getattr(self, 'action_%s' % action, None)
431
raise NotImplementedError(self.__class__.__name__ + '.' + action)
434
def associated_filenames(self):
435
"""The names of the files generated to help resolve the conflict."""
436
raise NotImplementedError(self.associated_filenames)
438
def cleanup(self, tree):
439
for fname in self.associated_filenames():
441
osutils.delete_any(tree.abspath(fname))
443
if e.errno != errno.ENOENT:
446
def action_done(self, tree):
447
"""Mark the conflict as solved once it has been handled."""
448
# This method does nothing but simplifies the design of upper levels.
451
def action_take_this(self, tree):
452
raise NotImplementedError(self.action_take_this)
454
def action_take_other(self, tree):
455
raise NotImplementedError(self.action_take_other)
457
def _resolve_with_cleanups(self, tree, *args, **kwargs):
458
tt = transform.TreeTransform(tree)
459
op = cleanup.OperationWithCleanups(self._resolve)
460
op.add_cleanup(tt.finalize)
461
op.run_simple(tt, *args, **kwargs)
464
class PathConflict(Conflict):
465
"""A conflict was encountered merging file paths"""
467
typestring = 'path conflict'
469
format = 'Path conflict: %(path)s / %(conflict_path)s'
471
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
473
def __init__(self, path, conflict_path=None, file_id=None):
474
Conflict.__init__(self, path, file_id)
475
self.conflict_path = conflict_path
478
s = Conflict.as_stanza(self)
479
if self.conflict_path is not None:
480
s.add('conflict_path', self.conflict_path)
483
def associated_filenames(self):
484
# No additional files have been generated here
487
def _resolve(self, tt, file_id, path, winner):
488
"""Resolve the conflict.
490
:param tt: The TreeTransform where the conflict is resolved.
491
:param file_id: The retained file id.
492
:param path: The retained path.
493
:param winner: 'this' or 'other' indicates which side is the winner.
495
path_to_create = None
497
if self.path == '<deleted>':
498
return # Nothing to do
499
if self.conflict_path == '<deleted>':
500
path_to_create = self.path
501
revid = tt._tree.get_parent_ids()[0]
502
elif winner == 'other':
503
if self.conflict_path == '<deleted>':
504
return # Nothing to do
505
if self.path == '<deleted>':
506
path_to_create = self.conflict_path
507
# FIXME: If there are more than two parents we may need to
508
# iterate. Taking the last parent is the safer bet in the mean
509
# time. -- vila 20100309
510
revid = tt._tree.get_parent_ids()[-1]
513
raise AssertionError('bad winner: %r' % (winner,))
514
if path_to_create is not None:
515
tid = tt.trans_id_tree_path(path_to_create)
516
tree = self._revision_tree(tt._tree, revid)
517
transform.create_from_tree(
518
tt, tid, tree, tree.id2path(file_id), file_id=file_id)
519
tt.version_file(file_id, tid)
521
tid = tt.trans_id_file_id(file_id)
522
# Adjust the path for the retained file id
523
parent_tid = tt.get_tree_parent(tid)
524
tt.adjust_path(osutils.basename(path), parent_tid, tid)
527
def _revision_tree(self, tree, revid):
528
return tree.branch.repository.revision_tree(revid)
530
def _infer_file_id(self, tree):
531
# Prior to bug #531967, file_id wasn't always set, there may still be
532
# conflict files in the wild so we need to cope with them
533
# Establish which path we should use to find back the file-id
535
for p in (self.path, self.conflict_path):
537
# special hard-coded path
540
possible_paths.append(p)
541
# Search the file-id in the parents with any path available
543
for revid in tree.get_parent_ids():
544
revtree = self._revision_tree(tree, revid)
545
for p in possible_paths:
546
file_id = revtree.path2id(p)
547
if file_id is not None:
548
return revtree, file_id
551
def action_take_this(self, tree):
552
if self.file_id is not None:
553
self._resolve_with_cleanups(tree, self.file_id, self.path,
556
# Prior to bug #531967 we need to find back the file_id and restore
557
# the content from there
558
revtree, file_id = self._infer_file_id(tree)
559
tree.revert([revtree.id2path(file_id)],
560
old_tree=revtree, backups=False)
562
def action_take_other(self, tree):
563
if self.file_id is not None:
564
self._resolve_with_cleanups(tree, self.file_id,
568
# Prior to bug #531967 we need to find back the file_id and restore
569
# the content from there
570
revtree, file_id = self._infer_file_id(tree)
571
tree.revert([revtree.id2path(file_id)],
572
old_tree=revtree, backups=False)
575
class ContentsConflict(PathConflict):
576
"""The files are of different types (or both binary), or not present"""
580
typestring = 'contents conflict'
582
format = 'Contents conflict in %(path)s'
584
def associated_filenames(self):
585
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
587
def _resolve(self, tt, suffix_to_remove):
588
"""Resolve the conflict.
590
:param tt: The TreeTransform where the conflict is resolved.
591
:param suffix_to_remove: Either 'THIS' or 'OTHER'
593
The resolution is symmetric: when taking THIS, OTHER is deleted and
594
item.THIS is renamed into item and vice-versa.
597
# Delete 'item.THIS' or 'item.OTHER' depending on
600
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
601
except errors.NoSuchFile:
602
# There are valid cases where 'item.suffix_to_remove' either
603
# never existed or was already deleted (including the case
604
# where the user deleted it)
607
this_path = tt._tree.id2path(self.file_id)
608
except errors.NoSuchId:
609
# The file is not present anymore. This may happen if the user
610
# deleted the file either manually or when resolving a conflict on
611
# the parent. We may raise some exception to indicate that the
612
# conflict doesn't exist anymore and as such doesn't need to be
613
# resolved ? -- vila 20110615
616
this_tid = tt.trans_id_tree_path(this_path)
617
if this_tid is not None:
618
# Rename 'item.suffix_to_remove' (note that if
619
# 'item.suffix_to_remove' has been deleted, this is a no-op)
620
parent_tid = tt.get_tree_parent(this_tid)
621
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
624
def action_take_this(self, tree):
625
self._resolve_with_cleanups(tree, 'OTHER')
627
def action_take_other(self, tree):
628
self._resolve_with_cleanups(tree, 'THIS')
631
# TODO: There should be a base revid attribute to better inform the user about
632
# how the conflicts were generated.
633
class TextConflict(Conflict):
634
"""The merge algorithm could not resolve all differences encountered."""
638
typestring = 'text conflict'
640
format = 'Text conflict in %(path)s'
642
rformat = '%(class)s(%(path)r, %(file_id)r)'
644
def associated_filenames(self):
645
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
647
def _resolve(self, tt, winner_suffix):
648
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
650
:param tt: The TreeTransform where the conflict is resolved.
651
:param winner_suffix: Either 'THIS' or 'OTHER'
653
The resolution is symmetric, when taking THIS, item.THIS is renamed
654
into item and vice-versa. This takes one of the files as a whole
655
ignoring every difference that could have been merged cleanly.
657
# To avoid useless copies, we switch item and item.winner_suffix, only
658
# item will exist after the conflict has been resolved anyway.
659
item_tid = tt.trans_id_file_id(self.file_id)
660
item_parent_tid = tt.get_tree_parent(item_tid)
661
winner_path = self.path + '.' + winner_suffix
662
winner_tid = tt.trans_id_tree_path(winner_path)
663
winner_parent_tid = tt.get_tree_parent(winner_tid)
664
# Switch the paths to preserve the content
665
tt.adjust_path(osutils.basename(self.path),
666
winner_parent_tid, winner_tid)
667
tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
668
# Associate the file_id to the right content
669
tt.unversion_file(item_tid)
670
tt.version_file(self.file_id, winner_tid)
673
def action_take_this(self, tree):
674
self._resolve_with_cleanups(tree, 'THIS')
676
def action_take_other(self, tree):
677
self._resolve_with_cleanups(tree, 'OTHER')
680
class HandledConflict(Conflict):
681
"""A path problem that has been provisionally resolved.
682
This is intended to be a base class.
685
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
687
def __init__(self, action, path, file_id=None):
688
Conflict.__init__(self, path, file_id)
692
return Conflict._cmp_list(self) + [self.action]
695
s = Conflict.as_stanza(self)
696
s.add('action', self.action)
699
def associated_filenames(self):
700
# Nothing has been generated here
704
class HandledPathConflict(HandledConflict):
705
"""A provisionally-resolved path problem involving two paths.
706
This is intended to be a base class.
709
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
710
" %(file_id)r, %(conflict_file_id)r)"
712
def __init__(self, action, path, conflict_path, file_id=None,
713
conflict_file_id=None):
714
HandledConflict.__init__(self, action, path, file_id)
715
self.conflict_path = conflict_path
716
# the factory blindly transfers the Stanza values to __init__,
717
# so they can be unicode.
718
if isinstance(conflict_file_id, text_type):
719
conflict_file_id = cache_utf8.encode(conflict_file_id)
720
self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
723
return HandledConflict._cmp_list(self) + [self.conflict_path,
724
self.conflict_file_id]
727
s = HandledConflict.as_stanza(self)
728
s.add('conflict_path', self.conflict_path)
729
if self.conflict_file_id is not None:
730
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
735
class DuplicateID(HandledPathConflict):
736
"""Two files want the same file_id."""
738
typestring = 'duplicate id'
740
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
743
class DuplicateEntry(HandledPathConflict):
744
"""Two directory entries want to have the same name."""
746
typestring = 'duplicate'
748
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
750
def action_take_this(self, tree):
751
tree.remove([self.conflict_path], force=True, keep_files=False)
752
tree.rename_one(self.path, self.conflict_path)
754
def action_take_other(self, tree):
755
tree.remove([self.path], force=True, keep_files=False)
758
class ParentLoop(HandledPathConflict):
759
"""An attempt to create an infinitely-looping directory structure.
760
This is rare, but can be produced like so:
769
typestring = 'parent loop'
771
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
773
def action_take_this(self, tree):
774
# just acccept brz proposal
777
def action_take_other(self, tree):
778
tt = transform.TreeTransform(tree)
780
p_tid = tt.trans_id_file_id(self.file_id)
781
parent_tid = tt.get_tree_parent(p_tid)
782
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
783
cparent_tid = tt.get_tree_parent(cp_tid)
784
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
785
tt.adjust_path(osutils.basename(self.conflict_path),
792
class UnversionedParent(HandledConflict):
793
"""An attempt to version a file whose parent directory is not versioned.
794
Typically, the result of a merge where one tree unversioned the directory
795
and the other added a versioned file to it.
798
typestring = 'unversioned parent'
800
format = 'Conflict because %(path)s is not versioned, but has versioned'\
801
' children. %(action)s.'
803
# FIXME: We silently do nothing to make tests pass, but most probably the
804
# conflict shouldn't exist (the long story is that the conflict is
805
# generated with another one that can be resolved properly) -- vila 091224
806
def action_take_this(self, tree):
809
def action_take_other(self, tree):
813
class MissingParent(HandledConflict):
814
"""An attempt to add files to a directory that is not present.
815
Typically, the result of a merge where THIS deleted the directory and
816
the OTHER added a file to it.
817
See also: DeletingParent (same situation, THIS and OTHER reversed)
820
typestring = 'missing parent'
822
format = 'Conflict adding files to %(path)s. %(action)s.'
824
def action_take_this(self, tree):
825
tree.remove([self.path], force=True, keep_files=False)
827
def action_take_other(self, tree):
828
# just acccept brz proposal
832
class DeletingParent(HandledConflict):
833
"""An attempt to add files to a directory that is not present.
834
Typically, the result of a merge where one OTHER deleted the directory and
835
the THIS added a file to it.
838
typestring = 'deleting parent'
840
format = "Conflict: can't delete %(path)s because it is not empty. "\
843
# FIXME: It's a bit strange that the default action is not coherent with
844
# MissingParent from the *user* pov.
846
def action_take_this(self, tree):
847
# just acccept brz proposal
850
def action_take_other(self, tree):
851
tree.remove([self.path], force=True, keep_files=False)
854
class NonDirectoryParent(HandledConflict):
855
"""An attempt to add files to a directory that is not a directory or
856
an attempt to change the kind of a directory with files.
859
typestring = 'non-directory parent'
861
format = "Conflict: %(path)s is not a directory, but has files in it."\
864
# FIXME: .OTHER should be used instead of .new when the conflict is created
866
def action_take_this(self, tree):
867
# FIXME: we should preserve that path when the conflict is generated !
868
if self.path.endswith('.new'):
869
conflict_path = self.path[:-(len('.new'))]
870
tree.remove([self.path], force=True, keep_files=False)
871
tree.add(conflict_path)
873
raise NotImplementedError(self.action_take_this)
875
def action_take_other(self, tree):
876
# FIXME: we should preserve that path when the conflict is generated !
877
if self.path.endswith('.new'):
878
conflict_path = self.path[:-(len('.new'))]
879
tree.remove([conflict_path], force=True, keep_files=False)
880
tree.rename_one(self.path, conflict_path)
882
raise NotImplementedError(self.action_take_other)
888
def register_types(*conflict_types):
889
"""Register a Conflict subclass for serialization purposes"""
891
for conflict_type in conflict_types:
892
ctype[conflict_type.typestring] = conflict_type
894
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
895
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
896
DeletingParent, NonDirectoryParent)