/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2007 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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
# TODO: Move this into builtins
18
 
 
19
17
# TODO: 'bzr resolve' should accept a directory name and work from that
20
18
# point down
21
19
 
27
25
 
28
26
from bzrlib import (
29
27
    builtins,
 
28
    cleanup,
30
29
    commands,
31
30
    errors,
32
31
    osutils,
33
32
    rio,
34
33
    trace,
 
34
    transform,
 
35
    workingtree,
35
36
    )
36
37
""")
37
 
from bzrlib.option import Option
 
38
from bzrlib import (
 
39
    option,
 
40
    registry,
 
41
    )
38
42
 
39
43
 
40
44
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
41
45
 
42
46
 
43
47
class cmd_conflicts(commands.Command):
44
 
    """List files with conflicts.
 
48
    __doc__ = """List files with conflicts.
45
49
 
46
50
    Merge will do its best to combine the changes in two branches, but there
47
51
    are some kinds of problems only a human can fix.  When it encounters those,
55
59
    Use bzr resolve when you have fixed a problem.
56
60
    """
57
61
    takes_options = [
58
 
            Option('text',
59
 
                   help='List paths of files with text conflicts.'),
 
62
            option.Option('text',
 
63
                          help='List paths of files with text conflicts.'),
60
64
        ]
61
65
    _see_also = ['resolve', 'conflict-types']
62
66
 
63
67
    def run(self, text=False):
64
 
        from bzrlib.workingtree import WorkingTree
65
 
        wt = WorkingTree.open_containing(u'.')[0]
 
68
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
66
69
        for conflict in wt.conflicts():
67
70
            if text:
68
71
                if conflict.typestring != 'text conflict':
72
75
                self.outf.write(str(conflict) + '\n')
73
76
 
74
77
 
 
78
resolve_action_registry = registry.Registry()
 
79
 
 
80
 
 
81
resolve_action_registry.register(
 
82
    'done', 'done', 'Marks the conflict as resolved' )
 
83
resolve_action_registry.register(
 
84
    'take-this', 'take_this',
 
85
    'Resolve the conflict preserving the version in the working tree' )
 
86
resolve_action_registry.register(
 
87
    'take-other', 'take_other',
 
88
    'Resolve the conflict taking the merged version into account' )
 
89
resolve_action_registry.default_key = 'done'
 
90
 
 
91
class ResolveActionOption(option.RegistryOption):
 
92
 
 
93
    def __init__(self):
 
94
        super(ResolveActionOption, self).__init__(
 
95
            'action', 'How to resolve the conflict.',
 
96
            value_switches=True,
 
97
            registry=resolve_action_registry)
 
98
 
 
99
 
75
100
class cmd_resolve(commands.Command):
76
 
    """Mark a conflict as resolved.
 
101
    __doc__ = """Mark a conflict as resolved.
77
102
 
78
103
    Merge will do its best to combine the changes in two branches, but there
79
104
    are some kinds of problems only a human can fix.  When it encounters those,
87
112
    aliases = ['resolved']
88
113
    takes_args = ['file*']
89
114
    takes_options = [
90
 
            Option('all', help='Resolve all conflicts in this tree.'),
 
115
            option.Option('all', help='Resolve all conflicts in this tree.'),
 
116
            ResolveActionOption(),
91
117
            ]
92
118
    _see_also = ['conflicts']
93
 
    def run(self, file_list=None, all=False):
94
 
        from bzrlib.workingtree import WorkingTree
 
119
    def run(self, file_list=None, all=False, action=None):
95
120
        if all:
96
121
            if file_list:
97
122
                raise errors.BzrCommandError("If --all is specified,"
98
123
                                             " no FILE may be provided")
99
 
            tree = WorkingTree.open_containing('.')[0]
100
 
            resolve(tree)
 
124
            tree = workingtree.WorkingTree.open_containing('.')[0]
 
125
            if action is None:
 
126
                action = 'done'
101
127
        else:
102
128
            tree, file_list = builtins.tree_files(file_list)
103
129
            if file_list is None:
 
130
                if action is None:
 
131
                    # FIXME: There is a special case here related to the option
 
132
                    # handling that could be clearer and easier to discover by
 
133
                    # providing an --auto action (bug #344013 and #383396) and
 
134
                    # make it mandatory instead of implicit and active only
 
135
                    # when no file_list is provided -- vila 091229
 
136
                    action = 'auto'
 
137
            else:
 
138
                if action is None:
 
139
                    action = 'done'
 
140
        if action == 'auto':
 
141
            if file_list is None:
104
142
                un_resolved, resolved = tree.auto_resolve()
105
143
                if len(un_resolved) > 0:
106
144
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
112
150
                    trace.note('All conflicts resolved.')
113
151
                    return 0
114
152
            else:
115
 
                resolve(tree, file_list)
116
 
 
117
 
 
118
 
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
 
153
                # FIXME: This can never occur but the block above needs some
 
154
                # refactoring to transfer tree.auto_resolve() to
 
155
                # conflict.auto(tree) --vila 091242
 
156
                pass
 
157
        else:
 
158
            resolve(tree, file_list, action=action)
 
159
 
 
160
 
 
161
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
 
162
            action='done'):
119
163
    """Resolve some or all of the conflicts in a working tree.
120
164
 
121
165
    :param paths: If None, resolve all conflicts.  Otherwise, select only
125
169
        recursive commands like revert, this should be True.  For commands
126
170
        or applications wishing finer-grained control, like the resolve
127
171
        command, this should be False.
128
 
    :ignore_misses: If False, warnings will be printed if the supplied paths
129
 
        do not have conflicts.
 
172
    :param ignore_misses: If False, warnings will be printed if the supplied
 
173
        paths do not have conflicts.
 
174
    :param action: How the conflict should be resolved,
130
175
    """
131
176
    tree.lock_tree_write()
132
177
    try:
133
178
        tree_conflicts = tree.conflicts()
134
179
        if paths is None:
135
180
            new_conflicts = ConflictList()
136
 
            selected_conflicts = tree_conflicts
 
181
            to_process = tree_conflicts
137
182
        else:
138
 
            new_conflicts, selected_conflicts = \
139
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses,
140
 
                    recursive)
 
183
            new_conflicts, to_process = tree_conflicts.select_conflicts(
 
184
                tree, paths, ignore_misses, recursive)
 
185
        for conflict in to_process:
 
186
            try:
 
187
                conflict._do(action, tree)
 
188
                conflict.cleanup(tree)
 
189
            except NotImplementedError:
 
190
                new_conflicts.append(conflict)
141
191
        try:
142
192
            tree.set_conflicts(new_conflicts)
143
193
        except errors.UnsupportedOperation:
144
194
            pass
145
 
        selected_conflicts.remove_files(tree)
146
195
    finally:
147
196
        tree.unlock()
148
197
 
237
286
        for conflict in self:
238
287
            if not conflict.has_files:
239
288
                continue
240
 
            for suffix in CONFLICT_SUFFIXES:
241
 
                try:
242
 
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
243
 
                except OSError, e:
244
 
                    if e.errno != errno.ENOENT:
245
 
                        raise
 
289
            conflict.cleanup(tree)
246
290
 
247
291
    def select_conflicts(self, tree, paths, ignore_misses=False,
248
292
                         recurse=False):
301
345
class Conflict(object):
302
346
    """Base class for all types of conflict"""
303
347
 
 
348
    # FIXME: cleanup should take care of that ? -- vila 091229
304
349
    has_files = False
305
350
 
306
351
    def __init__(self, path, file_id=None):
355
400
        else:
356
401
            return None, conflict.typestring
357
402
 
 
403
    def _do(self, action, tree):
 
404
        """Apply the specified action to the conflict.
 
405
 
 
406
        :param action: The method name to call.
 
407
 
 
408
        :param tree: The tree passed as a parameter to the method.
 
409
        """
 
410
        meth = getattr(self, 'action_%s' % action, None)
 
411
        if meth is None:
 
412
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
 
413
        meth(tree)
 
414
 
 
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
    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
 
426
 
 
427
    def action_done(self, tree):
 
428
        """Mark the conflict as solved once it has been handled."""
 
429
        # This method does nothing but simplifies the design of upper levels.
 
430
        pass
 
431
 
 
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)
 
443
 
358
444
 
359
445
class PathConflict(Conflict):
360
446
    """A conflict was encountered merging file paths"""
364
450
    format = 'Path conflict: %(path)s / %(conflict_path)s'
365
451
 
366
452
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
453
 
367
454
    def __init__(self, path, conflict_path=None, file_id=None):
368
455
        Conflict.__init__(self, path, file_id)
369
456
        self.conflict_path = conflict_path
374
461
            s.add('conflict_path', self.conflict_path)
375
462
        return s
376
463
 
 
464
    def associated_filenames(self):
 
465
        # 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)
 
554
 
377
555
 
378
556
class ContentsConflict(PathConflict):
379
 
    """The files are of different types, or not present"""
 
557
    """The files are of different types (or both binary), or not present"""
380
558
 
381
559
    has_files = True
382
560
 
384
562
 
385
563
    format = 'Contents conflict in %(path)s'
386
564
 
387
 
 
 
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')
 
599
 
 
600
 
 
601
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
 
602
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
 
603
 
 
604
# TODO: There should be a base revid attribute to better inform the user about
 
605
# how the conflicts were generated.
388
606
class TextConflict(PathConflict):
389
607
    """The merge algorithm could not resolve all differences encountered."""
390
608
 
394
612
 
395
613
    format = 'Text conflict in %(path)s'
396
614
 
 
615
    def associated_filenames(self):
 
616
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
617
 
397
618
 
398
619
class HandledConflict(Conflict):
399
620
    """A path problem that has been provisionally resolved.
414
635
        s.add('action', self.action)
415
636
        return s
416
637
 
 
638
    def associated_filenames(self):
 
639
        # Nothing has been generated here
 
640
        return []
 
641
 
417
642
 
418
643
class HandledPathConflict(HandledConflict):
419
644
    """A provisionally-resolved path problem involving two paths.
460
685
 
461
686
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
462
687
 
 
688
    def action_take_this(self, tree):
 
689
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
690
        tree.rename_one(self.path, self.conflict_path)
 
691
 
 
692
    def action_take_other(self, tree):
 
693
        tree.remove([self.path], force=True, keep_files=False)
 
694
 
463
695
 
464
696
class ParentLoop(HandledPathConflict):
465
697
    """An attempt to create an infinitely-looping directory structure.
466
698
    This is rare, but can be produced like so:
467
699
 
468
700
    tree A:
469
 
      mv foo/bar
 
701
      mv foo bar
470
702
    tree B:
471
 
      mv bar/foo
 
703
      mv bar foo
472
704
    merge A and B
473
705
    """
474
706
 
475
707
    typestring = 'parent loop'
476
708
 
477
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
709
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
710
 
 
711
    def action_take_this(self, tree):
 
712
        # just acccept bzr proposal
 
713
        pass
 
714
 
 
715
    def action_take_other(self, tree):
 
716
        # FIXME: We shouldn't have to manipulate so many paths here (and there
 
717
        # is probably a bug or two...)
 
718
        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()
478
731
 
479
732
 
480
733
class UnversionedParent(HandledConflict):
488
741
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
489
742
             ' children.  %(action)s.'
490
743
 
 
744
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
745
    # conflict shouldn't exist (the long story is that the conflict is
 
746
    # generated with another one that can be resolved properly) -- vila 091224
 
747
    def action_take_this(self, tree):
 
748
        pass
 
749
 
 
750
    def action_take_other(self, tree):
 
751
        pass
 
752
 
491
753
 
492
754
class MissingParent(HandledConflict):
493
755
    """An attempt to add files to a directory that is not present.
494
756
    Typically, the result of a merge where THIS deleted the directory and
495
757
    the OTHER added a file to it.
496
 
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
758
    See also: DeletingParent (same situation, THIS and OTHER reversed)
497
759
    """
498
760
 
499
761
    typestring = 'missing parent'
500
762
 
501
763
    format = 'Conflict adding files to %(path)s.  %(action)s.'
502
764
 
 
765
    def action_take_this(self, tree):
 
766
        tree.remove([self.path], force=True, keep_files=False)
 
767
 
 
768
    def action_take_other(self, tree):
 
769
        # just acccept bzr proposal
 
770
        pass
 
771
 
503
772
 
504
773
class DeletingParent(HandledConflict):
505
774
    """An attempt to add files to a directory that is not present.
512
781
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
513
782
             "%(action)s."
514
783
 
 
784
    # FIXME: It's a bit strange that the default action is not coherent with
 
785
    # MissingParent from the *user* pov.
 
786
 
 
787
    def action_take_this(self, tree):
 
788
        # just acccept bzr proposal
 
789
        pass
 
790
 
 
791
    def action_take_other(self, tree):
 
792
        tree.remove([self.path], force=True, keep_files=False)
 
793
 
515
794
 
516
795
class NonDirectoryParent(HandledConflict):
517
 
    """An attempt to add files to a directory that is not a director or
 
796
    """An attempt to add files to a directory that is not a directory or
518
797
    an attempt to change the kind of a directory with files.
519
798
    """
520
799
 
523
802
    format = "Conflict: %(path)s is not a directory, but has files in it."\
524
803
             "  %(action)s."
525
804
 
 
805
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
806
 
 
807
    def action_take_this(self, tree):
 
808
        # FIXME: we should preserve that path when the conflict is generated !
 
809
        if self.path.endswith('.new'):
 
810
            conflict_path = self.path[:-(len('.new'))]
 
811
            tree.remove([self.path], force=True, keep_files=False)
 
812
            tree.add(conflict_path)
 
813
        else:
 
814
            raise NotImplementedError(self.action_take_this)
 
815
 
 
816
    def action_take_other(self, tree):
 
817
        # FIXME: we should preserve that path when the conflict is generated !
 
818
        if self.path.endswith('.new'):
 
819
            conflict_path = self.path[:-(len('.new'))]
 
820
            tree.remove([conflict_path], force=True, keep_files=False)
 
821
            tree.rename_one(self.path, conflict_path)
 
822
        else:
 
823
            raise NotImplementedError(self.action_take_other)
 
824
 
 
825
 
526
826
ctype = {}
527
827
 
528
828
 
532
832
    for conflict_type in conflict_types:
533
833
        ctype[conflict_type.typestring] = conflict_type
534
834
 
535
 
 
536
835
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
537
836
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
538
837
               DeletingParent, NonDirectoryParent)