453
329
if e.errno != errno.ENOENT:
456
def action_done(self, tree):
457
"""Mark the conflict as solved once it has been handled."""
458
# This method does nothing but simplifies the design of upper levels.
461
def action_take_this(self, tree):
462
raise NotImplementedError(self.action_take_this)
464
def action_take_other(self, tree):
465
raise NotImplementedError(self.action_take_other)
467
def _resolve_with_cleanups(self, tree, *args, **kwargs):
468
tt = transform.TreeTransform(tree)
469
op = cleanup.OperationWithCleanups(self._resolve)
470
op.add_cleanup(tt.finalize)
471
op.run_simple(tt, *args, **kwargs)
474
class PathConflict(Conflict):
475
"""A conflict was encountered merging file paths"""
477
typestring = 'path conflict'
479
format = 'Path conflict: %(path)s / %(conflict_path)s'
481
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
483
def __init__(self, path, conflict_path=None, file_id=None):
484
Conflict.__init__(self, path, file_id)
485
self.conflict_path = conflict_path
488
s = Conflict.as_stanza(self)
489
if self.conflict_path is not None:
490
s.add('conflict_path', self.conflict_path)
493
def associated_filenames(self):
494
# No additional files have been generated here
497
def _resolve(self, tt, file_id, path, winner):
498
"""Resolve the conflict.
500
:param tt: The TreeTransform where the conflict is resolved.
501
:param file_id: The retained file id.
502
:param path: The retained path.
503
:param winner: 'this' or 'other' indicates which side is the winner.
505
path_to_create = None
507
if self.path == '<deleted>':
508
return # Nothing to do
509
if self.conflict_path == '<deleted>':
510
path_to_create = self.path
511
revid = tt._tree.get_parent_ids()[0]
512
elif winner == 'other':
513
if self.conflict_path == '<deleted>':
514
return # Nothing to do
515
if self.path == '<deleted>':
516
path_to_create = self.conflict_path
517
# FIXME: If there are more than two parents we may need to
518
# iterate. Taking the last parent is the safer bet in the mean
519
# time. -- vila 20100309
520
revid = tt._tree.get_parent_ids()[-1]
523
raise AssertionError('bad winner: %r' % (winner,))
524
if path_to_create is not None:
525
tid = tt.trans_id_tree_path(path_to_create)
526
tree = self._revision_tree(tt._tree, revid)
527
transform.create_from_tree(
528
tt, tid, tree, tree.id2path(file_id), file_id=file_id)
529
tt.version_file(file_id, tid)
531
tid = tt.trans_id_file_id(file_id)
532
# Adjust the path for the retained file id
533
parent_tid = tt.get_tree_parent(tid)
534
tt.adjust_path(osutils.basename(path), parent_tid, tid)
537
def _revision_tree(self, tree, revid):
538
return tree.branch.repository.revision_tree(revid)
540
def _infer_file_id(self, tree):
541
# Prior to bug #531967, file_id wasn't always set, there may still be
542
# conflict files in the wild so we need to cope with them
543
# Establish which path we should use to find back the file-id
545
for p in (self.path, self.conflict_path):
547
# special hard-coded path
550
possible_paths.append(p)
551
# Search the file-id in the parents with any path available
553
for revid in tree.get_parent_ids():
554
revtree = self._revision_tree(tree, revid)
555
for p in possible_paths:
556
file_id = revtree.path2id(p)
557
if file_id is not None:
558
return revtree, file_id
561
def action_take_this(self, tree):
562
if self.file_id is not None:
563
self._resolve_with_cleanups(tree, self.file_id, self.path,
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)
572
def action_take_other(self, tree):
573
if self.file_id is not None:
574
self._resolve_with_cleanups(tree, self.file_id,
578
# Prior to bug #531967 we need to find back the file_id and restore
579
# the content from there
580
revtree, file_id = self._infer_file_id(tree)
581
tree.revert([revtree.id2path(file_id)],
582
old_tree=revtree, backups=False)
585
class ContentsConflict(PathConflict):
586
"""The files are of different types (or both binary), or not present"""
590
typestring = 'contents conflict'
592
format = 'Contents conflict in %(path)s'
594
def associated_filenames(self):
595
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
597
def _resolve(self, tt, suffix_to_remove):
598
"""Resolve the conflict.
600
:param tt: The TreeTransform where the conflict is resolved.
601
:param suffix_to_remove: Either 'THIS' or 'OTHER'
603
The resolution is symmetric: when taking THIS, OTHER is deleted and
604
item.THIS is renamed into item and vice-versa.
607
# Delete 'item.THIS' or 'item.OTHER' depending on
610
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
611
except errors.NoSuchFile:
612
# There are valid cases where 'item.suffix_to_remove' either
613
# never existed or was already deleted (including the case
614
# where the user deleted it)
617
this_path = tt._tree.id2path(self.file_id)
618
except errors.NoSuchId:
619
# The file is not present anymore. This may happen if the user
620
# deleted the file either manually or when resolving a conflict on
621
# the parent. We may raise some exception to indicate that the
622
# conflict doesn't exist anymore and as such doesn't need to be
623
# resolved ? -- vila 20110615
626
this_tid = tt.trans_id_tree_path(this_path)
627
if this_tid is not None:
628
# Rename 'item.suffix_to_remove' (note that if
629
# 'item.suffix_to_remove' has been deleted, this is a no-op)
630
parent_tid = tt.get_tree_parent(this_tid)
631
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
634
def action_take_this(self, tree):
635
self._resolve_with_cleanups(tree, 'OTHER')
637
def action_take_other(self, tree):
638
self._resolve_with_cleanups(tree, 'THIS')
641
# TODO: There should be a base revid attribute to better inform the user about
642
# how the conflicts were generated.
643
class TextConflict(Conflict):
644
"""The merge algorithm could not resolve all differences encountered."""
648
typestring = 'text conflict'
650
format = 'Text conflict in %(path)s'
652
rformat = '%(class)s(%(path)r, %(file_id)r)'
654
def associated_filenames(self):
655
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
657
def _resolve(self, tt, winner_suffix):
658
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
660
:param tt: The TreeTransform where the conflict is resolved.
661
:param winner_suffix: Either 'THIS' or 'OTHER'
663
The resolution is symmetric, when taking THIS, item.THIS is renamed
664
into item and vice-versa. This takes one of the files as a whole
665
ignoring every difference that could have been merged cleanly.
667
# To avoid useless copies, we switch item and item.winner_suffix, only
668
# item will exist after the conflict has been resolved anyway.
669
item_tid = tt.trans_id_file_id(self.file_id)
670
item_parent_tid = tt.get_tree_parent(item_tid)
671
winner_path = self.path + '.' + winner_suffix
672
winner_tid = tt.trans_id_tree_path(winner_path)
673
winner_parent_tid = tt.get_tree_parent(winner_tid)
674
# Switch the paths to preserve the content
675
tt.adjust_path(osutils.basename(self.path),
676
winner_parent_tid, winner_tid)
677
tt.adjust_path(osutils.basename(winner_path),
678
item_parent_tid, item_tid)
679
# Associate the file_id to the right content
680
tt.unversion_file(item_tid)
681
tt.version_file(self.file_id, winner_tid)
684
def action_take_this(self, tree):
685
self._resolve_with_cleanups(tree, 'THIS')
687
def action_take_other(self, tree):
688
self._resolve_with_cleanups(tree, 'OTHER')
691
class HandledConflict(Conflict):
692
"""A path problem that has been provisionally resolved.
693
This is intended to be a base class.
696
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
698
def __init__(self, action, path, file_id=None):
699
Conflict.__init__(self, path, file_id)
703
return Conflict._cmp_list(self) + [self.action]
706
s = Conflict.as_stanza(self)
707
s.add('action', self.action)
710
def associated_filenames(self):
711
# Nothing has been generated here
715
class HandledPathConflict(HandledConflict):
716
"""A provisionally-resolved path problem involving two paths.
717
This is intended to be a base class.
720
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
721
" %(file_id)r, %(conflict_file_id)r)"
723
def __init__(self, action, path, conflict_path, file_id=None,
724
conflict_file_id=None):
725
HandledConflict.__init__(self, action, path, file_id)
726
self.conflict_path = conflict_path
727
# the factory blindly transfers the Stanza values to __init__,
728
# so they can be unicode.
729
if isinstance(conflict_file_id, text_type):
730
conflict_file_id = cache_utf8.encode(conflict_file_id)
731
self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
734
return HandledConflict._cmp_list(self) + [self.conflict_path,
735
self.conflict_file_id]
738
s = HandledConflict.as_stanza(self)
739
s.add('conflict_path', self.conflict_path)
740
if self.conflict_file_id is not None:
741
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
746
class DuplicateID(HandledPathConflict):
747
"""Two files want the same file_id."""
749
typestring = 'duplicate id'
751
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
754
class DuplicateEntry(HandledPathConflict):
755
"""Two directory entries want to have the same name."""
757
typestring = 'duplicate'
759
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
761
def action_take_this(self, tree):
762
tree.remove([self.conflict_path], force=True, keep_files=False)
763
tree.rename_one(self.path, self.conflict_path)
765
def action_take_other(self, tree):
766
tree.remove([self.path], force=True, keep_files=False)
769
class ParentLoop(HandledPathConflict):
770
"""An attempt to create an infinitely-looping directory structure.
771
This is rare, but can be produced like so:
780
typestring = 'parent loop'
782
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
784
def action_take_this(self, tree):
785
# just acccept brz proposal
788
def action_take_other(self, tree):
789
tt = transform.TreeTransform(tree)
791
p_tid = tt.trans_id_file_id(self.file_id)
792
parent_tid = tt.get_tree_parent(p_tid)
793
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
794
cparent_tid = tt.get_tree_parent(cp_tid)
795
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
796
tt.adjust_path(osutils.basename(self.conflict_path),
803
class UnversionedParent(HandledConflict):
804
"""An attempt to version a file whose parent directory is not versioned.
805
Typically, the result of a merge where one tree unversioned the directory
806
and the other added a versioned file to it.
809
typestring = 'unversioned parent'
811
format = 'Conflict because %(path)s is not versioned, but has versioned'\
812
' children. %(action)s.'
814
# FIXME: We silently do nothing to make tests pass, but most probably the
815
# conflict shouldn't exist (the long story is that the conflict is
816
# generated with another one that can be resolved properly) -- vila 091224
817
def action_take_this(self, tree):
820
def action_take_other(self, tree):
824
class MissingParent(HandledConflict):
825
"""An attempt to add files to a directory that is not present.
826
Typically, the result of a merge where THIS deleted the directory and
827
the OTHER added a file to it.
828
See also: DeletingParent (same situation, THIS and OTHER reversed)
831
typestring = 'missing parent'
833
format = 'Conflict adding files to %(path)s. %(action)s.'
835
def action_take_this(self, tree):
836
tree.remove([self.path], force=True, keep_files=False)
838
def action_take_other(self, tree):
839
# just acccept brz proposal
843
class DeletingParent(HandledConflict):
844
"""An attempt to add files to a directory that is not present.
845
Typically, the result of a merge where one OTHER deleted the directory and
846
the THIS added a file to it.
849
typestring = 'deleting parent'
851
format = "Conflict: can't delete %(path)s because it is not empty. "\
854
# FIXME: It's a bit strange that the default action is not coherent with
855
# MissingParent from the *user* pov.
857
def action_take_this(self, tree):
858
# just acccept brz proposal
861
def action_take_other(self, tree):
862
tree.remove([self.path], force=True, keep_files=False)
865
class NonDirectoryParent(HandledConflict):
866
"""An attempt to add files to a directory that is not a directory or
867
an attempt to change the kind of a directory with files.
870
typestring = 'non-directory parent'
872
format = "Conflict: %(path)s is not a directory, but has files in it."\
875
# FIXME: .OTHER should be used instead of .new when the conflict is created
877
def action_take_this(self, tree):
878
# FIXME: we should preserve that path when the conflict is generated !
879
if self.path.endswith('.new'):
880
conflict_path = self.path[:-(len('.new'))]
881
tree.remove([self.path], force=True, keep_files=False)
882
tree.add(conflict_path)
884
raise NotImplementedError(self.action_take_this)
886
def action_take_other(self, tree):
887
# FIXME: we should preserve that path when the conflict is generated !
888
if self.path.endswith('.new'):
889
conflict_path = self.path[:-(len('.new'))]
890
tree.remove([conflict_path], force=True, keep_files=False)
891
tree.rename_one(self.path, conflict_path)
893
raise NotImplementedError(self.action_take_other)
899
def register_types(*conflict_types):
900
"""Register a Conflict subclass for serialization purposes"""
902
for conflict_type in conflict_types:
903
ctype[conflict_type.typestring] = conflict_type
906
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
907
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
908
DeletingParent, NonDirectoryParent)
332
def do(self, action, tree):
333
"""Apply the specified action to the conflict.
335
:param action: The method name to call.
337
:param tree: The tree passed as a parameter to the method.
339
raise NotImplementedError(self.do)
342
"""Return a string description of this conflict."""
343
raise NotImplementedError(self.describe)