/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: 2019-06-21 11:21:12 UTC
  • mto: This revision was merged to the branch mainline in revision 7352.
  • Revision ID: jelmer@jelmer.uk-20190621112112-v0uorrdgqib5z1ur
For python 2.x, install sphinx 1.8.5. Newer versions don't support python 2.

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
from __future__ import absolute_import
 
21
 
20
22
import os
 
23
import re
21
24
 
22
 
from bzrlib.lazy_import import lazy_import
 
25
from .lazy_import import lazy_import
23
26
lazy_import(globals(), """
24
27
import errno
25
28
 
26
 
from bzrlib import (
27
 
    builtins,
 
29
from breezy import (
28
30
    cleanup,
29
 
    commands,
30
31
    errors,
31
32
    osutils,
32
33
    rio,
34
35
    transform,
35
36
    workingtree,
36
37
    )
 
38
from breezy.i18n import gettext, ngettext
37
39
""")
38
 
from bzrlib import (
 
40
from . import (
 
41
    cache_utf8,
 
42
    commands,
39
43
    option,
40
44
    registry,
41
45
    )
 
46
from .sixish import text_type
42
47
 
43
48
 
44
49
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
50
55
    Merge will do its best to combine the changes in two branches, but there
51
56
    are some kinds of problems only a human can fix.  When it encounters those,
52
57
    it will mark a conflict.  A conflict means that you need to fix something,
53
 
    before you should commit.
 
58
    before you can commit.
54
59
 
55
60
    Conflicts normally are listed as short, human-readable messages.  If --text
56
61
    is supplied, the pathnames of files with text conflicts are listed,
57
62
    instead.  (This is useful for editing all files with text conflicts.)
58
63
 
59
 
    Use bzr resolve when you have fixed a problem.
 
64
    Use brz resolve when you have fixed a problem.
60
65
    """
61
66
    takes_options = [
62
 
            option.Option('text',
63
 
                          help='List paths of files with text conflicts.'),
 
67
        'directory',
 
68
        option.Option('text',
 
69
                      help='List paths of files with text conflicts.'),
64
70
        ]
65
71
    _see_also = ['resolve', 'conflict-types']
66
72
 
67
 
    def run(self, text=False):
68
 
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
 
73
    def run(self, text=False, directory=u'.'):
 
74
        wt = workingtree.WorkingTree.open_containing(directory)[0]
69
75
        for conflict in wt.conflicts():
70
76
            if text:
71
77
                if conflict.typestring != 'text conflict':
72
78
                    continue
73
79
                self.outf.write(conflict.path + '\n')
74
80
            else:
75
 
                self.outf.write(str(conflict) + '\n')
 
81
                self.outf.write(text_type(conflict) + '\n')
76
82
 
77
83
 
78
84
resolve_action_registry = registry.Registry()
79
85
 
80
86
 
81
87
resolve_action_registry.register(
82
 
    'done', 'done', 'Marks the conflict as resolved' )
 
88
    'auto', 'auto', 'Detect whether conflict has been resolved by user.')
 
89
resolve_action_registry.register(
 
90
    'done', 'done', 'Marks the conflict as resolved.')
83
91
resolve_action_registry.register(
84
92
    'take-this', 'take_this',
85
 
    'Resolve the conflict preserving the version in the working tree' )
 
93
    'Resolve the conflict preserving the version in the working tree.')
86
94
resolve_action_registry.register(
87
95
    'take-other', 'take_other',
88
 
    'Resolve the conflict taking the merged version into account' )
 
96
    'Resolve the conflict taking the merged version into account.')
89
97
resolve_action_registry.default_key = 'done'
90
98
 
 
99
 
91
100
class ResolveActionOption(option.RegistryOption):
92
101
 
93
102
    def __init__(self):
103
112
    Merge will do its best to combine the changes in two branches, but there
104
113
    are some kinds of problems only a human can fix.  When it encounters those,
105
114
    it will mark a conflict.  A conflict means that you need to fix something,
106
 
    before you should commit.
 
115
    before you can commit.
107
116
 
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.
 
117
    Once you have fixed a problem, use "brz resolve" to automatically mark
 
118
    text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
 
119
    resolved, or "brz resolve --all" to mark all conflicts as resolved.
111
120
    """
112
121
    aliases = ['resolved']
113
122
    takes_args = ['file*']
114
123
    takes_options = [
115
 
            option.Option('all', help='Resolve all conflicts in this tree.'),
116
 
            ResolveActionOption(),
117
 
            ]
 
124
        'directory',
 
125
        option.Option('all', help='Resolve all conflicts in this tree.'),
 
126
        ResolveActionOption(),
 
127
        ]
118
128
    _see_also = ['conflicts']
119
 
    def run(self, file_list=None, all=False, action=None):
 
129
 
 
130
    def run(self, file_list=None, all=False, action=None, directory=None):
120
131
        if all:
121
132
            if file_list:
122
 
                raise errors.BzrCommandError("If --all is specified,"
123
 
                                             " no FILE may be provided")
124
 
            tree = workingtree.WorkingTree.open_containing('.')[0]
 
133
                raise errors.BzrCommandError(gettext("If --all is specified,"
 
134
                                                     " no FILE may be provided"))
 
135
            if directory is None:
 
136
                directory = u'.'
 
137
            tree = workingtree.WorkingTree.open_containing(directory)[0]
125
138
            if action is None:
126
139
                action = 'done'
127
140
        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
 
141
            tree, file_list = workingtree.WorkingTree.open_containing_paths(
 
142
                file_list, directory)
 
143
            if action is None:
 
144
                if file_list is None:
136
145
                    action = 'auto'
137
 
            else:
138
 
                if action is None:
 
146
                else:
139
147
                    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
 
148
        before, after = resolve(tree, file_list, action=action)
 
149
        # GZ 2012-07-27: Should unify UI below now that auto is less magical.
 
150
        if action == 'auto' and file_list is None:
 
151
            if after > 0:
 
152
                trace.note(
 
153
                    ngettext('%d conflict auto-resolved.',
 
154
                             '%d conflicts auto-resolved.', before - after),
 
155
                    before - after)
 
156
                trace.note(gettext('Remaining conflicts:'))
 
157
                for conflict in tree.conflicts():
 
158
                    trace.note(text_type(conflict))
 
159
                return 1
152
160
            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
 
161
                trace.note(gettext('All conflicts resolved.'))
 
162
                return 0
157
163
        else:
158
 
            resolve(tree, file_list, action=action)
 
164
            trace.note(ngettext('{0} conflict resolved, {1} remaining',
 
165
                                '{0} conflicts resolved, {1} remaining',
 
166
                                before - after).format(before - after, after))
159
167
 
160
168
 
161
169
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
173
181
        paths do not have conflicts.
174
182
    :param action: How the conflict should be resolved,
175
183
    """
176
 
    tree.lock_tree_write()
177
 
    try:
 
184
    nb_conflicts_after = None
 
185
    with tree.lock_tree_write():
178
186
        tree_conflicts = tree.conflicts()
 
187
        nb_conflicts_before = len(tree_conflicts)
179
188
        if paths is None:
180
189
            new_conflicts = ConflictList()
181
190
            to_process = tree_conflicts
189
198
            except NotImplementedError:
190
199
                new_conflicts.append(conflict)
191
200
        try:
 
201
            nb_conflicts_after = len(new_conflicts)
192
202
            tree.set_conflicts(new_conflicts)
193
203
        except errors.UnsupportedOperation:
194
204
            pass
195
 
    finally:
196
 
        tree.unlock()
 
205
    if nb_conflicts_after is None:
 
206
        nb_conflicts_after = nb_conflicts_before
 
207
    return nb_conflicts_before, nb_conflicts_after
197
208
 
198
209
 
199
210
def restore(filename):
205
216
    try:
206
217
        osutils.rename(filename + ".THIS", filename)
207
218
        conflicted = True
208
 
    except OSError, e:
 
219
    except OSError as e:
209
220
        if e.errno != errno.ENOENT:
210
221
            raise
211
222
    try:
212
223
        os.unlink(filename + ".BASE")
213
224
        conflicted = True
214
 
    except OSError, e:
 
225
    except OSError as e:
215
226
        if e.errno != errno.ENOENT:
216
227
            raise
217
228
    try:
218
229
        os.unlink(filename + ".OTHER")
219
230
        conflicted = True
220
 
    except OSError, e:
 
231
    except OSError as e:
221
232
        if e.errno != errno.ENOENT:
222
233
            raise
223
234
    if not conflicted:
279
290
    def to_strings(self):
280
291
        """Generate strings for the provided conflicts"""
281
292
        for conflict in self:
282
 
            yield str(conflict)
 
293
            yield text_type(conflict)
283
294
 
284
295
    def remove_files(self, tree):
285
296
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
336
347
        if ignore_misses is not True:
337
348
            for path in [p for p in paths if p not in selected_paths]:
338
349
                if not os.path.exists(tree.abspath(path)):
339
 
                    print "%s does not exist" % path
 
350
                    print("%s does not exist" % path)
340
351
                else:
341
 
                    print "%s is not conflicted" % path
 
352
                    print("%s is not conflicted" % path)
342
353
        return new_conflicts, selected_conflicts
343
354
 
344
355
 
350
361
 
351
362
    def __init__(self, path, file_id=None):
352
363
        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)
 
364
        # the factory blindly transfers the Stanza values to __init__ and
 
365
        # Stanza is purely a Unicode api.
 
366
        if isinstance(file_id, text_type):
 
367
            file_id = cache_utf8.encode(file_id)
 
368
        self.file_id = osutils.safe_file_id(file_id)
356
369
 
357
370
    def as_stanza(self):
358
371
        s = rio.Stanza(type=self.typestring, path=self.path)
367
380
    def __cmp__(self, other):
368
381
        if getattr(other, "_cmp_list", None) is None:
369
382
            return -1
370
 
        return cmp(self._cmp_list(), other._cmp_list())
 
383
        x = self._cmp_list()
 
384
        y = other._cmp_list()
 
385
        return (x > y) - (x < y)
371
386
 
372
387
    def __hash__(self):
373
388
        return hash((type(self), self.path, self.file_id))
378
393
    def __ne__(self, other):
379
394
        return not self.__eq__(other)
380
395
 
 
396
    def __unicode__(self):
 
397
        return self.describe()
 
398
 
381
399
    def __str__(self):
 
400
        return self.describe()
 
401
 
 
402
    def describe(self):
382
403
        return self.format % self.__dict__
383
404
 
384
405
    def __repr__(self):
420
441
        for fname in self.associated_filenames():
421
442
            try:
422
443
                osutils.delete_any(tree.abspath(fname))
423
 
            except OSError, e:
 
444
            except OSError as e:
424
445
                if e.errno != errno.ENOENT:
425
446
                    raise
426
447
 
 
448
    def action_auto(self, tree):
 
449
        raise NotImplementedError(self.action_auto)
 
450
 
427
451
    def action_done(self, tree):
428
452
        """Mark the conflict as solved once it has been handled."""
429
453
        # This method does nothing but simplifies the design of upper levels.
476
500
        path_to_create = None
477
501
        if winner == 'this':
478
502
            if self.path == '<deleted>':
479
 
                return # Nothing to do
 
503
                return  # Nothing to do
480
504
            if self.conflict_path == '<deleted>':
481
505
                path_to_create = self.path
482
506
                revid = tt._tree.get_parent_ids()[0]
494
518
            raise AssertionError('bad winner: %r' % (winner,))
495
519
        if path_to_create is not None:
496
520
            tid = tt.trans_id_tree_path(path_to_create)
 
521
            tree = self._revision_tree(tt._tree, revid)
497
522
            transform.create_from_tree(
498
 
                tt, tt.trans_id_tree_path(path_to_create),
499
 
                self._revision_tree(tt._tree, revid), file_id)
 
523
                tt, tid, tree, tree.id2path(file_id), file_id=file_id)
500
524
            tt.version_file(file_id, tid)
501
 
 
 
525
        else:
 
526
            tid = tt.trans_id_file_id(file_id)
502
527
        # Adjust the path for the retained file id
503
 
        tid = tt.trans_id_file_id(file_id)
504
528
        parent_tid = tt.get_tree_parent(tid)
505
 
        tt.adjust_path(path, parent_tid, tid)
 
529
        tt.adjust_path(osutils.basename(path), parent_tid, tid)
506
530
        tt.apply()
507
531
 
508
532
    def _revision_tree(self, tree, revid):
515
539
        possible_paths = []
516
540
        for p in (self.path, self.conflict_path):
517
541
            if p == '<deleted>':
518
 
                # special hard-coded path 
 
542
                # special hard-coded path
519
543
                continue
520
544
            if p is not None:
521
545
                possible_paths.append(p)
571
595
        :param tt: The TreeTransform where the conflict is resolved.
572
596
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
573
597
 
574
 
        The resolution is symmetric, when taking THIS, OTHER is deleted and
 
598
        The resolution is symmetric: when taking THIS, OTHER is deleted and
575
599
        item.THIS is renamed into item and vice-versa.
576
600
        """
577
601
        try:
584
608
            # never existed or was already deleted (including the case
585
609
            # where the user deleted it)
586
610
            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()
 
611
        try:
 
612
            this_path = tt._tree.id2path(self.file_id)
 
613
        except errors.NoSuchId:
 
614
            # The file is not present anymore. This may happen if the user
 
615
            # deleted the file either manually or when resolving a conflict on
 
616
            # the parent.  We may raise some exception to indicate that the
 
617
            # conflict doesn't exist anymore and as such doesn't need to be
 
618
            # resolved ? -- vila 20110615
 
619
            this_tid = None
 
620
        else:
 
621
            this_tid = tt.trans_id_tree_path(this_path)
 
622
        if this_tid is not None:
 
623
            # Rename 'item.suffix_to_remove' (note that if
 
624
            # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
625
            parent_tid = tt.get_tree_parent(this_tid)
 
626
            tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
 
627
            tt.apply()
593
628
 
594
629
    def action_take_this(self, tree):
595
630
        self._resolve_with_cleanups(tree, 'OTHER')
598
633
        self._resolve_with_cleanups(tree, 'THIS')
599
634
 
600
635
 
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
636
# TODO: There should be a base revid attribute to better inform the user about
605
637
# how the conflicts were generated.
606
 
class TextConflict(PathConflict):
 
638
class TextConflict(Conflict):
607
639
    """The merge algorithm could not resolve all differences encountered."""
608
640
 
609
641
    has_files = True
612
644
 
613
645
    format = 'Text conflict in %(path)s'
614
646
 
 
647
    rformat = '%(class)s(%(path)r, %(file_id)r)'
 
648
 
 
649
    _conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
 
650
 
615
651
    def associated_filenames(self):
616
652
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
617
653
 
 
654
    def _resolve(self, tt, winner_suffix):
 
655
        """Resolve the conflict by copying one of .THIS or .OTHER into file.
 
656
 
 
657
        :param tt: The TreeTransform where the conflict is resolved.
 
658
        :param winner_suffix: Either 'THIS' or 'OTHER'
 
659
 
 
660
        The resolution is symmetric, when taking THIS, item.THIS is renamed
 
661
        into item and vice-versa. This takes one of the files as a whole
 
662
        ignoring every difference that could have been merged cleanly.
 
663
        """
 
664
        # To avoid useless copies, we switch item and item.winner_suffix, only
 
665
        # item will exist after the conflict has been resolved anyway.
 
666
        item_tid = tt.trans_id_file_id(self.file_id)
 
667
        item_parent_tid = tt.get_tree_parent(item_tid)
 
668
        winner_path = self.path + '.' + winner_suffix
 
669
        winner_tid = tt.trans_id_tree_path(winner_path)
 
670
        winner_parent_tid = tt.get_tree_parent(winner_tid)
 
671
        # Switch the paths to preserve the content
 
672
        tt.adjust_path(osutils.basename(self.path),
 
673
                       winner_parent_tid, winner_tid)
 
674
        tt.adjust_path(osutils.basename(winner_path),
 
675
                       item_parent_tid, item_tid)
 
676
        # Associate the file_id to the right content
 
677
        tt.unversion_file(item_tid)
 
678
        tt.version_file(self.file_id, winner_tid)
 
679
        tt.apply()
 
680
 
 
681
    def action_auto(self, tree):
 
682
        # GZ 2012-07-27: Using NotImplementedError to signal that a conflict
 
683
        #                can't be auto resolved does not seem ideal.
 
684
        try:
 
685
            kind = tree.kind(self.path)
 
686
        except errors.NoSuchFile:
 
687
            return
 
688
        if kind != 'file':
 
689
            raise NotImplementedError("Conflict is not a file")
 
690
        conflict_markers_in_line = self._conflict_re.search
 
691
        # GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
 
692
        with tree.get_file(self.path) as f:
 
693
            for line in f:
 
694
                if conflict_markers_in_line(line):
 
695
                    raise NotImplementedError("Conflict markers present")
 
696
 
 
697
    def action_take_this(self, tree):
 
698
        self._resolve_with_cleanups(tree, 'THIS')
 
699
 
 
700
    def action_take_other(self, tree):
 
701
        self._resolve_with_cleanups(tree, 'OTHER')
 
702
 
618
703
 
619
704
class HandledConflict(Conflict):
620
705
    """A path problem that has been provisionally resolved.
652
737
                 conflict_file_id=None):
653
738
        HandledConflict.__init__(self, action, path, file_id)
654
739
        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)
 
740
        # the factory blindly transfers the Stanza values to __init__,
 
741
        # so they can be unicode.
 
742
        if isinstance(conflict_file_id, text_type):
 
743
            conflict_file_id = cache_utf8.encode(conflict_file_id)
 
744
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
659
745
 
660
746
    def _cmp_list(self):
661
747
        return HandledConflict._cmp_list(self) + [self.conflict_path,
709
795
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
710
796
 
711
797
    def action_take_this(self, tree):
712
 
        # just acccept bzr proposal
 
798
        # just acccept brz proposal
713
799
        pass
714
800
 
715
801
    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
802
        tt = transform.TreeTransform(tree)
721
803
        try:
722
804
            p_tid = tt.trans_id_file_id(self.file_id)
723
805
            parent_tid = tt.get_tree_parent(p_tid)
724
806
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
725
807
            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)
 
808
            tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
 
809
            tt.adjust_path(osutils.basename(self.conflict_path),
 
810
                           parent_tid, p_tid)
728
811
            tt.apply()
729
812
        finally:
730
813
            tt.finalize()
766
849
        tree.remove([self.path], force=True, keep_files=False)
767
850
 
768
851
    def action_take_other(self, tree):
769
 
        # just acccept bzr proposal
 
852
        # just acccept brz proposal
770
853
        pass
771
854
 
772
855
 
785
868
    # MissingParent from the *user* pov.
786
869
 
787
870
    def action_take_this(self, tree):
788
 
        # just acccept bzr proposal
 
871
        # just acccept brz proposal
789
872
        pass
790
873
 
791
874
    def action_take_other(self, tree):
832
915
    for conflict_type in conflict_types:
833
916
        ctype[conflict_type.typestring] = conflict_type
834
917
 
 
918
 
835
919
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
836
920
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
837
921
               DeletingParent, NonDirectoryParent)