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
25
from .lazy_import import lazy_import
26
lazy_import(globals(), """
36
from breezy.i18n import gettext, ngettext
47
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
50
class cmd_conflicts(commands.Command):
51
__doc__ = """List files with conflicts.
53
Merge will do its best to combine the changes in two branches, but there
54
are some kinds of problems only a human can fix. When it encounters those,
55
it will mark a conflict. A conflict means that you need to fix something,
56
before you can commit.
58
Conflicts normally are listed as short, human-readable messages. If --text
59
is supplied, the pathnames of files with text conflicts are listed,
60
instead. (This is useful for editing all files with text conflicts.)
62
Use brz resolve when you have fixed a problem.
67
help='List paths of files with text conflicts.'),
69
_see_also = ['resolve', 'conflict-types']
71
def run(self, text=False, directory=u'.'):
72
wt = workingtree.WorkingTree.open_containing(directory)[0]
73
for conflict in wt.conflicts():
75
if conflict.typestring != 'text conflict':
77
self.outf.write(conflict.path + '\n')
79
self.outf.write(str(conflict) + '\n')
82
resolve_action_registry = registry.Registry()
85
resolve_action_registry.register(
86
'auto', 'auto', 'Detect whether conflict has been resolved by user.')
87
resolve_action_registry.register(
88
'done', 'done', 'Marks the conflict as resolved.')
89
resolve_action_registry.register(
90
'take-this', 'take_this',
91
'Resolve the conflict preserving the version in the working tree.')
92
resolve_action_registry.register(
93
'take-other', 'take_other',
94
'Resolve the conflict taking the merged version into account.')
95
resolve_action_registry.default_key = 'done'
98
class ResolveActionOption(option.RegistryOption):
101
super(ResolveActionOption, self).__init__(
102
'action', 'How to resolve the conflict.',
104
registry=resolve_action_registry)
107
class cmd_resolve(commands.Command):
108
__doc__ = """Mark a conflict as resolved.
110
Merge will do its best to combine the changes in two branches, but there
111
are some kinds of problems only a human can fix. When it encounters those,
112
it will mark a conflict. A conflict means that you need to fix something,
113
before you can commit.
115
Once you have fixed a problem, use "brz resolve" to automatically mark
116
text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
117
resolved, or "brz resolve --all" to mark all conflicts as resolved.
119
aliases = ['resolved']
120
takes_args = ['file*']
123
option.Option('all', help='Resolve all conflicts in this tree.'),
124
ResolveActionOption(),
126
_see_also = ['conflicts']
128
def run(self, file_list=None, all=False, action=None, directory=None):
131
raise errors.BzrCommandError(gettext("If --all is specified,"
132
" no FILE may be provided"))
133
if directory is None:
135
tree = workingtree.WorkingTree.open_containing(directory)[0]
139
tree, file_list = workingtree.WorkingTree.open_containing_paths(
140
file_list, directory)
142
if file_list is None:
146
before, after = resolve(tree, file_list, action=action)
147
# GZ 2012-07-27: Should unify UI below now that auto is less magical.
148
if action == 'auto' and file_list is None:
151
ngettext('%d conflict auto-resolved.',
152
'%d conflicts auto-resolved.', before - after),
154
trace.note(gettext('Remaining conflicts:'))
155
for conflict in tree.conflicts():
156
trace.note(str(conflict))
159
trace.note(gettext('All conflicts resolved.'))
162
trace.note(ngettext('{0} conflict resolved, {1} remaining',
163
'{0} conflicts resolved, {1} remaining',
164
before - after).format(before - after, after))
167
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
169
"""Resolve some or all of the conflicts in a working tree.
171
:param paths: If None, resolve all conflicts. Otherwise, select only
173
:param recursive: If True, then elements of paths which are directories
174
have all their children resolved, etc. When invoked as part of
175
recursive commands like revert, this should be True. For commands
176
or applications wishing finer-grained control, like the resolve
177
command, this should be False.
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,
182
nb_conflicts_after = None
183
with tree.lock_tree_write():
184
tree_conflicts = tree.conflicts()
185
nb_conflicts_before = len(tree_conflicts)
187
new_conflicts = ConflictList()
188
to_process = tree_conflicts
190
new_conflicts, to_process = tree_conflicts.select_conflicts(
191
tree, paths, ignore_misses, recursive)
192
for conflict in to_process:
194
conflict._do(action, tree)
195
conflict.cleanup(tree)
196
except NotImplementedError:
197
new_conflicts.append(conflict)
199
nb_conflicts_after = len(new_conflicts)
200
tree.set_conflicts(new_conflicts)
201
except errors.UnsupportedOperation:
203
if nb_conflicts_after is None:
204
nb_conflicts_after = nb_conflicts_before
205
return nb_conflicts_before, nb_conflicts_after
208
def restore(filename):
209
"""Restore a conflicted file to the state it was in before merging.
211
Only text restoration is supported at present.
215
osutils.rename(filename + ".THIS", filename)
218
if e.errno != errno.ENOENT:
221
os.unlink(filename + ".BASE")
224
if e.errno != errno.ENOENT:
227
os.unlink(filename + ".OTHER")
230
if e.errno != errno.ENOENT:
233
raise errors.NotConflicted(filename)
236
class ConflictList(object):
237
"""List of conflicts.
239
Typically obtained from WorkingTree.conflicts()
241
Can be instantiated from stanzas or from Conflict subclasses.
244
def __init__(self, conflicts=None):
245
object.__init__(self)
246
if conflicts is None:
249
self.__list = conflicts
252
return len(self.__list) == 0
255
return len(self.__list)
258
return iter(self.__list)
260
def __getitem__(self, key):
261
return self.__list[key]
263
def append(self, conflict):
264
return self.__list.append(conflict)
266
def __eq__(self, other_list):
267
return list(self) == list(other_list)
269
def __ne__(self, other_list):
270
return not (self == other_list)
273
return "ConflictList(%r)" % self.__list
276
def from_stanzas(stanzas):
277
"""Produce a new ConflictList from an iterable of stanzas"""
278
conflicts = ConflictList()
279
for stanza in stanzas:
280
conflicts.append(Conflict.factory(**stanza.as_dict()))
283
def to_stanzas(self):
284
"""Generator of stanzas"""
285
for conflict in self:
286
yield conflict.as_stanza()
288
def to_strings(self):
289
"""Generate strings for the provided conflicts"""
290
for conflict in self:
293
def remove_files(self, tree):
294
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
295
for conflict in self:
296
if not conflict.has_files:
298
conflict.cleanup(tree)
300
def select_conflicts(self, tree, paths, ignore_misses=False,
302
"""Select the conflicts associated with paths in a tree.
304
File-ids are also used for this.
305
:return: a pair of ConflictLists: (not_selected, selected)
307
path_set = set(paths)
309
selected_paths = set()
310
new_conflicts = ConflictList()
311
selected_conflicts = ConflictList()
313
file_id = tree.path2id(path)
314
if file_id is not None:
317
for conflict in self:
319
for key in ('path', 'conflict_path'):
320
cpath = getattr(conflict, key, None)
323
if cpath in path_set:
325
selected_paths.add(cpath)
327
if osutils.is_inside_any(path_set, cpath):
329
selected_paths.add(cpath)
331
for key in ('file_id', 'conflict_file_id'):
332
cfile_id = getattr(conflict, key, None)
336
cpath = ids[cfile_id]
340
selected_paths.add(cpath)
342
selected_conflicts.append(conflict)
344
new_conflicts.append(conflict)
345
if ignore_misses is not True:
346
for path in [p for p in paths if p not in selected_paths]:
347
if not os.path.exists(tree.abspath(path)):
348
print("%s does not exist" % path)
350
print("%s is not conflicted" % path)
351
return new_conflicts, selected_conflicts
354
class Conflict(object):
355
"""Base class for all types of conflict"""
357
# FIXME: cleanup should take care of that ? -- vila 091229
360
def __init__(self, path, file_id=None):
362
# the factory blindly transfers the Stanza values to __init__ and
363
# Stanza is purely a Unicode api.
364
if isinstance(file_id, str):
365
file_id = cache_utf8.encode(file_id)
366
self.file_id = osutils.safe_file_id(file_id)
369
s = rio.Stanza(type=self.typestring, path=self.path)
370
if self.file_id is not None:
371
# Stanza requires Unicode apis
372
s.add('file_id', self.file_id.decode('utf8'))
376
return [type(self), self.path, self.file_id]
378
def __cmp__(self, other):
379
if getattr(other, "_cmp_list", None) is None:
382
y = other._cmp_list()
383
return (x > y) - (x < y)
386
return hash((type(self), self.path, self.file_id))
388
def __eq__(self, other):
389
return self.__cmp__(other) == 0
391
def __ne__(self, other):
392
return not self.__eq__(other)
394
def __unicode__(self):
395
return self.describe()
398
return self.describe()
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_auto(self, tree):
447
raise NotImplementedError(self.action_auto)
449
def action_done(self, tree):
450
"""Mark the conflict as solved once it has been handled."""
451
# This method does nothing but simplifies the design of upper levels.
454
def action_take_this(self, tree):
455
raise NotImplementedError(self.action_take_this)
457
def action_take_other(self, tree):
458
raise NotImplementedError(self.action_take_other)
460
def _resolve_with_cleanups(self, tree, *args, **kwargs):
461
with tree.get_transform() as tt:
462
self._resolve(tt, *args, **kwargs)
465
class PathConflict(Conflict):
466
"""A conflict was encountered merging file paths"""
468
typestring = 'path conflict'
470
format = 'Path conflict: %(path)s / %(conflict_path)s'
472
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
474
def __init__(self, path, conflict_path=None, file_id=None):
475
Conflict.__init__(self, path, file_id)
476
self.conflict_path = conflict_path
479
s = Conflict.as_stanza(self)
480
if self.conflict_path is not None:
481
s.add('conflict_path', self.conflict_path)
484
def associated_filenames(self):
485
# No additional files have been generated here
488
def _resolve(self, tt, file_id, path, winner):
489
"""Resolve the conflict.
491
:param tt: The TreeTransform where the conflict is resolved.
492
:param file_id: The retained file id.
493
:param path: The retained path.
494
:param winner: 'this' or 'other' indicates which side is the winner.
496
path_to_create = None
498
if self.path == '<deleted>':
499
return # Nothing to do
500
if self.conflict_path == '<deleted>':
501
path_to_create = self.path
502
revid = tt._tree.get_parent_ids()[0]
503
elif winner == 'other':
504
if self.conflict_path == '<deleted>':
505
return # Nothing to do
506
if self.path == '<deleted>':
507
path_to_create = self.conflict_path
508
# FIXME: If there are more than two parents we may need to
509
# iterate. Taking the last parent is the safer bet in the mean
510
# time. -- vila 20100309
511
revid = tt._tree.get_parent_ids()[-1]
514
raise AssertionError('bad winner: %r' % (winner,))
515
if path_to_create is not None:
516
tid = tt.trans_id_tree_path(path_to_create)
517
tree = self._revision_tree(tt._tree, revid)
518
transform.create_from_tree(
519
tt, tid, tree, tree.id2path(file_id))
520
tt.version_file(file_id, tid)
522
tid = tt.trans_id_file_id(file_id)
523
# Adjust the path for the retained file id
524
parent_tid = tt.get_tree_parent(tid)
525
tt.adjust_path(osutils.basename(path), parent_tid, tid)
528
def _revision_tree(self, tree, revid):
529
return tree.branch.repository.revision_tree(revid)
531
def _infer_file_id(self, tree):
532
# Prior to bug #531967, file_id wasn't always set, there may still be
533
# conflict files in the wild so we need to cope with them
534
# Establish which path we should use to find back the file-id
536
for p in (self.path, self.conflict_path):
538
# special hard-coded path
541
possible_paths.append(p)
542
# Search the file-id in the parents with any path available
544
for revid in tree.get_parent_ids():
545
revtree = self._revision_tree(tree, revid)
546
for p in possible_paths:
547
file_id = revtree.path2id(p)
548
if file_id is not None:
549
return revtree, file_id
552
def action_take_this(self, tree):
553
if self.file_id is not None:
554
self._resolve_with_cleanups(tree, self.file_id, self.path,
557
# Prior to bug #531967 we need to find back the file_id and restore
558
# the content from there
559
revtree, file_id = self._infer_file_id(tree)
560
tree.revert([revtree.id2path(file_id)],
561
old_tree=revtree, backups=False)
563
def action_take_other(self, tree):
564
if self.file_id is not None:
565
self._resolve_with_cleanups(tree, self.file_id,
569
# Prior to bug #531967 we need to find back the file_id and restore
570
# the content from there
571
revtree, file_id = self._infer_file_id(tree)
572
tree.revert([revtree.id2path(file_id)],
573
old_tree=revtree, backups=False)
576
class ContentsConflict(PathConflict):
577
"""The files are of different types (or both binary), or not present"""
581
typestring = 'contents conflict'
583
format = 'Contents conflict in %(path)s'
585
def associated_filenames(self):
586
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
588
def _resolve(self, tt, suffix_to_remove):
589
"""Resolve the conflict.
591
:param tt: The TreeTransform where the conflict is resolved.
592
:param suffix_to_remove: Either 'THIS' or 'OTHER'
594
The resolution is symmetric: when taking THIS, OTHER is deleted and
595
item.THIS is renamed into item and vice-versa.
598
# Delete 'item.THIS' or 'item.OTHER' depending on
601
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
602
except errors.NoSuchFile:
603
# There are valid cases where 'item.suffix_to_remove' either
604
# never existed or was already deleted (including the case
605
# where the user deleted it)
608
this_path = tt._tree.id2path(self.file_id)
609
except errors.NoSuchId:
610
# The file is not present anymore. This may happen if the user
611
# deleted the file either manually or when resolving a conflict on
612
# the parent. We may raise some exception to indicate that the
613
# conflict doesn't exist anymore and as such doesn't need to be
614
# resolved ? -- vila 20110615
617
this_tid = tt.trans_id_tree_path(this_path)
618
if this_tid is not None:
619
# Rename 'item.suffix_to_remove' (note that if
620
# 'item.suffix_to_remove' has been deleted, this is a no-op)
621
parent_tid = tt.get_tree_parent(this_tid)
622
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
625
def action_take_this(self, tree):
626
self._resolve_with_cleanups(tree, 'OTHER')
628
def action_take_other(self, tree):
629
self._resolve_with_cleanups(tree, 'THIS')
632
# TODO: There should be a base revid attribute to better inform the user about
633
# how the conflicts were generated.
634
class TextConflict(Conflict):
635
"""The merge algorithm could not resolve all differences encountered."""
639
typestring = 'text conflict'
641
format = 'Text conflict in %(path)s'
643
rformat = '%(class)s(%(path)r, %(file_id)r)'
645
_conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
647
def associated_filenames(self):
648
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
650
def _resolve(self, tt, winner_suffix):
651
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
653
:param tt: The TreeTransform where the conflict is resolved.
654
:param winner_suffix: Either 'THIS' or 'OTHER'
656
The resolution is symmetric, when taking THIS, item.THIS is renamed
657
into item and vice-versa. This takes one of the files as a whole
658
ignoring every difference that could have been merged cleanly.
660
# To avoid useless copies, we switch item and item.winner_suffix, only
661
# item will exist after the conflict has been resolved anyway.
662
item_tid = tt.trans_id_file_id(self.file_id)
663
item_parent_tid = tt.get_tree_parent(item_tid)
664
winner_path = self.path + '.' + winner_suffix
665
winner_tid = tt.trans_id_tree_path(winner_path)
666
winner_parent_tid = tt.get_tree_parent(winner_tid)
667
# Switch the paths to preserve the content
668
tt.adjust_path(osutils.basename(self.path),
669
winner_parent_tid, winner_tid)
670
tt.adjust_path(osutils.basename(winner_path),
671
item_parent_tid, item_tid)
672
# Associate the file_id to the right content
673
tt.unversion_file(item_tid)
674
tt.version_file(self.file_id, winner_tid)
677
def action_auto(self, tree):
678
# GZ 2012-07-27: Using NotImplementedError to signal that a conflict
679
# can't be auto resolved does not seem ideal.
681
kind = tree.kind(self.path)
682
except errors.NoSuchFile:
685
raise NotImplementedError("Conflict is not a file")
686
conflict_markers_in_line = self._conflict_re.search
687
# GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
688
with tree.get_file(self.path) as f:
690
if conflict_markers_in_line(line):
691
raise NotImplementedError("Conflict markers present")
693
def action_take_this(self, tree):
694
self._resolve_with_cleanups(tree, 'THIS')
696
def action_take_other(self, tree):
697
self._resolve_with_cleanups(tree, 'OTHER')
700
class HandledConflict(Conflict):
701
"""A path problem that has been provisionally resolved.
702
This is intended to be a base class.
705
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
707
def __init__(self, action, path, file_id=None):
708
Conflict.__init__(self, path, file_id)
712
return Conflict._cmp_list(self) + [self.action]
715
s = Conflict.as_stanza(self)
716
s.add('action', self.action)
719
def associated_filenames(self):
720
# Nothing has been generated here
724
class HandledPathConflict(HandledConflict):
725
"""A provisionally-resolved path problem involving two paths.
726
This is intended to be a base class.
729
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
730
" %(file_id)r, %(conflict_file_id)r)"
732
def __init__(self, action, path, conflict_path, file_id=None,
733
conflict_file_id=None):
734
HandledConflict.__init__(self, action, path, file_id)
735
self.conflict_path = conflict_path
736
# the factory blindly transfers the Stanza values to __init__,
737
# so they can be unicode.
738
if isinstance(conflict_file_id, str):
739
conflict_file_id = cache_utf8.encode(conflict_file_id)
740
self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
743
return HandledConflict._cmp_list(self) + [self.conflict_path,
744
self.conflict_file_id]
747
s = HandledConflict.as_stanza(self)
748
s.add('conflict_path', self.conflict_path)
749
if self.conflict_file_id is not None:
750
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
755
class DuplicateID(HandledPathConflict):
756
"""Two files want the same file_id."""
758
typestring = 'duplicate id'
760
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
763
class DuplicateEntry(HandledPathConflict):
764
"""Two directory entries want to have the same name."""
766
typestring = 'duplicate'
768
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
770
def action_take_this(self, tree):
771
tree.remove([self.conflict_path], force=True, keep_files=False)
772
tree.rename_one(self.path, self.conflict_path)
774
def action_take_other(self, tree):
775
tree.remove([self.path], force=True, keep_files=False)
778
class ParentLoop(HandledPathConflict):
779
"""An attempt to create an infinitely-looping directory structure.
780
This is rare, but can be produced like so:
789
typestring = 'parent loop'
791
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
793
def action_take_this(self, tree):
794
# just acccept brz proposal
797
def action_take_other(self, tree):
798
with tree.get_transform() as tt:
799
p_tid = tt.trans_id_file_id(self.file_id)
800
parent_tid = tt.get_tree_parent(p_tid)
801
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
802
cparent_tid = tt.get_tree_parent(cp_tid)
803
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
804
tt.adjust_path(osutils.basename(self.conflict_path),
809
class UnversionedParent(HandledConflict):
810
"""An attempt to version a file whose parent directory is not versioned.
811
Typically, the result of a merge where one tree unversioned the directory
812
and the other added a versioned file to it.
815
typestring = 'unversioned parent'
817
format = 'Conflict because %(path)s is not versioned, but has versioned'\
818
' children. %(action)s.'
820
# FIXME: We silently do nothing to make tests pass, but most probably the
821
# conflict shouldn't exist (the long story is that the conflict is
822
# generated with another one that can be resolved properly) -- vila 091224
823
def action_take_this(self, tree):
826
def action_take_other(self, tree):
830
class MissingParent(HandledConflict):
831
"""An attempt to add files to a directory that is not present.
832
Typically, the result of a merge where THIS deleted the directory and
833
the OTHER added a file to it.
834
See also: DeletingParent (same situation, THIS and OTHER reversed)
837
typestring = 'missing parent'
839
format = 'Conflict adding files to %(path)s. %(action)s.'
841
def action_take_this(self, tree):
842
tree.remove([self.path], force=True, keep_files=False)
844
def action_take_other(self, tree):
845
# just acccept brz proposal
849
class DeletingParent(HandledConflict):
850
"""An attempt to add files to a directory that is not present.
851
Typically, the result of a merge where one OTHER deleted the directory and
852
the THIS added a file to it.
855
typestring = 'deleting parent'
857
format = "Conflict: can't delete %(path)s because it is not empty. "\
860
# FIXME: It's a bit strange that the default action is not coherent with
861
# MissingParent from the *user* pov.
863
def action_take_this(self, tree):
864
# just acccept brz proposal
867
def action_take_other(self, tree):
868
tree.remove([self.path], force=True, keep_files=False)
871
class NonDirectoryParent(HandledConflict):
872
"""An attempt to add files to a directory that is not a directory or
873
an attempt to change the kind of a directory with files.
876
typestring = 'non-directory parent'
878
format = "Conflict: %(path)s is not a directory, but has files in it."\
881
# FIXME: .OTHER should be used instead of .new when the conflict is created
883
def action_take_this(self, tree):
884
# FIXME: we should preserve that path when the conflict is generated !
885
if self.path.endswith('.new'):
886
conflict_path = self.path[:-(len('.new'))]
887
tree.remove([self.path], force=True, keep_files=False)
888
tree.add(conflict_path)
890
raise NotImplementedError(self.action_take_this)
892
def action_take_other(self, tree):
893
# FIXME: we should preserve that path when the conflict is generated !
894
if self.path.endswith('.new'):
895
conflict_path = self.path[:-(len('.new'))]
896
tree.remove([conflict_path], force=True, keep_files=False)
897
tree.rename_one(self.path, conflict_path)
899
raise NotImplementedError(self.action_take_other)
905
def register_types(*conflict_types):
906
"""Register a Conflict subclass for serialization purposes"""
908
for conflict_type in conflict_types:
909
ctype[conflict_type.typestring] = conflict_type
912
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
913
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
914
DeletingParent, NonDirectoryParent)