/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 breezy/conflicts.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-18 01:57:45 UTC
  • mto: This revision was merged to the branch mainline in revision 7493.
  • Revision ID: jelmer@jelmer.uk-20200218015745-q2ss9tsk74h4nh61
drop use of future.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2009, 2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 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: 'bzr resolve' should accept a directory name and work from that
 
17
# TODO: 'brz resolve' should accept a directory name and work from that
18
18
# point down
19
19
 
20
20
import os
 
21
import re
21
22
 
22
 
from bzrlib.lazy_import import lazy_import
 
23
from .lazy_import import lazy_import
23
24
lazy_import(globals(), """
24
25
import errno
25
26
 
26
 
from bzrlib import (
27
 
    builtins,
28
 
    cleanup,
29
 
    commands,
30
 
    errors,
 
27
from breezy import (
31
28
    osutils,
32
29
    rio,
33
30
    trace,
34
31
    transform,
35
32
    workingtree,
36
33
    )
 
34
from breezy.i18n import gettext, ngettext
37
35
""")
38
 
from bzrlib import (
 
36
from . import (
 
37
    cache_utf8,
 
38
    errors,
 
39
    commands,
39
40
    option,
40
41
    registry,
41
42
    )
50
51
    Merge will do its best to combine the changes in two branches, but there
51
52
    are some kinds of problems only a human can fix.  When it encounters those,
52
53
    it will mark a conflict.  A conflict means that you need to fix something,
53
 
    before you should commit.
 
54
    before you can commit.
54
55
 
55
56
    Conflicts normally are listed as short, human-readable messages.  If --text
56
57
    is supplied, the pathnames of files with text conflicts are listed,
57
58
    instead.  (This is useful for editing all files with text conflicts.)
58
59
 
59
 
    Use bzr resolve when you have fixed a problem.
 
60
    Use brz resolve when you have fixed a problem.
60
61
    """
61
62
    takes_options = [
62
 
            option.Option('text',
63
 
                          help='List paths of files with text conflicts.'),
 
63
        'directory',
 
64
        option.Option('text',
 
65
                      help='List paths of files with text conflicts.'),
64
66
        ]
65
67
    _see_also = ['resolve', 'conflict-types']
66
68
 
67
 
    def run(self, text=False):
68
 
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
 
69
    def run(self, text=False, directory=u'.'):
 
70
        wt = workingtree.WorkingTree.open_containing(directory)[0]
69
71
        for conflict in wt.conflicts():
70
72
            if text:
71
73
                if conflict.typestring != 'text conflict':
79
81
 
80
82
 
81
83
resolve_action_registry.register(
82
 
    'done', 'done', 'Marks the conflict as resolved' )
 
84
    'auto', 'auto', 'Detect whether conflict has been resolved by user.')
 
85
resolve_action_registry.register(
 
86
    'done', 'done', 'Marks the conflict as resolved.')
83
87
resolve_action_registry.register(
84
88
    'take-this', 'take_this',
85
 
    'Resolve the conflict preserving the version in the working tree' )
 
89
    'Resolve the conflict preserving the version in the working tree.')
86
90
resolve_action_registry.register(
87
91
    'take-other', 'take_other',
88
 
    'Resolve the conflict taking the merged version into account' )
 
92
    'Resolve the conflict taking the merged version into account.')
89
93
resolve_action_registry.default_key = 'done'
90
94
 
 
95
 
91
96
class ResolveActionOption(option.RegistryOption):
92
97
 
93
98
    def __init__(self):
103
108
    Merge will do its best to combine the changes in two branches, but there
104
109
    are some kinds of problems only a human can fix.  When it encounters those,
105
110
    it will mark a conflict.  A conflict means that you need to fix something,
106
 
    before you should commit.
 
111
    before you can commit.
107
112
 
108
 
    Once you have fixed a problem, use "bzr resolve" to automatically mark
109
 
    text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
110
 
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
 
113
    Once you have fixed a problem, use "brz resolve" to automatically mark
 
114
    text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
 
115
    resolved, or "brz resolve --all" to mark all conflicts as resolved.
111
116
    """
112
117
    aliases = ['resolved']
113
118
    takes_args = ['file*']
114
119
    takes_options = [
115
 
            option.Option('all', help='Resolve all conflicts in this tree.'),
116
 
            ResolveActionOption(),
117
 
            ]
 
120
        'directory',
 
121
        option.Option('all', help='Resolve all conflicts in this tree.'),
 
122
        ResolveActionOption(),
 
123
        ]
118
124
    _see_also = ['conflicts']
119
 
    def run(self, file_list=None, all=False, action=None):
 
125
 
 
126
    def run(self, file_list=None, all=False, action=None, directory=None):
120
127
        if all:
121
128
            if file_list:
122
 
                raise errors.BzrCommandError("If --all is specified,"
123
 
                                             " no FILE may be provided")
124
 
            tree = workingtree.WorkingTree.open_containing('.')[0]
 
129
                raise errors.BzrCommandError(gettext("If --all is specified,"
 
130
                                                     " no FILE may be provided"))
 
131
            if directory is None:
 
132
                directory = u'.'
 
133
            tree = workingtree.WorkingTree.open_containing(directory)[0]
125
134
            if action is None:
126
135
                action = 'done'
127
136
        else:
128
 
            tree, file_list = builtins.tree_files(file_list)
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
 
137
            tree, file_list = workingtree.WorkingTree.open_containing_paths(
 
138
                file_list, directory)
 
139
            if action is None:
 
140
                if file_list is None:
136
141
                    action = 'auto'
137
 
            else:
138
 
                if action is None:
 
142
                else:
139
143
                    action = 'done'
140
 
        if action == 'auto':
141
 
            if file_list is None:
142
 
                un_resolved, resolved = tree.auto_resolve()
143
 
                if len(un_resolved) > 0:
144
 
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
145
 
                    trace.note('Remaining conflicts:')
146
 
                    for conflict in un_resolved:
147
 
                        trace.note(conflict)
148
 
                    return 1
149
 
                else:
150
 
                    trace.note('All conflicts resolved.')
151
 
                    return 0
 
144
        before, after = resolve(tree, file_list, action=action)
 
145
        # GZ 2012-07-27: Should unify UI below now that auto is less magical.
 
146
        if action == 'auto' and file_list is None:
 
147
            if after > 0:
 
148
                trace.note(
 
149
                    ngettext('%d conflict auto-resolved.',
 
150
                             '%d conflicts auto-resolved.', before - after),
 
151
                    before - after)
 
152
                trace.note(gettext('Remaining conflicts:'))
 
153
                for conflict in tree.conflicts():
 
154
                    trace.note(str(conflict))
 
155
                return 1
152
156
            else:
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
                trace.note(gettext('All conflicts resolved.'))
 
158
                return 0
157
159
        else:
158
 
            resolve(tree, file_list, action=action)
 
160
            trace.note(ngettext('{0} conflict resolved, {1} remaining',
 
161
                                '{0} conflicts resolved, {1} remaining',
 
162
                                before - after).format(before - after, after))
159
163
 
160
164
 
161
165
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
173
177
        paths do not have conflicts.
174
178
    :param action: How the conflict should be resolved,
175
179
    """
176
 
    tree.lock_tree_write()
177
 
    try:
 
180
    nb_conflicts_after = None
 
181
    with tree.lock_tree_write():
178
182
        tree_conflicts = tree.conflicts()
 
183
        nb_conflicts_before = len(tree_conflicts)
179
184
        if paths is None:
180
185
            new_conflicts = ConflictList()
181
186
            to_process = tree_conflicts
189
194
            except NotImplementedError:
190
195
                new_conflicts.append(conflict)
191
196
        try:
 
197
            nb_conflicts_after = len(new_conflicts)
192
198
            tree.set_conflicts(new_conflicts)
193
199
        except errors.UnsupportedOperation:
194
200
            pass
195
 
    finally:
196
 
        tree.unlock()
 
201
    if nb_conflicts_after is None:
 
202
        nb_conflicts_after = nb_conflicts_before
 
203
    return nb_conflicts_before, nb_conflicts_after
197
204
 
198
205
 
199
206
def restore(filename):
205
212
    try:
206
213
        osutils.rename(filename + ".THIS", filename)
207
214
        conflicted = True
208
 
    except OSError, e:
 
215
    except OSError as e:
209
216
        if e.errno != errno.ENOENT:
210
217
            raise
211
218
    try:
212
219
        os.unlink(filename + ".BASE")
213
220
        conflicted = True
214
 
    except OSError, e:
 
221
    except OSError as e:
215
222
        if e.errno != errno.ENOENT:
216
223
            raise
217
224
    try:
218
225
        os.unlink(filename + ".OTHER")
219
226
        conflicted = True
220
 
    except OSError, e:
 
227
    except OSError as e:
221
228
        if e.errno != errno.ENOENT:
222
229
            raise
223
230
    if not conflicted:
336
343
        if ignore_misses is not True:
337
344
            for path in [p for p in paths if p not in selected_paths]:
338
345
                if not os.path.exists(tree.abspath(path)):
339
 
                    print "%s does not exist" % path
 
346
                    print("%s does not exist" % path)
340
347
                else:
341
 
                    print "%s is not conflicted" % path
 
348
                    print("%s is not conflicted" % path)
342
349
        return new_conflicts, selected_conflicts
343
350
 
344
351
 
350
357
 
351
358
    def __init__(self, path, file_id=None):
352
359
        self.path = path
353
 
        # warn turned off, because the factory blindly transfers the Stanza
354
 
        # values to __init__ and Stanza is purely a Unicode api.
355
 
        self.file_id = osutils.safe_file_id(file_id, warn=False)
 
360
        # the factory blindly transfers the Stanza values to __init__ and
 
361
        # Stanza is purely a Unicode api.
 
362
        if isinstance(file_id, str):
 
363
            file_id = cache_utf8.encode(file_id)
 
364
        self.file_id = osutils.safe_file_id(file_id)
356
365
 
357
366
    def as_stanza(self):
358
367
        s = rio.Stanza(type=self.typestring, path=self.path)
367
376
    def __cmp__(self, other):
368
377
        if getattr(other, "_cmp_list", None) is None:
369
378
            return -1
370
 
        return cmp(self._cmp_list(), other._cmp_list())
 
379
        x = self._cmp_list()
 
380
        y = other._cmp_list()
 
381
        return (x > y) - (x < y)
371
382
 
372
383
    def __hash__(self):
373
384
        return hash((type(self), self.path, self.file_id))
378
389
    def __ne__(self, other):
379
390
        return not self.__eq__(other)
380
391
 
 
392
    def __unicode__(self):
 
393
        return self.describe()
 
394
 
381
395
    def __str__(self):
 
396
        return self.describe()
 
397
 
 
398
    def describe(self):
382
399
        return self.format % self.__dict__
383
400
 
384
401
    def __repr__(self):
420
437
        for fname in self.associated_filenames():
421
438
            try:
422
439
                osutils.delete_any(tree.abspath(fname))
423
 
            except OSError, e:
 
440
            except OSError as e:
424
441
                if e.errno != errno.ENOENT:
425
442
                    raise
426
443
 
 
444
    def action_auto(self, tree):
 
445
        raise NotImplementedError(self.action_auto)
 
446
 
427
447
    def action_done(self, tree):
428
448
        """Mark the conflict as solved once it has been handled."""
429
449
        # This method does nothing but simplifies the design of upper levels.
436
456
        raise NotImplementedError(self.action_take_other)
437
457
 
438
458
    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)
 
459
        with tree.get_transform() as tt:
 
460
            self._resolve(tt, *args, **kwargs)
443
461
 
444
462
 
445
463
class PathConflict(Conflict):
476
494
        path_to_create = None
477
495
        if winner == 'this':
478
496
            if self.path == '<deleted>':
479
 
                return # Nothing to do
 
497
                return  # Nothing to do
480
498
            if self.conflict_path == '<deleted>':
481
499
                path_to_create = self.path
482
500
                revid = tt._tree.get_parent_ids()[0]
494
512
            raise AssertionError('bad winner: %r' % (winner,))
495
513
        if path_to_create is not None:
496
514
            tid = tt.trans_id_tree_path(path_to_create)
 
515
            tree = self._revision_tree(tt._tree, revid)
497
516
            transform.create_from_tree(
498
 
                tt, tt.trans_id_tree_path(path_to_create),
499
 
                self._revision_tree(tt._tree, revid), file_id)
 
517
                tt, tid, tree, tree.id2path(file_id))
500
518
            tt.version_file(file_id, tid)
501
 
 
 
519
        else:
 
520
            tid = tt.trans_id_file_id(file_id)
502
521
        # Adjust the path for the retained file id
503
 
        tid = tt.trans_id_file_id(file_id)
504
522
        parent_tid = tt.get_tree_parent(tid)
505
 
        tt.adjust_path(path, parent_tid, tid)
 
523
        tt.adjust_path(osutils.basename(path), parent_tid, tid)
506
524
        tt.apply()
507
525
 
508
526
    def _revision_tree(self, tree, revid):
515
533
        possible_paths = []
516
534
        for p in (self.path, self.conflict_path):
517
535
            if p == '<deleted>':
518
 
                # special hard-coded path 
 
536
                # special hard-coded path
519
537
                continue
520
538
            if p is not None:
521
539
                possible_paths.append(p)
571
589
        :param tt: The TreeTransform where the conflict is resolved.
572
590
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
573
591
 
574
 
        The resolution is symmetric, when taking THIS, OTHER is deleted and
 
592
        The resolution is symmetric: when taking THIS, OTHER is deleted and
575
593
        item.THIS is renamed into item and vice-versa.
576
594
        """
577
595
        try:
584
602
            # never existed or was already deleted (including the case
585
603
            # where the user deleted it)
586
604
            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()
 
605
        try:
 
606
            this_path = tt._tree.id2path(self.file_id)
 
607
        except errors.NoSuchId:
 
608
            # The file is not present anymore. This may happen if the user
 
609
            # deleted the file either manually or when resolving a conflict on
 
610
            # the parent.  We may raise some exception to indicate that the
 
611
            # conflict doesn't exist anymore and as such doesn't need to be
 
612
            # resolved ? -- vila 20110615
 
613
            this_tid = None
 
614
        else:
 
615
            this_tid = tt.trans_id_tree_path(this_path)
 
616
        if this_tid is not None:
 
617
            # Rename 'item.suffix_to_remove' (note that if
 
618
            # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
619
            parent_tid = tt.get_tree_parent(this_tid)
 
620
            tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
 
621
            tt.apply()
593
622
 
594
623
    def action_take_this(self, tree):
595
624
        self._resolve_with_cleanups(tree, 'OTHER')
598
627
        self._resolve_with_cleanups(tree, 'THIS')
599
628
 
600
629
 
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
630
# TODO: There should be a base revid attribute to better inform the user about
605
631
# how the conflicts were generated.
606
 
class TextConflict(PathConflict):
 
632
class TextConflict(Conflict):
607
633
    """The merge algorithm could not resolve all differences encountered."""
608
634
 
609
635
    has_files = True
612
638
 
613
639
    format = 'Text conflict in %(path)s'
614
640
 
 
641
    rformat = '%(class)s(%(path)r, %(file_id)r)'
 
642
 
 
643
    _conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
 
644
 
615
645
    def associated_filenames(self):
616
646
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
617
647
 
 
648
    def _resolve(self, tt, winner_suffix):
 
649
        """Resolve the conflict by copying one of .THIS or .OTHER into file.
 
650
 
 
651
        :param tt: The TreeTransform where the conflict is resolved.
 
652
        :param winner_suffix: Either 'THIS' or 'OTHER'
 
653
 
 
654
        The resolution is symmetric, when taking THIS, item.THIS is renamed
 
655
        into item and vice-versa. This takes one of the files as a whole
 
656
        ignoring every difference that could have been merged cleanly.
 
657
        """
 
658
        # To avoid useless copies, we switch item and item.winner_suffix, only
 
659
        # item will exist after the conflict has been resolved anyway.
 
660
        item_tid = tt.trans_id_file_id(self.file_id)
 
661
        item_parent_tid = tt.get_tree_parent(item_tid)
 
662
        winner_path = self.path + '.' + winner_suffix
 
663
        winner_tid = tt.trans_id_tree_path(winner_path)
 
664
        winner_parent_tid = tt.get_tree_parent(winner_tid)
 
665
        # Switch the paths to preserve the content
 
666
        tt.adjust_path(osutils.basename(self.path),
 
667
                       winner_parent_tid, winner_tid)
 
668
        tt.adjust_path(osutils.basename(winner_path),
 
669
                       item_parent_tid, item_tid)
 
670
        # Associate the file_id to the right content
 
671
        tt.unversion_file(item_tid)
 
672
        tt.version_file(self.file_id, winner_tid)
 
673
        tt.apply()
 
674
 
 
675
    def action_auto(self, tree):
 
676
        # GZ 2012-07-27: Using NotImplementedError to signal that a conflict
 
677
        #                can't be auto resolved does not seem ideal.
 
678
        try:
 
679
            kind = tree.kind(self.path)
 
680
        except errors.NoSuchFile:
 
681
            return
 
682
        if kind != 'file':
 
683
            raise NotImplementedError("Conflict is not a file")
 
684
        conflict_markers_in_line = self._conflict_re.search
 
685
        # GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
 
686
        with tree.get_file(self.path) as f:
 
687
            for line in f:
 
688
                if conflict_markers_in_line(line):
 
689
                    raise NotImplementedError("Conflict markers present")
 
690
 
 
691
    def action_take_this(self, tree):
 
692
        self._resolve_with_cleanups(tree, 'THIS')
 
693
 
 
694
    def action_take_other(self, tree):
 
695
        self._resolve_with_cleanups(tree, 'OTHER')
 
696
 
618
697
 
619
698
class HandledConflict(Conflict):
620
699
    """A path problem that has been provisionally resolved.
652
731
                 conflict_file_id=None):
653
732
        HandledConflict.__init__(self, action, path, file_id)
654
733
        self.conflict_path = conflict_path
655
 
        # warn turned off, because the factory blindly transfers the Stanza
656
 
        # values to __init__.
657
 
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
658
 
                                                     warn=False)
 
734
        # the factory blindly transfers the Stanza values to __init__,
 
735
        # so they can be unicode.
 
736
        if isinstance(conflict_file_id, str):
 
737
            conflict_file_id = cache_utf8.encode(conflict_file_id)
 
738
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
659
739
 
660
740
    def _cmp_list(self):
661
741
        return HandledConflict._cmp_list(self) + [self.conflict_path,
709
789
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
710
790
 
711
791
    def action_take_this(self, tree):
712
 
        # just acccept bzr proposal
 
792
        # just acccept brz proposal
713
793
        pass
714
794
 
715
795
    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:
 
796
        with tree.get_transform() as tt:
722
797
            p_tid = tt.trans_id_file_id(self.file_id)
723
798
            parent_tid = tt.get_tree_parent(p_tid)
724
799
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
725
800
            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)
 
801
            tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
 
802
            tt.adjust_path(osutils.basename(self.conflict_path),
 
803
                           parent_tid, p_tid)
728
804
            tt.apply()
729
 
        finally:
730
 
            tt.finalize()
731
805
 
732
806
 
733
807
class UnversionedParent(HandledConflict):
766
840
        tree.remove([self.path], force=True, keep_files=False)
767
841
 
768
842
    def action_take_other(self, tree):
769
 
        # just acccept bzr proposal
 
843
        # just acccept brz proposal
770
844
        pass
771
845
 
772
846
 
785
859
    # MissingParent from the *user* pov.
786
860
 
787
861
    def action_take_this(self, tree):
788
 
        # just acccept bzr proposal
 
862
        # just acccept brz proposal
789
863
        pass
790
864
 
791
865
    def action_take_other(self, tree):
832
906
    for conflict_type in conflict_types:
833
907
        ctype[conflict_type.typestring] = conflict_type
834
908
 
 
909
 
835
910
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
836
911
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
837
912
               DeletingParent, NonDirectoryParent)