/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-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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