~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-02-02 15:23:15 UTC
  • mfrom: (4996.3.1 trivial)
  • Revision ID: pqm@pqm.ubuntu.com-20100202152315-dzbzbhpwun9xpnj6
(mbp) remove contrib/fortune

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