~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-10 16:41:09 UTC
  • mto: (5029.2.1 integration2)
  • mto: This revision was merged to the branch mainline in revision 5031.
  • Revision ID: v.ladeuil+lp@free.fr-20100210164109-q5wluu91am3vsf6d
Use a set() for conflicts_related to stay O(1).

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