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: '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
44
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
47
class cmd_conflicts(commands.Command):
48
__doc__ = """List files with conflicts.
50
Merge will do its best to combine the changes in two branches, but there
51
are some kinds of problems only a human can fix. When it encounters those,
52
it will mark a conflict. A conflict means that you need to fix something,
53
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
Use bzr resolve when you have fixed a problem.
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]
70
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.
104
Merge will do its best to combine the changes in two branches, but there
105
are some kinds of problems only a human can fix. When it encounters those,
106
it will mark a conflict. A conflict means that you need to fix something,
107
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.
113
aliases = ['resolved']
114
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):
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
189
tree_conflicts = tree.conflicts()
190
nb_conflicts_before = len(tree_conflicts)
192
new_conflicts = ConflictList()
193
to_process = 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)
204
nb_conflicts_after = len(new_conflicts)
205
tree.set_conflicts(new_conflicts)
206
except errors.UnsupportedOperation:
210
if nb_conflicts_after is None:
211
nb_conflicts_after = nb_conflicts_before
212
return nb_conflicts_before, nb_conflicts_after
215
def restore(filename):
216
"""Restore a conflicted file to the state it was in before merging.
218
Only text restoration is supported at present.
222
osutils.rename(filename + ".THIS", filename)
225
if e.errno != errno.ENOENT:
228
os.unlink(filename + ".BASE")
231
if e.errno != errno.ENOENT:
234
os.unlink(filename + ".OTHER")
237
if e.errno != errno.ENOENT:
240
raise errors.NotConflicted(filename)
243
class ConflictList(object):
244
"""List of conflicts.
246
Typically obtained from WorkingTree.conflicts()
248
Can be instantiated from stanzas or from Conflict subclasses.
251
def __init__(self, conflicts=None):
252
object.__init__(self)
253
if conflicts is None:
256
self.__list = conflicts
259
return len(self.__list) == 0
262
return len(self.__list)
265
return iter(self.__list)
267
def __getitem__(self, key):
268
return self.__list[key]
270
def append(self, conflict):
271
return self.__list.append(conflict)
273
def __eq__(self, other_list):
274
return list(self) == list(other_list)
276
def __ne__(self, other_list):
277
return not (self == other_list)
280
return "ConflictList(%r)" % self.__list
283
def from_stanzas(stanzas):
284
"""Produce a new ConflictList from an iterable of stanzas"""
285
conflicts = ConflictList()
286
for stanza in stanzas:
287
conflicts.append(Conflict.factory(**stanza.as_dict()))
290
def to_stanzas(self):
291
"""Generator of stanzas"""
292
for conflict in self:
293
yield conflict.as_stanza()
295
def to_strings(self):
296
"""Generate strings for the provided conflicts"""
297
for conflict in self:
298
yield unicode(conflict)
300
def remove_files(self, tree):
301
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
302
for conflict in self:
303
if not conflict.has_files:
305
conflict.cleanup(tree)
307
def select_conflicts(self, tree, paths, ignore_misses=False,
309
"""Select the conflicts associated with paths in a tree.
311
File-ids are also used for this.
312
:return: a pair of ConflictLists: (not_selected, selected)
314
path_set = set(paths)
316
selected_paths = set()
317
new_conflicts = ConflictList()
318
selected_conflicts = ConflictList()
320
file_id = tree.path2id(path)
321
if file_id is not None:
324
for conflict in self:
326
for key in ('path', 'conflict_path'):
327
cpath = getattr(conflict, key, None)
330
if cpath in path_set:
332
selected_paths.add(cpath)
334
if osutils.is_inside_any(path_set, cpath):
336
selected_paths.add(cpath)
338
for key in ('file_id', 'conflict_file_id'):
339
cfile_id = getattr(conflict, key, None)
343
cpath = ids[cfile_id]
347
selected_paths.add(cpath)
349
selected_conflicts.append(conflict)
351
new_conflicts.append(conflict)
352
if ignore_misses is not True:
353
for path in [p for p in paths if p not in selected_paths]:
354
if not os.path.exists(tree.abspath(path)):
355
print "%s does not exist" % path
357
print "%s is not conflicted" % path
358
return new_conflicts, selected_conflicts
361
class Conflict(object):
362
"""Base class for all types of conflict"""
364
# FIXME: cleanup should take care of that ? -- vila 091229
367
def __init__(self, path, file_id=None):
369
# warn turned off, because the factory blindly transfers the Stanza
370
# values to __init__ and Stanza is purely a Unicode api.
371
self.file_id = osutils.safe_file_id(file_id, warn=False)
374
s = rio.Stanza(type=self.typestring, path=self.path)
375
if self.file_id is not None:
376
# Stanza requires Unicode apis
377
s.add('file_id', self.file_id.decode('utf8'))
381
return [type(self), self.path, self.file_id]
383
def __cmp__(self, other):
384
if getattr(other, "_cmp_list", None) is None:
386
return cmp(self._cmp_list(), other._cmp_list())
389
return hash((type(self), self.path, self.file_id))
391
def __eq__(self, other):
392
return self.__cmp__(other) == 0
394
def __ne__(self, other):
395
return not self.__eq__(other)
397
def __unicode__(self):
398
return self.format % self.__dict__
401
rdict = dict(self.__dict__)
402
rdict['class'] = self.__class__.__name__
403
return self.rformat % rdict
406
def factory(type, **kwargs):
408
return ctype[type](**kwargs)
411
def sort_key(conflict):
412
if conflict.path is not None:
413
return conflict.path, conflict.typestring
414
elif getattr(conflict, "conflict_path", None) is not None:
415
return conflict.conflict_path, conflict.typestring
417
return None, conflict.typestring
419
def _do(self, action, tree):
420
"""Apply the specified action to the conflict.
422
:param action: The method name to call.
424
:param tree: The tree passed as a parameter to the method.
426
meth = getattr(self, 'action_%s' % action, None)
428
raise NotImplementedError(self.__class__.__name__ + '.' + action)
431
def associated_filenames(self):
432
"""The names of the files generated to help resolve the conflict."""
433
raise NotImplementedError(self.associated_filenames)
435
def cleanup(self, tree):
436
for fname in self.associated_filenames():
438
osutils.delete_any(tree.abspath(fname))
440
if e.errno != errno.ENOENT:
443
def action_done(self, tree):
444
"""Mark the conflict as solved once it has been handled."""
445
# This method does nothing but simplifies the design of upper levels.
448
def action_take_this(self, tree):
449
raise NotImplementedError(self.action_take_this)
451
def action_take_other(self, tree):
452
raise NotImplementedError(self.action_take_other)
454
def _resolve_with_cleanups(self, tree, *args, **kwargs):
455
tt = transform.TreeTransform(tree)
456
op = cleanup.OperationWithCleanups(self._resolve)
457
op.add_cleanup(tt.finalize)
458
op.run_simple(tt, *args, **kwargs)
461
class PathConflict(Conflict):
462
"""A conflict was encountered merging file paths"""
464
typestring = 'path conflict'
466
format = 'Path conflict: %(path)s / %(conflict_path)s'
468
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
470
def __init__(self, path, conflict_path=None, file_id=None):
471
Conflict.__init__(self, path, file_id)
472
self.conflict_path = conflict_path
475
s = Conflict.as_stanza(self)
476
if self.conflict_path is not None:
477
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
class ContentsConflict(PathConflict):
572
"""The files are of different types (or both binary), or not present"""
576
typestring = 'contents conflict'
578
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):
630
"""The merge algorithm could not resolve all differences encountered."""
634
typestring = 'text conflict'
636
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
class HandledConflict(Conflict):
677
"""A path problem that has been provisionally resolved.
678
This is intended to be a base class.
681
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
683
def __init__(self, action, path, file_id=None):
684
Conflict.__init__(self, path, file_id)
688
return Conflict._cmp_list(self) + [self.action]
691
s = Conflict.as_stanza(self)
692
s.add('action', self.action)
695
def associated_filenames(self):
696
# Nothing has been generated here
700
class HandledPathConflict(HandledConflict):
701
"""A provisionally-resolved path problem involving two paths.
702
This is intended to be a base class.
705
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
706
" %(file_id)r, %(conflict_file_id)r)"
708
def __init__(self, action, path, conflict_path, file_id=None,
709
conflict_file_id=None):
710
HandledConflict.__init__(self, action, path, file_id)
711
self.conflict_path = conflict_path
712
# warn turned off, because the factory blindly transfers the Stanza
713
# values to __init__.
714
self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
718
return HandledConflict._cmp_list(self) + [self.conflict_path,
719
self.conflict_file_id]
722
s = HandledConflict.as_stanza(self)
723
s.add('conflict_path', self.conflict_path)
724
if self.conflict_file_id is not None:
725
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
730
class DuplicateID(HandledPathConflict):
731
"""Two files want the same file_id."""
733
typestring = 'duplicate id'
735
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
738
class DuplicateEntry(HandledPathConflict):
739
"""Two directory entries want to have the same name."""
741
typestring = 'duplicate'
743
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
class ParentLoop(HandledPathConflict):
754
"""An attempt to create an infinitely-looping directory structure.
755
This is rare, but can be produced like so:
764
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),
787
class UnversionedParent(HandledConflict):
788
"""An attempt to version a file whose parent directory is not versioned.
789
Typically, the result of a merge where one tree unversioned the directory
790
and the other added a versioned file to it.
793
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):
808
class MissingParent(HandledConflict):
809
"""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)
815
typestring = 'missing parent'
817
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)
883
def register_types(*conflict_types):
884
"""Register a Conflict subclass for serialization purposes"""
886
for conflict_type in conflict_types:
887
ctype[conflict_type.typestring] = conflict_type
889
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
890
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
891
DeletingParent, NonDirectoryParent)