~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,
28
29
    commands,
29
30
    errors,
30
31
    osutils,
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':
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
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):
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, 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(osutils.basename(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
462
    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)
 
463
        tree.rename_one(self.conflict_path, self.path)
554
464
 
555
465
    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)
 
466
        # just acccept bzr proposal
 
467
        pass
566
468
 
567
469
 
568
470
class ContentsConflict(PathConflict):
569
 
    """The files are of different types (or both binary), or not present"""
 
471
    """The files are of different types, or not present"""
570
472
 
571
473
    has_files = True
572
474
 
577
479
    def associated_filenames(self):
578
480
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
579
481
 
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(osutils.basename(self.path), parent_tid, this_tid)
604
 
        tt.apply()
605
 
 
 
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
606
486
    def action_take_this(self, tree):
607
 
        self._resolve_with_cleanups(tree, 'OTHER')
 
487
        tree.remove([self.path + '.OTHER'], force=True, keep_files=False)
608
488
 
609
489
    def action_take_other(self, tree):
610
 
        self._resolve_with_cleanups(tree, 'THIS')
611
 
 
 
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
612
496
 
613
497
# TODO: There should be a base revid attribute to better inform the user about
614
498
# how the conflicts were generated.
615
 
class TextConflict(Conflict):
 
499
class TextConflict(PathConflict):
616
500
    """The merge algorithm could not resolve all differences encountered."""
617
501
 
618
502
    has_files = True
621
505
 
622
506
    format = 'Text conflict in %(path)s'
623
507
 
624
 
    rformat = '%(class)s(%(path)r, %(file_id)r)'
625
 
 
626
508
    def associated_filenames(self):
627
509
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
628
510
 
629
 
    def _resolve(self, tt, winner_suffix):
630
 
        """Resolve the conflict by copying one of .THIS or .OTHER into file.
631
 
 
632
 
        :param tt: The TreeTransform where the conflict is resolved.
633
 
        :param winner_suffix: Either 'THIS' or 'OTHER'
634
 
 
635
 
        The resolution is symmetric, when taking THIS, item.THIS is renamed
636
 
        into item and vice-versa. This takes one of the files as a whole
637
 
        ignoring every difference that could have been merged cleanly.
638
 
        """
639
 
        # To avoid useless copies, we switch item and item.winner_suffix, only
640
 
        # item will exist after the conflict has been resolved anyway.
641
 
        item_tid = tt.trans_id_file_id(self.file_id)
642
 
        item_parent_tid = tt.get_tree_parent(item_tid)
643
 
        winner_path = self.path + '.' + winner_suffix
644
 
        winner_tid = tt.trans_id_tree_path(winner_path)
645
 
        winner_parent_tid = tt.get_tree_parent(winner_tid)
646
 
        # Switch the paths to preserve the content
647
 
        tt.adjust_path(osutils.basename(self.path),
648
 
                       winner_parent_tid, winner_tid)
649
 
        tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
650
 
        # Associate the file_id to the right content
651
 
        tt.unversion_file(item_tid)
652
 
        tt.version_file(self.file_id, winner_tid)
653
 
        tt.apply()
654
 
 
655
 
    def action_take_this(self, tree):
656
 
        self._resolve_with_cleanups(tree, 'THIS')
657
 
 
658
 
    def action_take_other(self, tree):
659
 
        self._resolve_with_cleanups(tree, 'OTHER')
660
 
 
661
511
 
662
512
class HandledConflict(Conflict):
663
513
    """A path problem that has been provisionally resolved.
749
599
 
750
600
    typestring = 'parent loop'
751
601
 
752
 
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
602
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
753
603
 
754
604
    def action_take_this(self, tree):
755
605
        # just acccept bzr proposal
756
606
        pass
757
607
 
758
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)
759
613
        tt = transform.TreeTransform(tree)
760
614
        try:
761
615
            p_tid = tt.trans_id_file_id(self.file_id)
762
616
            parent_tid = tt.get_tree_parent(p_tid)
763
617
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
764
618
            cparent_tid = tt.get_tree_parent(cp_tid)
765
 
            tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
766
 
            tt.adjust_path(osutils.basename(self.conflict_path),
767
 
                           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)
768
621
            tt.apply()
769
622
        finally:
770
623
            tt.finalize()