~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

Add a NEWS entry and prepare submission.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2009, 2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2007, 2009 Canonical Ltd
2
2
#
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
18
18
# point down
19
19
 
20
20
import os
 
21
import re
21
22
 
22
23
from bzrlib.lazy_import import lazy_import
23
24
lazy_import(globals(), """
25
26
 
26
27
from bzrlib import (
27
28
    builtins,
28
 
    cleanup,
29
29
    commands,
30
30
    errors,
31
31
    osutils,
32
32
    rio,
33
33
    trace,
34
 
    transform,
35
34
    workingtree,
36
35
    )
37
36
""")
45
44
 
46
45
 
47
46
class cmd_conflicts(commands.Command):
48
 
    __doc__ = """List files with conflicts.
 
47
    """List files with conflicts.
49
48
 
50
49
    Merge will do its best to combine the changes in two branches, but there
51
50
    are some kinds of problems only a human can fix.  When it encounters those,
62
61
            option.Option('text',
63
62
                          help='List paths of files with text conflicts.'),
64
63
        ]
65
 
    _see_also = ['resolve', 'conflict-types']
 
64
    _see_also = ['resolve']
66
65
 
67
66
    def run(self, text=False):
68
67
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
81
80
resolve_action_registry.register(
82
81
    'done', 'done', 'Marks the conflict as resolved' )
83
82
resolve_action_registry.register(
84
 
    'take-this', 'take_this',
 
83
    'keep-mine', 'keep_mine',
85
84
    'Resolve the conflict preserving the version in the working tree' )
86
85
resolve_action_registry.register(
87
 
    'take-other', 'take_other',
 
86
    'take-their', 'take_their',
88
87
    'Resolve the conflict taking the merged version into account' )
89
88
resolve_action_registry.default_key = 'done'
90
89
 
98
97
 
99
98
 
100
99
class cmd_resolve(commands.Command):
101
 
    __doc__ = """Mark a conflict as resolved.
 
100
    """Mark a conflict as resolved.
102
101
 
103
102
    Merge will do its best to combine the changes in two branches, but there
104
103
    are some kinds of problems only a human can fix.  When it encounters those,
407
406
 
408
407
        :param tree: The tree passed as a parameter to the method.
409
408
        """
410
 
        meth = getattr(self, 'action_%s' % action, None)
 
409
        meth = getattr(self, action, None)
411
410
        if meth is None:
412
411
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
413
412
        meth(tree)
414
413
 
415
 
    def associated_filenames(self):
416
 
        """The names of the files generated to help resolve the conflict."""
417
 
        raise NotImplementedError(self.associated_filenames)
418
 
 
419
414
    def cleanup(self, tree):
420
 
        for fname in self.associated_filenames():
421
 
            try:
422
 
                osutils.delete_any(tree.abspath(fname))
423
 
            except OSError, e:
424
 
                if e.errno != errno.ENOENT:
425
 
                    raise
 
415
        raise NotImplementedError(self.cleanup)
426
416
 
427
 
    def action_done(self, tree):
 
417
    def done(self, tree):
428
418
        """Mark the conflict as solved once it has been handled."""
429
419
        # This method does nothing but simplifies the design of upper levels.
430
420
        pass
431
421
 
432
 
    def action_take_this(self, tree):
433
 
        raise NotImplementedError(self.action_take_this)
434
 
 
435
 
    def action_take_other(self, tree):
436
 
        raise NotImplementedError(self.action_take_other)
437
 
 
438
 
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
439
 
        tt = transform.TreeTransform(tree)
440
 
        op = cleanup.OperationWithCleanups(self._resolve)
441
 
        op.add_cleanup(tt.finalize)
442
 
        op.run_simple(tt, *args, **kwargs)
 
422
    def keep_mine(self, tree):
 
423
        raise NotImplementedError(self.keep_mine)
 
424
 
 
425
    def take_their(self, tree):
 
426
        raise NotImplementedError(self.take_their)
443
427
 
444
428
 
445
429
class PathConflict(Conflict):
461
445
            s.add('conflict_path', self.conflict_path)
462
446
        return s
463
447
 
464
 
    def associated_filenames(self):
 
448
    def cleanup(self, tree):
465
449
        # No additional files have been generated here
466
 
        return []
467
 
 
468
 
    def _resolve(self, tt, file_id, path, winner):
469
 
        """Resolve the conflict.
470
 
 
471
 
        :param tt: The TreeTransform where the conflict is resolved.
472
 
        :param file_id: The retained file id.
473
 
        :param path: The retained path.
474
 
        :param winner: 'this' or 'other' indicates which side is the winner.
475
 
        """
476
 
        path_to_create = None
477
 
        if winner == 'this':
478
 
            if self.path == '<deleted>':
479
 
                return # Nothing to do
480
 
            if self.conflict_path == '<deleted>':
481
 
                path_to_create = self.path
482
 
                revid = tt._tree.get_parent_ids()[0]
483
 
        elif winner == 'other':
484
 
            if self.conflict_path == '<deleted>':
485
 
                return  # Nothing to do
486
 
            if self.path == '<deleted>':
487
 
                path_to_create = self.conflict_path
488
 
                # FIXME: If there are more than two parents we may need to
489
 
                # iterate. Taking the last parent is the safer bet in the mean
490
 
                # time. -- vila 20100309
491
 
                revid = tt._tree.get_parent_ids()[-1]
492
 
        else:
493
 
            # Programmer error
494
 
            raise AssertionError('bad winner: %r' % (winner,))
495
 
        if path_to_create is not None:
496
 
            tid = tt.trans_id_tree_path(path_to_create)
497
 
            transform.create_from_tree(
498
 
                tt, tt.trans_id_tree_path(path_to_create),
499
 
                self._revision_tree(tt._tree, revid), file_id)
500
 
            tt.version_file(file_id, tid)
501
 
 
502
 
        # Adjust the path for the retained file id
503
 
        tid = tt.trans_id_file_id(file_id)
504
 
        parent_tid = tt.get_tree_parent(tid)
505
 
        tt.adjust_path(path, parent_tid, tid)
506
 
        tt.apply()
507
 
 
508
 
    def _revision_tree(self, tree, revid):
509
 
        return tree.branch.repository.revision_tree(revid)
510
 
 
511
 
    def _infer_file_id(self, tree):
512
 
        # Prior to bug #531967, file_id wasn't always set, there may still be
513
 
        # conflict files in the wild so we need to cope with them
514
 
        # Establish which path we should use to find back the file-id
515
 
        possible_paths = []
516
 
        for p in (self.path, self.conflict_path):
517
 
            if p == '<deleted>':
518
 
                # special hard-coded path 
519
 
                continue
520
 
            if p is not None:
521
 
                possible_paths.append(p)
522
 
        # Search the file-id in the parents with any path available
523
 
        file_id = None
524
 
        for revid in tree.get_parent_ids():
525
 
            revtree = self._revision_tree(tree, revid)
526
 
            for p in possible_paths:
527
 
                file_id = revtree.path2id(p)
528
 
                if file_id is not None:
529
 
                    return revtree, file_id
530
 
        return None, None
531
 
 
532
 
    def action_take_this(self, tree):
533
 
        if self.file_id is not None:
534
 
            self._resolve_with_cleanups(tree, self.file_id, self.path,
535
 
                                        winner='this')
536
 
        else:
537
 
            # Prior to bug #531967 we need to find back the file_id and restore
538
 
            # the content from there
539
 
            revtree, file_id = self._infer_file_id(tree)
540
 
            tree.revert([revtree.id2path(file_id)],
541
 
                        old_tree=revtree, backups=False)
542
 
 
543
 
    def action_take_other(self, tree):
544
 
        if self.file_id is not None:
545
 
            self._resolve_with_cleanups(tree, self.file_id,
546
 
                                        self.conflict_path,
547
 
                                        winner='other')
548
 
        else:
549
 
            # Prior to bug #531967 we need to find back the file_id and restore
550
 
            # the content from there
551
 
            revtree, file_id = self._infer_file_id(tree)
552
 
            tree.revert([revtree.id2path(file_id)],
553
 
                        old_tree=revtree, backups=False)
 
450
        pass
 
451
 
 
452
    def keep_mine(self, tree):
 
453
        tree.rename_one(self.conflict_path, self.path)
 
454
 
 
455
    def take_their(self, tree):
 
456
        # just acccept bzr proposal
 
457
        pass
554
458
 
555
459
 
556
460
class ContentsConflict(PathConflict):
557
 
    """The files are of different types (or both binary), or not present"""
 
461
    """The files are of different types, or not present"""
558
462
 
559
463
    has_files = True
560
464
 
562
466
 
563
467
    format = 'Contents conflict in %(path)s'
564
468
 
565
 
    def associated_filenames(self):
566
 
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
567
 
 
568
 
    def _resolve(self, tt, suffix_to_remove):
569
 
        """Resolve the conflict.
570
 
 
571
 
        :param tt: The TreeTransform where the conflict is resolved.
572
 
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
573
 
 
574
 
        The resolution is symmetric, when taking THIS, OTHER is deleted and
575
 
        item.THIS is renamed into item and vice-versa.
576
 
        """
577
 
        try:
578
 
            # Delete 'item.THIS' or 'item.OTHER' depending on
579
 
            # suffix_to_remove
580
 
            tt.delete_contents(
581
 
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
582
 
        except errors.NoSuchFile:
583
 
            # There are valid cases where 'item.suffix_to_remove' either
584
 
            # never existed or was already deleted (including the case
585
 
            # where the user deleted it)
586
 
            pass
587
 
        # Rename 'item.suffix_to_remove' (note that if
588
 
        # 'item.suffix_to_remove' has been deleted, this is a no-op)
589
 
        this_tid = tt.trans_id_file_id(self.file_id)
590
 
        parent_tid = tt.get_tree_parent(this_tid)
591
 
        tt.adjust_path(self.path, parent_tid, this_tid)
592
 
        tt.apply()
593
 
 
594
 
    def action_take_this(self, tree):
595
 
        self._resolve_with_cleanups(tree, 'OTHER')
596
 
 
597
 
    def action_take_other(self, tree):
598
 
        self._resolve_with_cleanups(tree, 'THIS')
 
469
    def cleanup(self, tree):
 
470
        for suffix in ('.BASE', '.OTHER'):
 
471
            try:
 
472
                osutils.delete_any(tree.abspath(self.path + suffix))
 
473
            except OSError, e:
 
474
                if e.errno != errno.ENOENT:
 
475
                    raise
 
476
 
 
477
    # FIXME: I smell something weird here and it seems we should be able to be
 
478
    # more coherent with some other conflict ? bzr *did* a choice there but
 
479
    # neither keep_mine nor take_their reflect that... -- vila 091224
 
480
    def keep_mine(self, tree):
 
481
        tree.remove([self.path + '.OTHER'], force=True, keep_files=False)
 
482
 
 
483
    def take_their(self, tree):
 
484
        tree.remove([self.path], force=True, keep_files=False)
 
485
 
599
486
 
600
487
 
601
488
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
612
499
 
613
500
    format = 'Text conflict in %(path)s'
614
501
 
615
 
    def associated_filenames(self):
616
 
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
502
    def cleanup(self, tree):
 
503
        for suffix in CONFLICT_SUFFIXES:
 
504
            try:
 
505
                osutils.delete_any(tree.abspath(self.path+suffix))
 
506
            except OSError, e:
 
507
                if e.errno != errno.ENOENT:
 
508
                    raise
617
509
 
618
510
 
619
511
class HandledConflict(Conflict):
635
527
        s.add('action', self.action)
636
528
        return s
637
529
 
638
 
    def associated_filenames(self):
639
 
        # Nothing has been generated here
640
 
        return []
 
530
    def cleanup(self, tree):
 
531
        """Nothing to cleanup."""
 
532
        pass
641
533
 
642
534
 
643
535
class HandledPathConflict(HandledConflict):
685
577
 
686
578
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
687
579
 
688
 
    def action_take_this(self, tree):
 
580
    def keep_mine(self, tree):
689
581
        tree.remove([self.conflict_path], force=True, keep_files=False)
690
582
        tree.rename_one(self.path, self.conflict_path)
691
583
 
692
 
    def action_take_other(self, tree):
 
584
    def take_their(self, tree):
693
585
        tree.remove([self.path], force=True, keep_files=False)
694
586
 
695
587
 
706
598
 
707
599
    typestring = 'parent loop'
708
600
 
709
 
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
601
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
710
602
 
711
 
    def action_take_this(self, tree):
 
603
    def keep_mine(self, tree):
712
604
        # just acccept bzr proposal
713
605
        pass
714
606
 
715
 
    def action_take_other(self, tree):
 
607
    def take_their(self, tree):
716
608
        # FIXME: We shouldn't have to manipulate so many paths here (and there
717
609
        # is probably a bug or two...)
 
610
        conflict_base_path = osutils.basename(self.conflict_path)
718
611
        base_path = osutils.basename(self.path)
719
 
        conflict_base_path = osutils.basename(self.conflict_path)
720
 
        tt = transform.TreeTransform(tree)
721
 
        try:
722
 
            p_tid = tt.trans_id_file_id(self.file_id)
723
 
            parent_tid = tt.get_tree_parent(p_tid)
724
 
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
725
 
            cparent_tid = tt.get_tree_parent(cp_tid)
726
 
            tt.adjust_path(base_path, cparent_tid, cp_tid)
727
 
            tt.adjust_path(conflict_base_path, parent_tid, p_tid)
728
 
            tt.apply()
729
 
        finally:
730
 
            tt.finalize()
 
612
        tree.rename_one(self.conflict_path, conflict_base_path)
 
613
        tree.rename_one(self.path,
 
614
                        osutils.joinpath([conflict_base_path, base_path]))
731
615
 
732
616
 
733
617
class UnversionedParent(HandledConflict):
744
628
    # FIXME: We silently do nothing to make tests pass, but most probably the
745
629
    # conflict shouldn't exist (the long story is that the conflict is
746
630
    # generated with another one that can be resolved properly) -- vila 091224
747
 
    def action_take_this(self, tree):
 
631
    def keep_mine(self, tree):
748
632
        pass
749
633
 
750
 
    def action_take_other(self, tree):
 
634
    def take_their(self, tree):
751
635
        pass
752
636
 
753
637
 
762
646
 
763
647
    format = 'Conflict adding files to %(path)s.  %(action)s.'
764
648
 
765
 
    def action_take_this(self, tree):
 
649
    def keep_mine(self, tree):
766
650
        tree.remove([self.path], force=True, keep_files=False)
767
651
 
768
 
    def action_take_other(self, tree):
 
652
    def take_their(self, tree):
769
653
        # just acccept bzr proposal
770
654
        pass
771
655
 
784
668
    # FIXME: It's a bit strange that the default action is not coherent with
785
669
    # MissingParent from the *user* pov.
786
670
 
787
 
    def action_take_this(self, tree):
 
671
    def keep_mine(self, tree):
788
672
        # just acccept bzr proposal
789
673
        pass
790
674
 
791
 
    def action_take_other(self, tree):
 
675
    def take_their(self, tree):
792
676
        tree.remove([self.path], force=True, keep_files=False)
793
677
 
794
678
 
802
686
    format = "Conflict: %(path)s is not a directory, but has files in it."\
803
687
             "  %(action)s."
804
688
 
805
 
    # FIXME: .OTHER should be used instead of .new when the conflict is created
806
 
 
807
 
    def action_take_this(self, tree):
 
689
    def keep_mine(self, tree):
808
690
        # FIXME: we should preserve that path when the conflict is generated !
809
691
        if self.path.endswith('.new'):
810
692
            conflict_path = self.path[:-(len('.new'))]
811
693
            tree.remove([self.path], force=True, keep_files=False)
812
694
            tree.add(conflict_path)
813
695
        else:
814
 
            raise NotImplementedError(self.action_take_this)
 
696
            raise NotImplementedError(self.keep_mine)
815
697
 
816
 
    def action_take_other(self, tree):
 
698
    def take_their(self, tree):
817
699
        # FIXME: we should preserve that path when the conflict is generated !
818
700
        if self.path.endswith('.new'):
819
701
            conflict_path = self.path[:-(len('.new'))]
820
702
            tree.remove([conflict_path], force=True, keep_files=False)
821
703
            tree.rename_one(self.path, conflict_path)
822
704
        else:
823
 
            raise NotImplementedError(self.action_take_other)
 
705
            raise NotImplementedError(self.take_their)
824
706
 
825
707
 
826
708
ctype = {}