~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Martin Pool
  • Date: 2010-02-25 06:17:27 UTC
  • mfrom: (5055 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5057.
  • Revision ID: mbp@sourcefrog.net-20100225061727-4sd9lt0qmdc6087t
merge news

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 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
21
22
 
22
23
from bzrlib.lazy_import import lazy_import
23
24
lazy_import(globals(), """
24
25
import errno
25
26
 
26
27
from bzrlib import (
27
 
    cleanup,
 
28
    builtins,
 
29
    commands,
28
30
    errors,
29
31
    osutils,
30
32
    rio,
32
34
    transform,
33
35
    workingtree,
34
36
    )
35
 
from bzrlib.i18n import gettext, ngettext
36
37
""")
37
38
from bzrlib import (
38
 
    commands,
39
39
    option,
40
40
    registry,
41
41
    )
45
45
 
46
46
 
47
47
class cmd_conflicts(commands.Command):
48
 
    __doc__ = """List files with conflicts.
 
48
    """List files with conflicts.
49
49
 
50
50
    Merge will do its best to combine the changes in two branches, but there
51
51
    are some kinds of problems only a human can fix.  When it encounters those,
59
59
    Use bzr resolve when you have fixed a problem.
60
60
    """
61
61
    takes_options = [
62
 
            'directory',
63
62
            option.Option('text',
64
63
                          help='List paths of files with text conflicts.'),
65
64
        ]
66
65
    _see_also = ['resolve', 'conflict-types']
67
66
 
68
 
    def run(self, text=False, directory=u'.'):
69
 
        wt = workingtree.WorkingTree.open_containing(directory)[0]
 
67
    def run(self, text=False):
 
68
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
70
69
        for conflict in wt.conflicts():
71
70
            if text:
72
71
                if conflict.typestring != 'text conflict':
73
72
                    continue
74
73
                self.outf.write(conflict.path + '\n')
75
74
            else:
76
 
                self.outf.write(unicode(conflict) + '\n')
 
75
                self.outf.write(str(conflict) + '\n')
77
76
 
78
77
 
79
78
resolve_action_registry = registry.Registry()
99
98
 
100
99
 
101
100
class cmd_resolve(commands.Command):
102
 
    __doc__ = """Mark a conflict as resolved.
 
101
    """Mark a conflict as resolved.
103
102
 
104
103
    Merge will do its best to combine the changes in two branches, but there
105
104
    are some kinds of problems only a human can fix.  When it encounters those,
113
112
    aliases = ['resolved']
114
113
    takes_args = ['file*']
115
114
    takes_options = [
116
 
            'directory',
117
115
            option.Option('all', help='Resolve all conflicts in this tree.'),
118
116
            ResolveActionOption(),
119
117
            ]
120
118
    _see_also = ['conflicts']
121
 
    def run(self, file_list=None, all=False, action=None, directory=None):
 
119
    def run(self, file_list=None, all=False, action=None):
122
120
        if all:
123
121
            if file_list:
124
 
                raise errors.BzrCommandError(gettext("If --all is specified,"
125
 
                                             " no FILE may be provided"))
126
 
            if directory is None:
127
 
                directory = u'.'
128
 
            tree = workingtree.WorkingTree.open_containing(directory)[0]
 
122
                raise errors.BzrCommandError("If --all is specified,"
 
123
                                             " no FILE may be provided")
 
124
            tree = workingtree.WorkingTree.open_containing('.')[0]
129
125
            if action is None:
130
126
                action = 'done'
131
127
        else:
132
 
            tree, file_list = workingtree.WorkingTree.open_containing_paths(
133
 
                file_list, directory)
 
128
            tree, file_list = builtins.tree_files(file_list)
134
129
            if file_list is None:
135
130
                if action is None:
136
131
                    # FIXME: There is a special case here related to the option
146
141
            if file_list is None:
147
142
                un_resolved, resolved = tree.auto_resolve()
148
143
                if len(un_resolved) > 0:
149
 
                    trace.note(ngettext('%d conflict auto-resolved.',
150
 
                        '%d conflicts auto-resolved.', len(resolved)),
151
 
                        len(resolved))
152
 
                    trace.note(gettext('Remaining conflicts:'))
 
144
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
 
145
                    trace.note('Remaining conflicts:')
153
146
                    for conflict in un_resolved:
154
 
                        trace.note(unicode(conflict))
 
147
                        trace.note(conflict)
155
148
                    return 1
156
149
                else:
157
 
                    trace.note(gettext('All conflicts resolved.'))
 
150
                    trace.note('All conflicts resolved.')
158
151
                    return 0
159
152
            else:
160
153
                # FIXME: This can never occur but the block above needs some
162
155
                # conflict.auto(tree) --vila 091242
163
156
                pass
164
157
        else:
165
 
            before, after = resolve(tree, file_list, action=action)
166
 
            trace.note(ngettext('{0} conflict resolved, {1} remaining',
167
 
                                '{0} conflicts resolved, {1} remaining',
168
 
                                before-after).format(before - after, after))
 
158
            resolve(tree, file_list, action=action)
169
159
 
170
160
 
171
161
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
184
174
    :param action: How the conflict should be resolved,
185
175
    """
186
176
    tree.lock_tree_write()
187
 
    nb_conflicts_after = None
188
177
    try:
189
178
        tree_conflicts = tree.conflicts()
190
 
        nb_conflicts_before = len(tree_conflicts)
191
179
        if paths is None:
192
180
            new_conflicts = ConflictList()
193
181
            to_process = tree_conflicts
201
189
            except NotImplementedError:
202
190
                new_conflicts.append(conflict)
203
191
        try:
204
 
            nb_conflicts_after = len(new_conflicts)
205
192
            tree.set_conflicts(new_conflicts)
206
193
        except errors.UnsupportedOperation:
207
194
            pass
208
195
    finally:
209
196
        tree.unlock()
210
 
    if nb_conflicts_after is None:
211
 
        nb_conflicts_after = nb_conflicts_before
212
 
    return nb_conflicts_before, nb_conflicts_after
213
197
 
214
198
 
215
199
def restore(filename):
295
279
    def to_strings(self):
296
280
        """Generate strings for the provided conflicts"""
297
281
        for conflict in self:
298
 
            yield unicode(conflict)
 
282
            yield str(conflict)
299
283
 
300
284
    def remove_files(self, tree):
301
285
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
394
378
    def __ne__(self, other):
395
379
        return not self.__eq__(other)
396
380
 
397
 
    def __unicode__(self):
 
381
    def __str__(self):
398
382
        return self.format % self.__dict__
399
383
 
400
384
    def __repr__(self):
451
435
    def action_take_other(self, tree):
452
436
        raise NotImplementedError(self.action_take_other)
453
437
 
454
 
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
455
 
        tt = transform.TreeTransform(tree)
456
 
        op = cleanup.OperationWithCleanups(self._resolve)
457
 
        op.add_cleanup(tt.finalize)
458
 
        op.run_simple(tt, *args, **kwargs)
459
 
 
460
438
 
461
439
class PathConflict(Conflict):
462
440
    """A conflict was encountered merging file paths"""
481
459
        # No additional files have been generated here
482
460
        return []
483
461
 
484
 
    def _resolve(self, tt, file_id, path, winner):
485
 
        """Resolve the conflict.
486
 
 
487
 
        :param tt: The TreeTransform where the conflict is resolved.
488
 
        :param file_id: The retained file id.
489
 
        :param path: The retained path.
490
 
        :param winner: 'this' or 'other' indicates which side is the winner.
491
 
        """
492
 
        path_to_create = None
493
 
        if winner == 'this':
494
 
            if self.path == '<deleted>':
495
 
                return # Nothing to do
496
 
            if self.conflict_path == '<deleted>':
497
 
                path_to_create = self.path
498
 
                revid = tt._tree.get_parent_ids()[0]
499
 
        elif winner == 'other':
500
 
            if self.conflict_path == '<deleted>':
501
 
                return  # Nothing to do
502
 
            if self.path == '<deleted>':
503
 
                path_to_create = self.conflict_path
504
 
                # FIXME: If there are more than two parents we may need to
505
 
                # iterate. Taking the last parent is the safer bet in the mean
506
 
                # time. -- vila 20100309
507
 
                revid = tt._tree.get_parent_ids()[-1]
508
 
        else:
509
 
            # Programmer error
510
 
            raise AssertionError('bad winner: %r' % (winner,))
511
 
        if path_to_create is not None:
512
 
            tid = tt.trans_id_tree_path(path_to_create)
513
 
            transform.create_from_tree(
514
 
                tt, tid, self._revision_tree(tt._tree, revid), file_id)
515
 
            tt.version_file(file_id, tid)
516
 
        else:
517
 
            tid = tt.trans_id_file_id(file_id)
518
 
        # Adjust the path for the retained file id
519
 
        parent_tid = tt.get_tree_parent(tid)
520
 
        tt.adjust_path(osutils.basename(path), parent_tid, tid)
521
 
        tt.apply()
522
 
 
523
 
    def _revision_tree(self, tree, revid):
524
 
        return tree.branch.repository.revision_tree(revid)
525
 
 
526
 
    def _infer_file_id(self, tree):
527
 
        # Prior to bug #531967, file_id wasn't always set, there may still be
528
 
        # conflict files in the wild so we need to cope with them
529
 
        # Establish which path we should use to find back the file-id
530
 
        possible_paths = []
531
 
        for p in (self.path, self.conflict_path):
532
 
            if p == '<deleted>':
533
 
                # special hard-coded path 
534
 
                continue
535
 
            if p is not None:
536
 
                possible_paths.append(p)
537
 
        # Search the file-id in the parents with any path available
538
 
        file_id = None
539
 
        for revid in tree.get_parent_ids():
540
 
            revtree = self._revision_tree(tree, revid)
541
 
            for p in possible_paths:
542
 
                file_id = revtree.path2id(p)
543
 
                if file_id is not None:
544
 
                    return revtree, file_id
545
 
        return None, None
546
 
 
547
462
    def action_take_this(self, tree):
548
 
        if self.file_id is not None:
549
 
            self._resolve_with_cleanups(tree, self.file_id, self.path,
550
 
                                        winner='this')
551
 
        else:
552
 
            # Prior to bug #531967 we need to find back the file_id and restore
553
 
            # the content from there
554
 
            revtree, file_id = self._infer_file_id(tree)
555
 
            tree.revert([revtree.id2path(file_id)],
556
 
                        old_tree=revtree, backups=False)
 
463
        tree.rename_one(self.conflict_path, self.path)
557
464
 
558
465
    def action_take_other(self, tree):
559
 
        if self.file_id is not None:
560
 
            self._resolve_with_cleanups(tree, self.file_id,
561
 
                                        self.conflict_path,
562
 
                                        winner='other')
563
 
        else:
564
 
            # Prior to bug #531967 we need to find back the file_id and restore
565
 
            # the content from there
566
 
            revtree, file_id = self._infer_file_id(tree)
567
 
            tree.revert([revtree.id2path(file_id)],
568
 
                        old_tree=revtree, backups=False)
 
466
        # just acccept bzr proposal
 
467
        pass
569
468
 
570
469
 
571
470
class ContentsConflict(PathConflict):
572
 
    """The files are of different types (or both binary), or not present"""
 
471
    """The files are of different types, or not present"""
573
472
 
574
473
    has_files = True
575
474
 
580
479
    def associated_filenames(self):
581
480
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
582
481
 
583
 
    def _resolve(self, tt, suffix_to_remove):
584
 
        """Resolve the conflict.
585
 
 
586
 
        :param tt: The TreeTransform where the conflict is resolved.
587
 
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
588
 
 
589
 
        The resolution is symmetric: when taking THIS, OTHER is deleted and
590
 
        item.THIS is renamed into item and vice-versa.
591
 
        """
592
 
        try:
593
 
            # Delete 'item.THIS' or 'item.OTHER' depending on
594
 
            # suffix_to_remove
595
 
            tt.delete_contents(
596
 
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
597
 
        except errors.NoSuchFile:
598
 
            # There are valid cases where 'item.suffix_to_remove' either
599
 
            # never existed or was already deleted (including the case
600
 
            # where the user deleted it)
601
 
            pass
602
 
        try:
603
 
            this_path = tt._tree.id2path(self.file_id)
604
 
        except errors.NoSuchId:
605
 
            # The file is not present anymore. This may happen if the user
606
 
            # deleted the file either manually or when resolving a conflict on
607
 
            # the parent.  We may raise some exception to indicate that the
608
 
            # conflict doesn't exist anymore and as such doesn't need to be
609
 
            # resolved ? -- vila 20110615 
610
 
            this_tid = None
611
 
        else:
612
 
            this_tid = tt.trans_id_tree_path(this_path)
613
 
        if this_tid is not None:
614
 
            # Rename 'item.suffix_to_remove' (note that if
615
 
            # 'item.suffix_to_remove' has been deleted, this is a no-op)
616
 
            parent_tid = tt.get_tree_parent(this_tid)
617
 
            tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
618
 
            tt.apply()
619
 
 
 
482
    # FIXME: I smell something weird here and it seems we should be able to be
 
483
    # more coherent with some other conflict ? bzr *did* a choice there but
 
484
    # neither action_take_this nor action_take_other reflect that...
 
485
    # -- vila 20091224
620
486
    def action_take_this(self, tree):
621
 
        self._resolve_with_cleanups(tree, 'OTHER')
 
487
        tree.remove([self.path + '.OTHER'], force=True, keep_files=False)
622
488
 
623
489
    def action_take_other(self, tree):
624
 
        self._resolve_with_cleanups(tree, 'THIS')
625
 
 
 
490
        tree.remove([self.path], force=True, keep_files=False)
 
491
 
 
492
 
 
493
 
 
494
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
 
495
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
626
496
 
627
497
# TODO: There should be a base revid attribute to better inform the user about
628
498
# how the conflicts were generated.
629
 
class TextConflict(Conflict):
 
499
class TextConflict(PathConflict):
630
500
    """The merge algorithm could not resolve all differences encountered."""
631
501
 
632
502
    has_files = True
635
505
 
636
506
    format = 'Text conflict in %(path)s'
637
507
 
638
 
    rformat = '%(class)s(%(path)r, %(file_id)r)'
639
 
 
640
508
    def associated_filenames(self):
641
509
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
642
510
 
643
 
    def _resolve(self, tt, winner_suffix):
644
 
        """Resolve the conflict by copying one of .THIS or .OTHER into file.
645
 
 
646
 
        :param tt: The TreeTransform where the conflict is resolved.
647
 
        :param winner_suffix: Either 'THIS' or 'OTHER'
648
 
 
649
 
        The resolution is symmetric, when taking THIS, item.THIS is renamed
650
 
        into item and vice-versa. This takes one of the files as a whole
651
 
        ignoring every difference that could have been merged cleanly.
652
 
        """
653
 
        # To avoid useless copies, we switch item and item.winner_suffix, only
654
 
        # item will exist after the conflict has been resolved anyway.
655
 
        item_tid = tt.trans_id_file_id(self.file_id)
656
 
        item_parent_tid = tt.get_tree_parent(item_tid)
657
 
        winner_path = self.path + '.' + winner_suffix
658
 
        winner_tid = tt.trans_id_tree_path(winner_path)
659
 
        winner_parent_tid = tt.get_tree_parent(winner_tid)
660
 
        # Switch the paths to preserve the content
661
 
        tt.adjust_path(osutils.basename(self.path),
662
 
                       winner_parent_tid, winner_tid)
663
 
        tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
664
 
        # Associate the file_id to the right content
665
 
        tt.unversion_file(item_tid)
666
 
        tt.version_file(self.file_id, winner_tid)
667
 
        tt.apply()
668
 
 
669
 
    def action_take_this(self, tree):
670
 
        self._resolve_with_cleanups(tree, 'THIS')
671
 
 
672
 
    def action_take_other(self, tree):
673
 
        self._resolve_with_cleanups(tree, 'OTHER')
674
 
 
675
511
 
676
512
class HandledConflict(Conflict):
677
513
    """A path problem that has been provisionally resolved.
763
599
 
764
600
    typestring = 'parent loop'
765
601
 
766
 
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
602
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
767
603
 
768
604
    def action_take_this(self, tree):
769
605
        # just acccept bzr proposal
770
606
        pass
771
607
 
772
608
    def action_take_other(self, tree):
 
609
        # FIXME: We shouldn't have to manipulate so many paths here (and there
 
610
        # is probably a bug or two...)
 
611
        base_path = osutils.basename(self.path)
 
612
        conflict_base_path = osutils.basename(self.conflict_path)
773
613
        tt = transform.TreeTransform(tree)
774
614
        try:
775
615
            p_tid = tt.trans_id_file_id(self.file_id)
776
616
            parent_tid = tt.get_tree_parent(p_tid)
777
617
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
778
618
            cparent_tid = tt.get_tree_parent(cp_tid)
779
 
            tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
780
 
            tt.adjust_path(osutils.basename(self.conflict_path),
781
 
                           parent_tid, p_tid)
 
619
            tt.adjust_path(base_path, cparent_tid, cp_tid)
 
620
            tt.adjust_path(conflict_base_path, parent_tid, p_tid)
782
621
            tt.apply()
783
622
        finally:
784
623
            tt.finalize()