1
# Copyright (C) 2005 by Aaron Bentley
1
# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 Canonical Ltd
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., 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
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
# TODO: bzr revert should resolve; even when reverting the whole tree
23
# or particular directories
20
from __future__ import absolute_import
24
from bzrlib.lazy_import import lazy_import
25
lazy_import(globals(), """
29
from bzrlib.branch import Branch
30
from bzrlib.errors import BzrCommandError
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
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.
37
from bzrlib.i18n import gettext, ngettext
46
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
49
class cmd_conflicts(commands.Command):
50
__doc__ = """List files with conflicts.
52
Merge will do its best to combine the changes in two branches, but there
53
are some kinds of problems only a human can fix. When it encounters those,
54
it will mark a conflict. A conflict means that you need to fix something,
55
before you can commit.
57
Conflicts normally are listed as short, human-readable messages. If --text
58
is supplied, the pathnames of files with text conflicts are listed,
59
instead. (This is useful for editing all files with text conflicts.)
61
Use bzr resolve when you have fixed a problem.
66
help='List paths of files with text conflicts.'),
68
_see_also = ['resolve', 'conflict-types']
70
def run(self, text=False, directory=u'.'):
71
wt = workingtree.WorkingTree.open_containing(directory)[0]
72
for conflict in wt.conflicts():
74
if conflict.typestring != 'text conflict':
76
self.outf.write(conflict.path + '\n')
78
self.outf.write(unicode(conflict) + '\n')
81
resolve_action_registry = registry.Registry()
84
resolve_action_registry.register(
85
'done', 'done', 'Marks the conflict as resolved.')
86
resolve_action_registry.register(
87
'take-this', 'take_this',
88
'Resolve the conflict preserving the version in the working tree.')
89
resolve_action_registry.register(
90
'take-other', 'take_other',
91
'Resolve the conflict taking the merged version into account.')
92
resolve_action_registry.default_key = 'done'
94
class ResolveActionOption(option.RegistryOption):
97
super(ResolveActionOption, self).__init__(
98
'action', 'How to resolve the conflict.',
100
registry=resolve_action_registry)
103
class cmd_resolve(commands.Command):
104
__doc__ = """Mark a conflict as resolved.
106
Merge will do its best to combine the changes in two branches, but there
107
are some kinds of problems only a human can fix. When it encounters those,
108
it will mark a conflict. A conflict means that you need to fix something,
109
before you can commit.
111
Once you have fixed a problem, use "bzr resolve" to automatically mark
112
text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
113
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
115
aliases = ['resolved']
46
116
takes_args = ['file*']
47
takes_options = ['all']
48
def run(self, file_list=None, all=False):
51
raise BzrCommandError(
52
"command 'resolve' needs one or more FILE, or --all")
53
tree = Branch.open_containing('.')[0].working_tree()
54
file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
57
raise BzrCommandError(
58
"If --all is specified, no FILE may be provided")
59
for filename in file_list:
61
for suffix in CONFLICT_SUFFIXES:
119
option.Option('all', help='Resolve all conflicts in this tree.'),
120
ResolveActionOption(),
122
_see_also = ['conflicts']
123
def run(self, file_list=None, all=False, action=None, directory=None):
126
raise errors.BzrCommandError(gettext("If --all is specified,"
127
" no FILE may be provided"))
128
if directory is None:
130
tree = workingtree.WorkingTree.open_containing(directory)[0]
134
tree, file_list = workingtree.WorkingTree.open_containing_paths(
135
file_list, directory)
136
if file_list is None:
138
# FIXME: There is a special case here related to the option
139
# handling that could be clearer and easier to discover by
140
# providing an --auto action (bug #344013 and #383396) and
141
# make it mandatory instead of implicit and active only
142
# when no file_list is provided -- vila 091229
148
if file_list is None:
149
un_resolved, resolved = tree.auto_resolve()
150
if len(un_resolved) > 0:
151
trace.note(ngettext('%d conflict auto-resolved.',
152
'%d conflicts auto-resolved.', len(resolved)),
154
trace.note(gettext('Remaining conflicts:'))
155
for conflict in un_resolved:
156
trace.note(unicode(conflict))
159
trace.note(gettext('All conflicts resolved.'))
162
# FIXME: This can never occur but the block above needs some
163
# refactoring to transfer tree.auto_resolve() to
164
# conflict.auto(tree) --vila 091242
167
before, after = resolve(tree, file_list, action=action)
168
trace.note(ngettext('{0} conflict resolved, {1} remaining',
169
'{0} conflicts resolved, {1} remaining',
170
before-after).format(before - after, after))
173
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
175
"""Resolve some or all of the conflicts in a working tree.
177
:param paths: If None, resolve all conflicts. Otherwise, select only
179
:param recursive: If True, then elements of paths which are directories
180
have all their children resolved, etc. When invoked as part of
181
recursive commands like revert, this should be True. For commands
182
or applications wishing finer-grained control, like the resolve
183
command, this should be False.
184
:param ignore_misses: If False, warnings will be printed if the supplied
185
paths do not have conflicts.
186
:param action: How the conflict should be resolved,
188
tree.lock_tree_write()
189
nb_conflicts_after = None
191
tree_conflicts = tree.conflicts()
192
nb_conflicts_before = len(tree_conflicts)
194
new_conflicts = ConflictList()
195
to_process = tree_conflicts
197
new_conflicts, to_process = tree_conflicts.select_conflicts(
198
tree, paths, ignore_misses, recursive)
199
for conflict in to_process:
201
conflict._do(action, tree)
202
conflict.cleanup(tree)
203
except NotImplementedError:
204
new_conflicts.append(conflict)
206
nb_conflicts_after = len(new_conflicts)
207
tree.set_conflicts(new_conflicts)
208
except errors.UnsupportedOperation:
212
if nb_conflicts_after is None:
213
nb_conflicts_after = nb_conflicts_before
214
return nb_conflicts_before, nb_conflicts_after
217
def restore(filename):
218
"""Restore a conflicted file to the state it was in before merging.
220
Only text restoration is supported at present.
224
osutils.rename(filename + ".THIS", filename)
227
if e.errno != errno.ENOENT:
230
os.unlink(filename + ".BASE")
233
if e.errno != errno.ENOENT:
236
os.unlink(filename + ".OTHER")
239
if e.errno != errno.ENOENT:
242
raise errors.NotConflicted(filename)
245
class ConflictList(object):
246
"""List of conflicts.
248
Typically obtained from WorkingTree.conflicts()
250
Can be instantiated from stanzas or from Conflict subclasses.
253
def __init__(self, conflicts=None):
254
object.__init__(self)
255
if conflicts is None:
258
self.__list = conflicts
261
return len(self.__list) == 0
264
return len(self.__list)
267
return iter(self.__list)
269
def __getitem__(self, key):
270
return self.__list[key]
272
def append(self, conflict):
273
return self.__list.append(conflict)
275
def __eq__(self, other_list):
276
return list(self) == list(other_list)
278
def __ne__(self, other_list):
279
return not (self == other_list)
282
return "ConflictList(%r)" % self.__list
285
def from_stanzas(stanzas):
286
"""Produce a new ConflictList from an iterable of stanzas"""
287
conflicts = ConflictList()
288
for stanza in stanzas:
289
conflicts.append(Conflict.factory(**stanza.as_dict()))
292
def to_stanzas(self):
293
"""Generator of stanzas"""
294
for conflict in self:
295
yield conflict.as_stanza()
297
def to_strings(self):
298
"""Generate strings for the provided conflicts"""
299
for conflict in self:
300
yield unicode(conflict)
302
def remove_files(self, tree):
303
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
304
for conflict in self:
305
if not conflict.has_files:
307
conflict.cleanup(tree)
309
def select_conflicts(self, tree, paths, ignore_misses=False,
311
"""Select the conflicts associated with paths in a tree.
313
File-ids are also used for this.
314
:return: a pair of ConflictLists: (not_selected, selected)
316
path_set = set(paths)
318
selected_paths = set()
319
new_conflicts = ConflictList()
320
selected_conflicts = ConflictList()
322
file_id = tree.path2id(path)
323
if file_id is not None:
326
for conflict in self:
328
for key in ('path', 'conflict_path'):
329
cpath = getattr(conflict, key, None)
332
if cpath in path_set:
334
selected_paths.add(cpath)
336
if osutils.is_inside_any(path_set, cpath):
338
selected_paths.add(cpath)
340
for key in ('file_id', 'conflict_file_id'):
341
cfile_id = getattr(conflict, key, None)
63
os.unlink(filename+suffix)
65
if e.errno != errno.ENOENT:
69
if failures == len(CONFLICT_SUFFIXES):
70
if not os.path.exists(filename):
71
print "%s does not exist" % filename
345
cpath = ids[cfile_id]
349
selected_paths.add(cpath)
351
selected_conflicts.append(conflict)
353
new_conflicts.append(conflict)
354
if ignore_misses is not True:
355
for path in [p for p in paths if p not in selected_paths]:
356
if not os.path.exists(tree.abspath(path)):
357
print "%s does not exist" % path
73
print "%s is not conflicted" % filename
359
print "%s is not conflicted" % path
360
return new_conflicts, selected_conflicts
363
class Conflict(object):
364
"""Base class for all types of conflict"""
366
# FIXME: cleanup should take care of that ? -- vila 091229
369
def __init__(self, path, file_id=None):
371
# warn turned off, because the factory blindly transfers the Stanza
372
# values to __init__ and Stanza is purely a Unicode api.
373
self.file_id = osutils.safe_file_id(file_id, warn=False)
376
s = rio.Stanza(type=self.typestring, path=self.path)
377
if self.file_id is not None:
378
# Stanza requires Unicode apis
379
s.add('file_id', self.file_id.decode('utf8'))
383
return [type(self), self.path, self.file_id]
385
def __cmp__(self, other):
386
if getattr(other, "_cmp_list", None) is None:
388
return cmp(self._cmp_list(), other._cmp_list())
391
return hash((type(self), self.path, self.file_id))
393
def __eq__(self, other):
394
return self.__cmp__(other) == 0
396
def __ne__(self, other):
397
return not self.__eq__(other)
399
def __unicode__(self):
400
return self.format % self.__dict__
403
rdict = dict(self.__dict__)
404
rdict['class'] = self.__class__.__name__
405
return self.rformat % rdict
408
def factory(type, **kwargs):
410
return ctype[type](**kwargs)
413
def sort_key(conflict):
414
if conflict.path is not None:
415
return conflict.path, conflict.typestring
416
elif getattr(conflict, "conflict_path", None) is not None:
417
return conflict.conflict_path, conflict.typestring
419
return None, conflict.typestring
421
def _do(self, action, tree):
422
"""Apply the specified action to the conflict.
424
:param action: The method name to call.
426
:param tree: The tree passed as a parameter to the method.
428
meth = getattr(self, 'action_%s' % action, None)
430
raise NotImplementedError(self.__class__.__name__ + '.' + action)
433
def associated_filenames(self):
434
"""The names of the files generated to help resolve the conflict."""
435
raise NotImplementedError(self.associated_filenames)
437
def cleanup(self, tree):
438
for fname in self.associated_filenames():
440
osutils.delete_any(tree.abspath(fname))
442
if e.errno != errno.ENOENT:
445
def action_done(self, tree):
446
"""Mark the conflict as solved once it has been handled."""
447
# This method does nothing but simplifies the design of upper levels.
450
def action_take_this(self, tree):
451
raise NotImplementedError(self.action_take_this)
453
def action_take_other(self, tree):
454
raise NotImplementedError(self.action_take_other)
456
def _resolve_with_cleanups(self, tree, *args, **kwargs):
457
tt = transform.TreeTransform(tree)
458
op = cleanup.OperationWithCleanups(self._resolve)
459
op.add_cleanup(tt.finalize)
460
op.run_simple(tt, *args, **kwargs)
463
class PathConflict(Conflict):
464
"""A conflict was encountered merging file paths"""
466
typestring = 'path conflict'
468
format = 'Path conflict: %(path)s / %(conflict_path)s'
470
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
472
def __init__(self, path, conflict_path=None, file_id=None):
473
Conflict.__init__(self, path, file_id)
474
self.conflict_path = conflict_path
477
s = Conflict.as_stanza(self)
478
if self.conflict_path is not None:
479
s.add('conflict_path', self.conflict_path)
482
def associated_filenames(self):
483
# No additional files have been generated here
486
def _resolve(self, tt, file_id, path, winner):
487
"""Resolve the conflict.
489
:param tt: The TreeTransform where the conflict is resolved.
490
:param file_id: The retained file id.
491
:param path: The retained path.
492
:param winner: 'this' or 'other' indicates which side is the winner.
494
path_to_create = None
496
if self.path == '<deleted>':
497
return # Nothing to do
498
if self.conflict_path == '<deleted>':
499
path_to_create = self.path
500
revid = tt._tree.get_parent_ids()[0]
501
elif winner == 'other':
502
if self.conflict_path == '<deleted>':
503
return # Nothing to do
504
if self.path == '<deleted>':
505
path_to_create = self.conflict_path
506
# FIXME: If there are more than two parents we may need to
507
# iterate. Taking the last parent is the safer bet in the mean
508
# time. -- vila 20100309
509
revid = tt._tree.get_parent_ids()[-1]
512
raise AssertionError('bad winner: %r' % (winner,))
513
if path_to_create is not None:
514
tid = tt.trans_id_tree_path(path_to_create)
515
transform.create_from_tree(
516
tt, tid, self._revision_tree(tt._tree, revid), file_id)
517
tt.version_file(file_id, tid)
519
tid = tt.trans_id_file_id(file_id)
520
# Adjust the path for the retained file id
521
parent_tid = tt.get_tree_parent(tid)
522
tt.adjust_path(osutils.basename(path), parent_tid, tid)
525
def _revision_tree(self, tree, revid):
526
return tree.branch.repository.revision_tree(revid)
528
def _infer_file_id(self, tree):
529
# Prior to bug #531967, file_id wasn't always set, there may still be
530
# conflict files in the wild so we need to cope with them
531
# Establish which path we should use to find back the file-id
533
for p in (self.path, self.conflict_path):
535
# special hard-coded path
538
possible_paths.append(p)
539
# Search the file-id in the parents with any path available
541
for revid in tree.get_parent_ids():
542
revtree = self._revision_tree(tree, revid)
543
for p in possible_paths:
544
file_id = revtree.path2id(p)
545
if file_id is not None:
546
return revtree, file_id
549
def action_take_this(self, tree):
550
if self.file_id is not None:
551
self._resolve_with_cleanups(tree, self.file_id, self.path,
554
# Prior to bug #531967 we need to find back the file_id and restore
555
# the content from there
556
revtree, file_id = self._infer_file_id(tree)
557
tree.revert([revtree.id2path(file_id)],
558
old_tree=revtree, backups=False)
560
def action_take_other(self, tree):
561
if self.file_id is not None:
562
self._resolve_with_cleanups(tree, self.file_id,
566
# Prior to bug #531967 we need to find back the file_id and restore
567
# the content from there
568
revtree, file_id = self._infer_file_id(tree)
569
tree.revert([revtree.id2path(file_id)],
570
old_tree=revtree, backups=False)
573
class ContentsConflict(PathConflict):
574
"""The files are of different types (or both binary), or not present"""
578
typestring = 'contents conflict'
580
format = 'Contents conflict in %(path)s'
582
def associated_filenames(self):
583
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
585
def _resolve(self, tt, suffix_to_remove):
586
"""Resolve the conflict.
588
:param tt: The TreeTransform where the conflict is resolved.
589
:param suffix_to_remove: Either 'THIS' or 'OTHER'
591
The resolution is symmetric: when taking THIS, OTHER is deleted and
592
item.THIS is renamed into item and vice-versa.
595
# Delete 'item.THIS' or 'item.OTHER' depending on
598
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
599
except errors.NoSuchFile:
600
# There are valid cases where 'item.suffix_to_remove' either
601
# never existed or was already deleted (including the case
602
# where the user deleted it)
605
this_path = tt._tree.id2path(self.file_id)
606
except errors.NoSuchId:
607
# The file is not present anymore. This may happen if the user
608
# deleted the file either manually or when resolving a conflict on
609
# the parent. We may raise some exception to indicate that the
610
# conflict doesn't exist anymore and as such doesn't need to be
611
# resolved ? -- vila 20110615
614
this_tid = tt.trans_id_tree_path(this_path)
615
if this_tid is not None:
616
# Rename 'item.suffix_to_remove' (note that if
617
# 'item.suffix_to_remove' has been deleted, this is a no-op)
618
parent_tid = tt.get_tree_parent(this_tid)
619
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
622
def action_take_this(self, tree):
623
self._resolve_with_cleanups(tree, 'OTHER')
625
def action_take_other(self, tree):
626
self._resolve_with_cleanups(tree, 'THIS')
629
# TODO: There should be a base revid attribute to better inform the user about
630
# how the conflicts were generated.
631
class TextConflict(Conflict):
632
"""The merge algorithm could not resolve all differences encountered."""
636
typestring = 'text conflict'
638
format = 'Text conflict in %(path)s'
640
rformat = '%(class)s(%(path)r, %(file_id)r)'
642
def associated_filenames(self):
643
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
645
def _resolve(self, tt, winner_suffix):
646
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
648
:param tt: The TreeTransform where the conflict is resolved.
649
:param winner_suffix: Either 'THIS' or 'OTHER'
651
The resolution is symmetric, when taking THIS, item.THIS is renamed
652
into item and vice-versa. This takes one of the files as a whole
653
ignoring every difference that could have been merged cleanly.
655
# To avoid useless copies, we switch item and item.winner_suffix, only
656
# item will exist after the conflict has been resolved anyway.
657
item_tid = tt.trans_id_file_id(self.file_id)
658
item_parent_tid = tt.get_tree_parent(item_tid)
659
winner_path = self.path + '.' + winner_suffix
660
winner_tid = tt.trans_id_tree_path(winner_path)
661
winner_parent_tid = tt.get_tree_parent(winner_tid)
662
# Switch the paths to preserve the content
663
tt.adjust_path(osutils.basename(self.path),
664
winner_parent_tid, winner_tid)
665
tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
666
# Associate the file_id to the right content
667
tt.unversion_file(item_tid)
668
tt.version_file(self.file_id, winner_tid)
671
def action_take_this(self, tree):
672
self._resolve_with_cleanups(tree, 'THIS')
674
def action_take_other(self, tree):
675
self._resolve_with_cleanups(tree, 'OTHER')
678
class HandledConflict(Conflict):
679
"""A path problem that has been provisionally resolved.
680
This is intended to be a base class.
683
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
685
def __init__(self, action, path, file_id=None):
686
Conflict.__init__(self, path, file_id)
690
return Conflict._cmp_list(self) + [self.action]
693
s = Conflict.as_stanza(self)
694
s.add('action', self.action)
697
def associated_filenames(self):
698
# Nothing has been generated here
702
class HandledPathConflict(HandledConflict):
703
"""A provisionally-resolved path problem involving two paths.
704
This is intended to be a base class.
707
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
708
" %(file_id)r, %(conflict_file_id)r)"
710
def __init__(self, action, path, conflict_path, file_id=None,
711
conflict_file_id=None):
712
HandledConflict.__init__(self, action, path, file_id)
713
self.conflict_path = conflict_path
714
# warn turned off, because the factory blindly transfers the Stanza
715
# values to __init__.
716
self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
720
return HandledConflict._cmp_list(self) + [self.conflict_path,
721
self.conflict_file_id]
724
s = HandledConflict.as_stanza(self)
725
s.add('conflict_path', self.conflict_path)
726
if self.conflict_file_id is not None:
727
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
732
class DuplicateID(HandledPathConflict):
733
"""Two files want the same file_id."""
735
typestring = 'duplicate id'
737
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
740
class DuplicateEntry(HandledPathConflict):
741
"""Two directory entries want to have the same name."""
743
typestring = 'duplicate'
745
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
747
def action_take_this(self, tree):
748
tree.remove([self.conflict_path], force=True, keep_files=False)
749
tree.rename_one(self.path, self.conflict_path)
751
def action_take_other(self, tree):
752
tree.remove([self.path], force=True, keep_files=False)
755
class ParentLoop(HandledPathConflict):
756
"""An attempt to create an infinitely-looping directory structure.
757
This is rare, but can be produced like so:
766
typestring = 'parent loop'
768
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
770
def action_take_this(self, tree):
771
# just acccept bzr proposal
774
def action_take_other(self, tree):
775
tt = transform.TreeTransform(tree)
777
p_tid = tt.trans_id_file_id(self.file_id)
778
parent_tid = tt.get_tree_parent(p_tid)
779
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
780
cparent_tid = tt.get_tree_parent(cp_tid)
781
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
782
tt.adjust_path(osutils.basename(self.conflict_path),
789
class UnversionedParent(HandledConflict):
790
"""An attempt to version a file whose parent directory is not versioned.
791
Typically, the result of a merge where one tree unversioned the directory
792
and the other added a versioned file to it.
795
typestring = 'unversioned parent'
797
format = 'Conflict because %(path)s is not versioned, but has versioned'\
798
' children. %(action)s.'
800
# FIXME: We silently do nothing to make tests pass, but most probably the
801
# conflict shouldn't exist (the long story is that the conflict is
802
# generated with another one that can be resolved properly) -- vila 091224
803
def action_take_this(self, tree):
806
def action_take_other(self, tree):
810
class MissingParent(HandledConflict):
811
"""An attempt to add files to a directory that is not present.
812
Typically, the result of a merge where THIS deleted the directory and
813
the OTHER added a file to it.
814
See also: DeletingParent (same situation, THIS and OTHER reversed)
817
typestring = 'missing parent'
819
format = 'Conflict adding files to %(path)s. %(action)s.'
821
def action_take_this(self, tree):
822
tree.remove([self.path], force=True, keep_files=False)
824
def action_take_other(self, tree):
825
# just acccept bzr proposal
829
class DeletingParent(HandledConflict):
830
"""An attempt to add files to a directory that is not present.
831
Typically, the result of a merge where one OTHER deleted the directory and
832
the THIS added a file to it.
835
typestring = 'deleting parent'
837
format = "Conflict: can't delete %(path)s because it is not empty. "\
840
# FIXME: It's a bit strange that the default action is not coherent with
841
# MissingParent from the *user* pov.
843
def action_take_this(self, tree):
844
# just acccept bzr proposal
847
def action_take_other(self, tree):
848
tree.remove([self.path], force=True, keep_files=False)
851
class NonDirectoryParent(HandledConflict):
852
"""An attempt to add files to a directory that is not a directory or
853
an attempt to change the kind of a directory with files.
856
typestring = 'non-directory parent'
858
format = "Conflict: %(path)s is not a directory, but has files in it."\
861
# FIXME: .OTHER should be used instead of .new when the conflict is created
863
def action_take_this(self, tree):
864
# FIXME: we should preserve that path when the conflict is generated !
865
if self.path.endswith('.new'):
866
conflict_path = self.path[:-(len('.new'))]
867
tree.remove([self.path], force=True, keep_files=False)
868
tree.add(conflict_path)
870
raise NotImplementedError(self.action_take_this)
872
def action_take_other(self, tree):
873
# FIXME: we should preserve that path when the conflict is generated !
874
if self.path.endswith('.new'):
875
conflict_path = self.path[:-(len('.new'))]
876
tree.remove([conflict_path], force=True, keep_files=False)
877
tree.rename_one(self.path, conflict_path)
879
raise NotImplementedError(self.action_take_other)
885
def register_types(*conflict_types):
886
"""Register a Conflict subclass for serialization purposes"""
888
for conflict_type in conflict_types:
889
ctype[conflict_type.typestring] = conflict_type
891
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
892
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
893
DeletingParent, NonDirectoryParent)