~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-09 20:33:43 UTC
  • mto: (5029.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5030.
  • Revision ID: v.ladeuil+lp@free.fr-20100209203343-ktxx7t0xvptvjnt1
Move TestingPathFilteringServer to bzrlib.tests.test_server

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2007 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
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: Move this into builtins
18
 
 
19
17
# TODO: 'bzr resolve' should accept a directory name and work from that
20
18
# point down
21
19
 
22
20
import os
 
21
import re
23
22
 
24
23
from bzrlib.lazy_import import lazy_import
25
24
lazy_import(globals(), """
32
31
    osutils,
33
32
    rio,
34
33
    trace,
 
34
    transform,
 
35
    workingtree,
35
36
    )
36
37
""")
37
 
from bzrlib.option import Option
 
38
from bzrlib import (
 
39
    option,
 
40
    registry,
 
41
    )
38
42
 
39
43
 
40
44
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
53
57
    instead.  (This is useful for editing all files with text conflicts.)
54
58
 
55
59
    Use bzr resolve when you have fixed a problem.
56
 
 
57
 
    See also bzr resolve.
58
60
    """
59
61
    takes_options = [
60
 
            Option('text',
61
 
                   help='List paths of files with text conflicts.'),
 
62
            option.Option('text',
 
63
                          help='List paths of files with text conflicts.'),
62
64
        ]
 
65
    _see_also = ['resolve', 'conflict-types']
63
66
 
64
67
    def run(self, text=False):
65
 
        from bzrlib.workingtree import WorkingTree
66
 
        wt = WorkingTree.open_containing(u'.')[0]
 
68
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
67
69
        for conflict in wt.conflicts():
68
70
            if text:
69
71
                if conflict.typestring != 'text conflict':
73
75
                self.outf.write(str(conflict) + '\n')
74
76
 
75
77
 
 
78
resolve_action_registry = registry.Registry()
 
79
 
 
80
 
 
81
resolve_action_registry.register(
 
82
    'done', 'done', 'Marks the conflict as resolved' )
 
83
resolve_action_registry.register(
 
84
    'take-this', 'take_this',
 
85
    'Resolve the conflict preserving the version in the working tree' )
 
86
resolve_action_registry.register(
 
87
    'take-other', 'take_other',
 
88
    'Resolve the conflict taking the merged version into account' )
 
89
resolve_action_registry.default_key = 'done'
 
90
 
 
91
class ResolveActionOption(option.RegistryOption):
 
92
 
 
93
    def __init__(self):
 
94
        super(ResolveActionOption, self).__init__(
 
95
            'action', 'How to resolve the conflict.',
 
96
            value_switches=True,
 
97
            registry=resolve_action_registry)
 
98
 
 
99
 
76
100
class cmd_resolve(commands.Command):
77
101
    """Mark a conflict as resolved.
78
102
 
82
106
    before you should commit.
83
107
 
84
108
    Once you have fixed a problem, use "bzr resolve" to automatically mark
85
 
    text conflicts as fixed, resolve FILE to mark a specific conflict as
 
109
    text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
86
110
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
87
 
 
88
 
    See also bzr conflicts.
89
111
    """
90
112
    aliases = ['resolved']
91
113
    takes_args = ['file*']
92
114
    takes_options = [
93
 
            Option('all', help='Resolve all conflicts in this tree.'),
 
115
            option.Option('all', help='Resolve all conflicts in this tree.'),
 
116
            ResolveActionOption(),
94
117
            ]
95
 
    def run(self, file_list=None, all=False):
96
 
        from bzrlib.workingtree import WorkingTree
 
118
    _see_also = ['conflicts']
 
119
    def run(self, file_list=None, all=False, action=None):
97
120
        if all:
98
121
            if file_list:
99
122
                raise errors.BzrCommandError("If --all is specified,"
100
123
                                             " no FILE may be provided")
101
 
            tree = WorkingTree.open_containing('.')[0]
102
 
            resolve(tree)
 
124
            tree = workingtree.WorkingTree.open_containing('.')[0]
 
125
            if action is None:
 
126
                action = 'done'
103
127
        else:
104
128
            tree, file_list = builtins.tree_files(file_list)
105
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
 
136
                    action = 'auto'
 
137
            else:
 
138
                if action is None:
 
139
                    action = 'done'
 
140
        if action == 'auto':
 
141
            if file_list is None:
106
142
                un_resolved, resolved = tree.auto_resolve()
107
143
                if len(un_resolved) > 0:
108
144
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
114
150
                    trace.note('All conflicts resolved.')
115
151
                    return 0
116
152
            else:
117
 
                resolve(tree, file_list)
118
 
 
119
 
 
120
 
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
 
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
        else:
 
158
            resolve(tree, file_list, action=action)
 
159
 
 
160
 
 
161
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
 
162
            action='done'):
121
163
    """Resolve some or all of the conflicts in a working tree.
122
164
 
123
165
    :param paths: If None, resolve all conflicts.  Otherwise, select only
127
169
        recursive commands like revert, this should be True.  For commands
128
170
        or applications wishing finer-grained control, like the resolve
129
171
        command, this should be False.
130
 
    :ignore_misses: If False, warnings will be printed if the supplied paths
131
 
        do not have conflicts.
 
172
    :param ignore_misses: If False, warnings will be printed if the supplied
 
173
        paths do not have conflicts.
 
174
    :param action: How the conflict should be resolved,
132
175
    """
133
176
    tree.lock_tree_write()
134
177
    try:
135
178
        tree_conflicts = tree.conflicts()
136
179
        if paths is None:
137
180
            new_conflicts = ConflictList()
138
 
            selected_conflicts = tree_conflicts
 
181
            to_process = tree_conflicts
139
182
        else:
140
 
            new_conflicts, selected_conflicts = \
141
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses,
142
 
                    recursive)
 
183
            new_conflicts, to_process = tree_conflicts.select_conflicts(
 
184
                tree, paths, ignore_misses, recursive)
 
185
        for conflict in to_process:
 
186
            try:
 
187
                conflict._do(action, tree)
 
188
                conflict.cleanup(tree)
 
189
            except NotImplementedError:
 
190
                new_conflicts.append(conflict)
143
191
        try:
144
192
            tree.set_conflicts(new_conflicts)
145
193
        except errors.UnsupportedOperation:
146
194
            pass
147
 
        selected_conflicts.remove_files(tree)
148
195
    finally:
149
196
        tree.unlock()
150
197
 
151
198
 
152
199
def restore(filename):
153
 
    """\
154
 
    Restore a conflicted file to the state it was in before merging.
155
 
    Only text restoration supported at present.
 
200
    """Restore a conflicted file to the state it was in before merging.
 
201
 
 
202
    Only text restoration is supported at present.
156
203
    """
157
204
    conflicted = False
158
205
    try:
239
286
        for conflict in self:
240
287
            if not conflict.has_files:
241
288
                continue
242
 
            for suffix in CONFLICT_SUFFIXES:
243
 
                try:
244
 
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
245
 
                except OSError, e:
246
 
                    if e.errno != errno.ENOENT:
247
 
                        raise
 
289
            conflict.cleanup(tree)
248
290
 
249
291
    def select_conflicts(self, tree, paths, ignore_misses=False,
250
292
                         recurse=False):
303
345
class Conflict(object):
304
346
    """Base class for all types of conflict"""
305
347
 
 
348
    # FIXME: cleanup should take care of that ? -- vila 091229
306
349
    has_files = False
307
350
 
308
351
    def __init__(self, path, file_id=None):
357
400
        else:
358
401
            return None, conflict.typestring
359
402
 
 
403
    def _do(self, action, tree):
 
404
        """Apply the specified action to the conflict.
 
405
 
 
406
        :param action: The method name to call.
 
407
 
 
408
        :param tree: The tree passed as a parameter to the method.
 
409
        """
 
410
        meth = getattr(self, 'action_%s' % action, None)
 
411
        if meth is None:
 
412
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
 
413
        meth(tree)
 
414
 
 
415
    def cleanup(self, tree):
 
416
        raise NotImplementedError(self.cleanup)
 
417
 
 
418
    def action_done(self, tree):
 
419
        """Mark the conflict as solved once it has been handled."""
 
420
        # This method does nothing but simplifies the design of upper levels.
 
421
        pass
 
422
 
 
423
    def action_take_this(self, tree):
 
424
        raise NotImplementedError(self.action_take_this)
 
425
 
 
426
    def action_take_other(self, tree):
 
427
        raise NotImplementedError(self.action_take_other)
 
428
 
360
429
 
361
430
class PathConflict(Conflict):
362
431
    """A conflict was encountered merging file paths"""
366
435
    format = 'Path conflict: %(path)s / %(conflict_path)s'
367
436
 
368
437
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
438
 
369
439
    def __init__(self, path, conflict_path=None, file_id=None):
370
440
        Conflict.__init__(self, path, file_id)
371
441
        self.conflict_path = conflict_path
376
446
            s.add('conflict_path', self.conflict_path)
377
447
        return s
378
448
 
 
449
    def cleanup(self, tree):
 
450
        # No additional files have been generated here
 
451
        pass
 
452
 
 
453
    def action_take_this(self, tree):
 
454
        tree.rename_one(self.conflict_path, self.path)
 
455
 
 
456
    def action_take_other(self, tree):
 
457
        # just acccept bzr proposal
 
458
        pass
 
459
 
379
460
 
380
461
class ContentsConflict(PathConflict):
381
462
    """The files are of different types, or not present"""
386
467
 
387
468
    format = 'Contents conflict in %(path)s'
388
469
 
389
 
 
 
470
    def cleanup(self, tree):
 
471
        for suffix in ('.BASE', '.OTHER'):
 
472
            try:
 
473
                osutils.delete_any(tree.abspath(self.path + suffix))
 
474
            except OSError, e:
 
475
                if e.errno != errno.ENOENT:
 
476
                    raise
 
477
 
 
478
    # FIXME: I smell something weird here and it seems we should be able to be
 
479
    # more coherent with some other conflict ? bzr *did* a choice there but
 
480
    # neither action_take_this nor action_take_other reflect that...
 
481
    # -- vila 20091224
 
482
    def action_take_this(self, tree):
 
483
        tree.remove([self.path + '.OTHER'], force=True, keep_files=False)
 
484
 
 
485
    def action_take_other(self, tree):
 
486
        tree.remove([self.path], force=True, keep_files=False)
 
487
 
 
488
 
 
489
 
 
490
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
 
491
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
 
492
 
 
493
# TODO: There should be a base revid attribute to better inform the user about
 
494
# how the conflicts were generated.
390
495
class TextConflict(PathConflict):
391
496
    """The merge algorithm could not resolve all differences encountered."""
392
497
 
396
501
 
397
502
    format = 'Text conflict in %(path)s'
398
503
 
 
504
    def cleanup(self, tree):
 
505
        for suffix in CONFLICT_SUFFIXES:
 
506
            try:
 
507
                osutils.delete_any(tree.abspath(self.path+suffix))
 
508
            except OSError, e:
 
509
                if e.errno != errno.ENOENT:
 
510
                    raise
 
511
 
399
512
 
400
513
class HandledConflict(Conflict):
401
514
    """A path problem that has been provisionally resolved.
416
529
        s.add('action', self.action)
417
530
        return s
418
531
 
 
532
    def cleanup(self, tree):
 
533
        """Nothing to cleanup."""
 
534
        pass
 
535
 
419
536
 
420
537
class HandledPathConflict(HandledConflict):
421
538
    """A provisionally-resolved path problem involving two paths.
462
579
 
463
580
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
464
581
 
 
582
    def action_take_this(self, tree):
 
583
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
584
        tree.rename_one(self.path, self.conflict_path)
 
585
 
 
586
    def action_take_other(self, tree):
 
587
        tree.remove([self.path], force=True, keep_files=False)
 
588
 
465
589
 
466
590
class ParentLoop(HandledPathConflict):
467
591
    """An attempt to create an infinitely-looping directory structure.
468
592
    This is rare, but can be produced like so:
469
593
 
470
594
    tree A:
471
 
      mv foo/bar
 
595
      mv foo bar
472
596
    tree B:
473
 
      mv bar/foo
 
597
      mv bar foo
474
598
    merge A and B
475
599
    """
476
600
 
478
602
 
479
603
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
480
604
 
 
605
    def action_take_this(self, tree):
 
606
        # just acccept bzr proposal
 
607
        pass
 
608
 
 
609
    def action_take_other(self, tree):
 
610
        # FIXME: We shouldn't have to manipulate so many paths here (and there
 
611
        # is probably a bug or two...)
 
612
        base_path = osutils.basename(self.path)
 
613
        conflict_base_path = osutils.basename(self.conflict_path)
 
614
        tt = transform.TreeTransform(tree)
 
615
        try:
 
616
            p_tid = tt.trans_id_file_id(self.file_id)
 
617
            parent_tid = tt.get_tree_parent(p_tid)
 
618
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
 
619
            cparent_tid = tt.get_tree_parent(cp_tid)
 
620
            tt.adjust_path(base_path, cparent_tid, cp_tid)
 
621
            tt.adjust_path(conflict_base_path, parent_tid, p_tid)
 
622
            tt.apply()
 
623
        finally:
 
624
            tt.finalize()
 
625
 
481
626
 
482
627
class UnversionedParent(HandledConflict):
483
 
    """An attempt to version an file whose parent directory is not versioned.
 
628
    """An attempt to version a file whose parent directory is not versioned.
484
629
    Typically, the result of a merge where one tree unversioned the directory
485
630
    and the other added a versioned file to it.
486
631
    """
490
635
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
491
636
             ' children.  %(action)s.'
492
637
 
 
638
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
639
    # conflict shouldn't exist (the long story is that the conflict is
 
640
    # generated with another one that can be resolved properly) -- vila 091224
 
641
    def action_take_this(self, tree):
 
642
        pass
 
643
 
 
644
    def action_take_other(self, tree):
 
645
        pass
 
646
 
493
647
 
494
648
class MissingParent(HandledConflict):
495
649
    """An attempt to add files to a directory that is not present.
496
650
    Typically, the result of a merge where THIS deleted the directory and
497
651
    the OTHER added a file to it.
498
 
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
652
    See also: DeletingParent (same situation, THIS and OTHER reversed)
499
653
    """
500
654
 
501
655
    typestring = 'missing parent'
502
656
 
503
657
    format = 'Conflict adding files to %(path)s.  %(action)s.'
504
658
 
 
659
    def action_take_this(self, tree):
 
660
        tree.remove([self.path], force=True, keep_files=False)
 
661
 
 
662
    def action_take_other(self, tree):
 
663
        # just acccept bzr proposal
 
664
        pass
 
665
 
505
666
 
506
667
class DeletingParent(HandledConflict):
507
668
    """An attempt to add files to a directory that is not present.
514
675
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
515
676
             "%(action)s."
516
677
 
 
678
    # FIXME: It's a bit strange that the default action is not coherent with
 
679
    # MissingParent from the *user* pov.
 
680
 
 
681
    def action_take_this(self, tree):
 
682
        # just acccept bzr proposal
 
683
        pass
 
684
 
 
685
    def action_take_other(self, tree):
 
686
        tree.remove([self.path], force=True, keep_files=False)
 
687
 
517
688
 
518
689
class NonDirectoryParent(HandledConflict):
519
 
    """An attempt to add files to a directory that is not a director or
 
690
    """An attempt to add files to a directory that is not a directory or
520
691
    an attempt to change the kind of a directory with files.
521
692
    """
522
693
 
525
696
    format = "Conflict: %(path)s is not a directory, but has files in it."\
526
697
             "  %(action)s."
527
698
 
 
699
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
700
 
 
701
    def action_take_this(self, tree):
 
702
        # FIXME: we should preserve that path when the conflict is generated !
 
703
        if self.path.endswith('.new'):
 
704
            conflict_path = self.path[:-(len('.new'))]
 
705
            tree.remove([self.path], force=True, keep_files=False)
 
706
            tree.add(conflict_path)
 
707
        else:
 
708
            raise NotImplementedError(self.action_take_this)
 
709
 
 
710
    def action_take_other(self, tree):
 
711
        # FIXME: we should preserve that path when the conflict is generated !
 
712
        if self.path.endswith('.new'):
 
713
            conflict_path = self.path[:-(len('.new'))]
 
714
            tree.remove([conflict_path], force=True, keep_files=False)
 
715
            tree.rename_one(self.path, conflict_path)
 
716
        else:
 
717
            raise NotImplementedError(self.action_take_other)
 
718
 
 
719
 
528
720
ctype = {}
529
721
 
530
722
 
534
726
    for conflict_type in conflict_types:
535
727
        ctype[conflict_type.typestring] = conflict_type
536
728
 
537
 
 
538
729
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
539
730
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
540
731
               DeletingParent, NonDirectoryParent)