~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

(gz) Backslash escape selftest output when printing to non-unicode consoles
 (Martin [gz])

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2007, 2009, 2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2009, 2010 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
22
21
 
23
22
from bzrlib.lazy_import import lazy_import
24
23
lazy_import(globals(), """
25
24
import errno
26
25
 
27
26
from bzrlib import (
28
 
    builtins,
 
27
    cleanup,
29
28
    commands,
30
29
    errors,
31
30
    osutils,
45
44
 
46
45
 
47
46
class cmd_conflicts(commands.Command):
48
 
    """List files with conflicts.
 
47
    __doc__ = """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,
59
58
    Use bzr resolve when you have fixed a problem.
60
59
    """
61
60
    takes_options = [
 
61
            'directory',
62
62
            option.Option('text',
63
63
                          help='List paths of files with text conflicts.'),
64
64
        ]
65
65
    _see_also = ['resolve', 'conflict-types']
66
66
 
67
 
    def run(self, text=False):
68
 
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
 
67
    def run(self, text=False, directory=u'.'):
 
68
        wt = workingtree.WorkingTree.open_containing(directory)[0]
69
69
        for conflict in wt.conflicts():
70
70
            if text:
71
71
                if conflict.typestring != 'text conflict':
98
98
 
99
99
 
100
100
class cmd_resolve(commands.Command):
101
 
    """Mark a conflict as resolved.
 
101
    __doc__ = """Mark a conflict as resolved.
102
102
 
103
103
    Merge will do its best to combine the changes in two branches, but there
104
104
    are some kinds of problems only a human can fix.  When it encounters those,
112
112
    aliases = ['resolved']
113
113
    takes_args = ['file*']
114
114
    takes_options = [
 
115
            'directory',
115
116
            option.Option('all', help='Resolve all conflicts in this tree.'),
116
117
            ResolveActionOption(),
117
118
            ]
118
119
    _see_also = ['conflicts']
119
 
    def run(self, file_list=None, all=False, action=None):
 
120
    def run(self, file_list=None, all=False, action=None, directory=None):
120
121
        if all:
121
122
            if file_list:
122
123
                raise errors.BzrCommandError("If --all is specified,"
123
124
                                             " no FILE may be provided")
124
 
            tree = workingtree.WorkingTree.open_containing('.')[0]
 
125
            if directory is None:
 
126
                directory = u'.'
 
127
            tree = workingtree.WorkingTree.open_containing(directory)[0]
125
128
            if action is None:
126
129
                action = 'done'
127
130
        else:
128
 
            tree, file_list = builtins.tree_files(file_list)
 
131
            tree, file_list = workingtree.WorkingTree.open_containing_paths(
 
132
                file_list, directory)
129
133
            if file_list is None:
130
134
                if action is None:
131
135
                    # FIXME: There is a special case here related to the option
155
159
                # conflict.auto(tree) --vila 091242
156
160
                pass
157
161
        else:
158
 
            resolve(tree, file_list, action=action)
 
162
            before, after = resolve(tree, file_list, action=action)
 
163
            trace.note('%d conflict(s) resolved, %d remaining'
 
164
                       % (before - after, after))
159
165
 
160
166
 
161
167
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
174
180
    :param action: How the conflict should be resolved,
175
181
    """
176
182
    tree.lock_tree_write()
 
183
    nb_conflicts_after = None
177
184
    try:
178
185
        tree_conflicts = tree.conflicts()
 
186
        nb_conflicts_before = len(tree_conflicts)
179
187
        if paths is None:
180
188
            new_conflicts = ConflictList()
181
189
            to_process = tree_conflicts
189
197
            except NotImplementedError:
190
198
                new_conflicts.append(conflict)
191
199
        try:
 
200
            nb_conflicts_after = len(new_conflicts)
192
201
            tree.set_conflicts(new_conflicts)
193
202
        except errors.UnsupportedOperation:
194
203
            pass
195
204
    finally:
196
205
        tree.unlock()
 
206
    if nb_conflicts_after is None:
 
207
        nb_conflicts_after = nb_conflicts_before
 
208
    return nb_conflicts_before, nb_conflicts_after
197
209
 
198
210
 
199
211
def restore(filename):
407
419
 
408
420
        :param tree: The tree passed as a parameter to the method.
409
421
        """
410
 
        meth = getattr(self, action, None)
 
422
        meth = getattr(self, 'action_%s' % action, None)
411
423
        if meth is None:
412
424
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
413
425
        meth(tree)
414
426
 
 
427
    def associated_filenames(self):
 
428
        """The names of the files generated to help resolve the conflict."""
 
429
        raise NotImplementedError(self.associated_filenames)
 
430
 
415
431
    def cleanup(self, tree):
416
 
        raise NotImplementedError(self.cleanup)
 
432
        for fname in self.associated_filenames():
 
433
            try:
 
434
                osutils.delete_any(tree.abspath(fname))
 
435
            except OSError, e:
 
436
                if e.errno != errno.ENOENT:
 
437
                    raise
417
438
 
418
 
    def done(self, tree):
 
439
    def action_done(self, tree):
419
440
        """Mark the conflict as solved once it has been handled."""
420
441
        # This method does nothing but simplifies the design of upper levels.
421
442
        pass
422
443
 
423
 
    def take_this(self, tree):
424
 
        raise NotImplementedError(self.take_this)
425
 
 
426
 
    def take_other(self, tree):
427
 
        raise NotImplementedError(self.take_other)
 
444
    def action_take_this(self, tree):
 
445
        raise NotImplementedError(self.action_take_this)
 
446
 
 
447
    def action_take_other(self, tree):
 
448
        raise NotImplementedError(self.action_take_other)
 
449
 
 
450
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
 
451
        tt = transform.TreeTransform(tree)
 
452
        op = cleanup.OperationWithCleanups(self._resolve)
 
453
        op.add_cleanup(tt.finalize)
 
454
        op.run_simple(tt, *args, **kwargs)
428
455
 
429
456
 
430
457
class PathConflict(Conflict):
446
473
            s.add('conflict_path', self.conflict_path)
447
474
        return s
448
475
 
449
 
    def cleanup(self, tree):
 
476
    def associated_filenames(self):
450
477
        # No additional files have been generated here
451
 
        pass
452
 
 
453
 
    def take_this(self, tree):
454
 
        tree.rename_one(self.conflict_path, self.path)
455
 
 
456
 
    def take_other(self, tree):
457
 
        # just acccept bzr proposal
458
 
        pass
 
478
        return []
 
479
 
 
480
    def _resolve(self, tt, file_id, path, winner):
 
481
        """Resolve the conflict.
 
482
 
 
483
        :param tt: The TreeTransform where the conflict is resolved.
 
484
        :param file_id: The retained file id.
 
485
        :param path: The retained path.
 
486
        :param winner: 'this' or 'other' indicates which side is the winner.
 
487
        """
 
488
        path_to_create = None
 
489
        if winner == 'this':
 
490
            if self.path == '<deleted>':
 
491
                return # Nothing to do
 
492
            if self.conflict_path == '<deleted>':
 
493
                path_to_create = self.path
 
494
                revid = tt._tree.get_parent_ids()[0]
 
495
        elif winner == 'other':
 
496
            if self.conflict_path == '<deleted>':
 
497
                return  # Nothing to do
 
498
            if self.path == '<deleted>':
 
499
                path_to_create = self.conflict_path
 
500
                # FIXME: If there are more than two parents we may need to
 
501
                # iterate. Taking the last parent is the safer bet in the mean
 
502
                # time. -- vila 20100309
 
503
                revid = tt._tree.get_parent_ids()[-1]
 
504
        else:
 
505
            # Programmer error
 
506
            raise AssertionError('bad winner: %r' % (winner,))
 
507
        if path_to_create is not None:
 
508
            tid = tt.trans_id_tree_path(path_to_create)
 
509
            transform.create_from_tree(
 
510
                tt, tt.trans_id_tree_path(path_to_create),
 
511
                self._revision_tree(tt._tree, revid), file_id)
 
512
            tt.version_file(file_id, tid)
 
513
 
 
514
        # Adjust the path for the retained file id
 
515
        tid = tt.trans_id_file_id(file_id)
 
516
        parent_tid = tt.get_tree_parent(tid)
 
517
        tt.adjust_path(path, parent_tid, tid)
 
518
        tt.apply()
 
519
 
 
520
    def _revision_tree(self, tree, revid):
 
521
        return tree.branch.repository.revision_tree(revid)
 
522
 
 
523
    def _infer_file_id(self, tree):
 
524
        # Prior to bug #531967, file_id wasn't always set, there may still be
 
525
        # conflict files in the wild so we need to cope with them
 
526
        # Establish which path we should use to find back the file-id
 
527
        possible_paths = []
 
528
        for p in (self.path, self.conflict_path):
 
529
            if p == '<deleted>':
 
530
                # special hard-coded path 
 
531
                continue
 
532
            if p is not None:
 
533
                possible_paths.append(p)
 
534
        # Search the file-id in the parents with any path available
 
535
        file_id = None
 
536
        for revid in tree.get_parent_ids():
 
537
            revtree = self._revision_tree(tree, revid)
 
538
            for p in possible_paths:
 
539
                file_id = revtree.path2id(p)
 
540
                if file_id is not None:
 
541
                    return revtree, file_id
 
542
        return None, None
 
543
 
 
544
    def action_take_this(self, tree):
 
545
        if self.file_id is not None:
 
546
            self._resolve_with_cleanups(tree, self.file_id, self.path,
 
547
                                        winner='this')
 
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)
 
554
 
 
555
    def action_take_other(self, tree):
 
556
        if self.file_id is not None:
 
557
            self._resolve_with_cleanups(tree, self.file_id,
 
558
                                        self.conflict_path,
 
559
                                        winner='other')
 
560
        else:
 
561
            # Prior to bug #531967 we need to find back the file_id and restore
 
562
            # the content from there
 
563
            revtree, file_id = self._infer_file_id(tree)
 
564
            tree.revert([revtree.id2path(file_id)],
 
565
                        old_tree=revtree, backups=False)
459
566
 
460
567
 
461
568
class ContentsConflict(PathConflict):
462
 
    """The files are of different types, or not present"""
 
569
    """The files are of different types (or both binary), or not present"""
463
570
 
464
571
    has_files = True
465
572
 
467
574
 
468
575
    format = 'Contents conflict in %(path)s'
469
576
 
470
 
    def cleanup(self, tree):
471
 
        for suffix in ('.BASE', '.OTHER'):
472
 
            try:
473
 
                osutils.delete_any(tree.abspath(self.path + suffix))
474
 
            except OSError, e:
475
 
                if e.errno != errno.ENOENT:
476
 
                    raise
477
 
 
478
 
    # FIXME: I smell something weird here and it seems we should be able to be
479
 
    # more coherent with some other conflict ? bzr *did* a choice there but
480
 
    # neither take_this nor take_other reflect that... -- vila 091224
481
 
    def take_this(self, tree):
482
 
        tree.remove([self.path + '.OTHER'], force=True, keep_files=False)
483
 
 
484
 
    def take_other(self, tree):
485
 
        tree.remove([self.path], force=True, keep_files=False)
486
 
 
487
 
 
488
 
 
489
 
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
490
 
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
 
577
    def associated_filenames(self):
 
578
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
 
579
 
 
580
    def _resolve(self, tt, suffix_to_remove):
 
581
        """Resolve the conflict.
 
582
 
 
583
        :param tt: The TreeTransform where the conflict is resolved.
 
584
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
 
585
 
 
586
        The resolution is symmetric, when taking THIS, OTHER is deleted and
 
587
        item.THIS is renamed into item and vice-versa.
 
588
        """
 
589
        try:
 
590
            # Delete 'item.THIS' or 'item.OTHER' depending on
 
591
            # suffix_to_remove
 
592
            tt.delete_contents(
 
593
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
 
594
        except errors.NoSuchFile:
 
595
            # There are valid cases where 'item.suffix_to_remove' either
 
596
            # never existed or was already deleted (including the case
 
597
            # where the user deleted it)
 
598
            pass
 
599
        # Rename 'item.suffix_to_remove' (note that if
 
600
        # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
601
        this_tid = tt.trans_id_file_id(self.file_id)
 
602
        parent_tid = tt.get_tree_parent(this_tid)
 
603
        tt.adjust_path(self.path, parent_tid, this_tid)
 
604
        tt.apply()
 
605
 
 
606
    def action_take_this(self, tree):
 
607
        self._resolve_with_cleanups(tree, 'OTHER')
 
608
 
 
609
    def action_take_other(self, tree):
 
610
        self._resolve_with_cleanups(tree, 'THIS')
 
611
 
491
612
 
492
613
# TODO: There should be a base revid attribute to better inform the user about
493
614
# how the conflicts were generated.
494
 
class TextConflict(PathConflict):
 
615
class TextConflict(Conflict):
495
616
    """The merge algorithm could not resolve all differences encountered."""
496
617
 
497
618
    has_files = True
500
621
 
501
622
    format = 'Text conflict in %(path)s'
502
623
 
503
 
    def cleanup(self, tree):
504
 
        for suffix in CONFLICT_SUFFIXES:
505
 
            try:
506
 
                osutils.delete_any(tree.abspath(self.path+suffix))
507
 
            except OSError, e:
508
 
                if e.errno != errno.ENOENT:
509
 
                    raise
 
624
    rformat = '%(class)s(%(path)r, %(file_id)r)'
 
625
 
 
626
    def associated_filenames(self):
 
627
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
510
628
 
511
629
 
512
630
class HandledConflict(Conflict):
528
646
        s.add('action', self.action)
529
647
        return s
530
648
 
531
 
    def cleanup(self, tree):
532
 
        """Nothing to cleanup."""
533
 
        pass
 
649
    def associated_filenames(self):
 
650
        # Nothing has been generated here
 
651
        return []
534
652
 
535
653
 
536
654
class HandledPathConflict(HandledConflict):
578
696
 
579
697
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
580
698
 
581
 
    def take_this(self, tree):
 
699
    def action_take_this(self, tree):
582
700
        tree.remove([self.conflict_path], force=True, keep_files=False)
583
701
        tree.rename_one(self.path, self.conflict_path)
584
702
 
585
 
    def take_other(self, tree):
 
703
    def action_take_other(self, tree):
586
704
        tree.remove([self.path], force=True, keep_files=False)
587
705
 
588
706
 
599
717
 
600
718
    typestring = 'parent loop'
601
719
 
602
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
720
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
603
721
 
604
 
    def take_this(self, tree):
 
722
    def action_take_this(self, tree):
605
723
        # just acccept bzr proposal
606
724
        pass
607
725
 
608
 
    def take_other(self, tree):
 
726
    def action_take_other(self, tree):
609
727
        # FIXME: We shouldn't have to manipulate so many paths here (and there
610
728
        # is probably a bug or two...)
611
729
        base_path = osutils.basename(self.path)
637
755
    # FIXME: We silently do nothing to make tests pass, but most probably the
638
756
    # conflict shouldn't exist (the long story is that the conflict is
639
757
    # generated with another one that can be resolved properly) -- vila 091224
640
 
    def take_this(self, tree):
 
758
    def action_take_this(self, tree):
641
759
        pass
642
760
 
643
 
    def take_other(self, tree):
 
761
    def action_take_other(self, tree):
644
762
        pass
645
763
 
646
764
 
655
773
 
656
774
    format = 'Conflict adding files to %(path)s.  %(action)s.'
657
775
 
658
 
    def take_this(self, tree):
 
776
    def action_take_this(self, tree):
659
777
        tree.remove([self.path], force=True, keep_files=False)
660
778
 
661
 
    def take_other(self, tree):
 
779
    def action_take_other(self, tree):
662
780
        # just acccept bzr proposal
663
781
        pass
664
782
 
677
795
    # FIXME: It's a bit strange that the default action is not coherent with
678
796
    # MissingParent from the *user* pov.
679
797
 
680
 
    def take_this(self, tree):
 
798
    def action_take_this(self, tree):
681
799
        # just acccept bzr proposal
682
800
        pass
683
801
 
684
 
    def take_other(self, tree):
 
802
    def action_take_other(self, tree):
685
803
        tree.remove([self.path], force=True, keep_files=False)
686
804
 
687
805
 
697
815
 
698
816
    # FIXME: .OTHER should be used instead of .new when the conflict is created
699
817
 
700
 
    def take_this(self, tree):
 
818
    def action_take_this(self, tree):
701
819
        # FIXME: we should preserve that path when the conflict is generated !
702
820
        if self.path.endswith('.new'):
703
821
            conflict_path = self.path[:-(len('.new'))]
704
822
            tree.remove([self.path], force=True, keep_files=False)
705
823
            tree.add(conflict_path)
706
824
        else:
707
 
            raise NotImplementedError(self.take_this)
 
825
            raise NotImplementedError(self.action_take_this)
708
826
 
709
 
    def take_other(self, tree):
 
827
    def action_take_other(self, tree):
710
828
        # FIXME: we should preserve that path when the conflict is generated !
711
829
        if self.path.endswith('.new'):
712
830
            conflict_path = self.path[:-(len('.new'))]
713
831
            tree.remove([conflict_path], force=True, keep_files=False)
714
832
            tree.rename_one(self.path, conflict_path)
715
833
        else:
716
 
            raise NotImplementedError(self.take_other)
 
834
            raise NotImplementedError(self.action_take_other)
717
835
 
718
836
 
719
837
ctype = {}