~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Vincent Ladeuil
  • Date: 2010-03-02 10:21:39 UTC
  • mfrom: (4797.2.24 2.1)
  • mto: This revision was merged to the branch mainline in revision 5069.
  • Revision ID: v.ladeuil+lp@free.fr-20100302102139-b5cba7h6xu13mekg
Merge 2.1 into trunk including fixes for #331095, #507557, #185103, #524184 and #369501

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Aaron Bentley, Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 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
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
# TODO: Move this into builtins
18
 
 
19
 
# TODO: 'bzr resolve' should accept a directory name and work from that 
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
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(), """
27
26
 
28
27
from bzrlib import (
29
28
    builtins,
 
29
    cleanup,
30
30
    commands,
31
31
    errors,
32
32
    osutils,
33
33
    rio,
34
34
    trace,
 
35
    transform,
 
36
    workingtree,
35
37
    )
36
38
""")
37
 
from bzrlib.option import Option
 
39
from bzrlib import (
 
40
    option,
 
41
    registry,
 
42
    )
38
43
 
39
44
 
40
45
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
53
58
    instead.  (This is useful for editing all files with text conflicts.)
54
59
 
55
60
    Use bzr resolve when you have fixed a problem.
56
 
 
57
 
    See also bzr resolve.
58
61
    """
59
62
    takes_options = [
60
 
            Option('text',
61
 
                   help='List paths of files with text conflicts.'),
 
63
            option.Option('text',
 
64
                          help='List paths of files with text conflicts.'),
62
65
        ]
 
66
    _see_also = ['resolve', 'conflict-types']
63
67
 
64
68
    def run(self, text=False):
65
 
        from bzrlib.workingtree import WorkingTree
66
 
        wt = WorkingTree.open_containing(u'.')[0]
 
69
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
67
70
        for conflict in wt.conflicts():
68
71
            if text:
69
72
                if conflict.typestring != 'text conflict':
73
76
                self.outf.write(str(conflict) + '\n')
74
77
 
75
78
 
 
79
resolve_action_registry = registry.Registry()
 
80
 
 
81
 
 
82
resolve_action_registry.register(
 
83
    'done', 'done', 'Marks the conflict as resolved' )
 
84
resolve_action_registry.register(
 
85
    'take-this', 'take_this',
 
86
    'Resolve the conflict preserving the version in the working tree' )
 
87
resolve_action_registry.register(
 
88
    'take-other', 'take_other',
 
89
    'Resolve the conflict taking the merged version into account' )
 
90
resolve_action_registry.default_key = 'done'
 
91
 
 
92
class ResolveActionOption(option.RegistryOption):
 
93
 
 
94
    def __init__(self):
 
95
        super(ResolveActionOption, self).__init__(
 
96
            'action', 'How to resolve the conflict.',
 
97
            value_switches=True,
 
98
            registry=resolve_action_registry)
 
99
 
 
100
 
76
101
class cmd_resolve(commands.Command):
77
102
    """Mark a conflict as resolved.
78
103
 
82
107
    before you should commit.
83
108
 
84
109
    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
 
110
    text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
86
111
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
87
 
 
88
 
    See also bzr conflicts.
89
112
    """
90
113
    aliases = ['resolved']
91
114
    takes_args = ['file*']
92
115
    takes_options = [
93
 
            Option('all', help='Resolve all conflicts in this tree.'),
 
116
            option.Option('all', help='Resolve all conflicts in this tree.'),
 
117
            ResolveActionOption(),
94
118
            ]
95
 
    def run(self, file_list=None, all=False):
96
 
        from bzrlib.workingtree import WorkingTree
 
119
    _see_also = ['conflicts']
 
120
    def run(self, file_list=None, all=False, action=None):
97
121
        if all:
98
122
            if file_list:
99
123
                raise errors.BzrCommandError("If --all is specified,"
100
124
                                             " no FILE may be provided")
101
 
            tree = WorkingTree.open_containing('.')[0]
102
 
            resolve(tree)
 
125
            tree = workingtree.WorkingTree.open_containing('.')[0]
 
126
            if action is None:
 
127
                action = 'done'
103
128
        else:
104
129
            tree, file_list = builtins.tree_files(file_list)
105
130
            if file_list is None:
 
131
                if action is None:
 
132
                    # FIXME: There is a special case here related to the option
 
133
                    # handling that could be clearer and easier to discover by
 
134
                    # providing an --auto action (bug #344013 and #383396) and
 
135
                    # make it mandatory instead of implicit and active only
 
136
                    # when no file_list is provided -- vila 091229
 
137
                    action = 'auto'
 
138
            else:
 
139
                if action is None:
 
140
                    action = 'done'
 
141
        if action == 'auto':
 
142
            if file_list is None:
106
143
                un_resolved, resolved = tree.auto_resolve()
107
144
                if len(un_resolved) > 0:
108
145
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
114
151
                    trace.note('All conflicts resolved.')
115
152
                    return 0
116
153
            else:
117
 
                resolve(tree, file_list)
118
 
 
119
 
 
120
 
def resolve(tree, paths=None, ignore_misses=False):
 
154
                # FIXME: This can never occur but the block above needs some
 
155
                # refactoring to transfer tree.auto_resolve() to
 
156
                # conflict.auto(tree) --vila 091242
 
157
                pass
 
158
        else:
 
159
            resolve(tree, file_list, action=action)
 
160
 
 
161
 
 
162
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
 
163
            action='done'):
 
164
    """Resolve some or all of the conflicts in a working tree.
 
165
 
 
166
    :param paths: If None, resolve all conflicts.  Otherwise, select only
 
167
        specified conflicts.
 
168
    :param recursive: If True, then elements of paths which are directories
 
169
        have all their children resolved, etc.  When invoked as part of
 
170
        recursive commands like revert, this should be True.  For commands
 
171
        or applications wishing finer-grained control, like the resolve
 
172
        command, this should be False.
 
173
    :param ignore_misses: If False, warnings will be printed if the supplied
 
174
        paths do not have conflicts.
 
175
    :param action: How the conflict should be resolved,
 
176
    """
121
177
    tree.lock_tree_write()
122
178
    try:
123
179
        tree_conflicts = tree.conflicts()
124
180
        if paths is None:
125
181
            new_conflicts = ConflictList()
126
 
            selected_conflicts = tree_conflicts
 
182
            to_process = tree_conflicts
127
183
        else:
128
 
            new_conflicts, selected_conflicts = \
129
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses)
 
184
            new_conflicts, to_process = tree_conflicts.select_conflicts(
 
185
                tree, paths, ignore_misses, recursive)
 
186
        for conflict in to_process:
 
187
            try:
 
188
                conflict._do(action, tree)
 
189
                conflict.cleanup(tree)
 
190
            except NotImplementedError:
 
191
                new_conflicts.append(conflict)
130
192
        try:
131
193
            tree.set_conflicts(new_conflicts)
132
194
        except errors.UnsupportedOperation:
133
195
            pass
134
 
        selected_conflicts.remove_files(tree)
135
196
    finally:
136
197
        tree.unlock()
137
198
 
138
199
 
139
200
def restore(filename):
140
 
    """\
141
 
    Restore a conflicted file to the state it was in before merging.
142
 
    Only text restoration supported at present.
 
201
    """Restore a conflicted file to the state it was in before merging.
 
202
 
 
203
    Only text restoration is supported at present.
143
204
    """
144
205
    conflicted = False
145
206
    try:
215
276
        """Generator of stanzas"""
216
277
        for conflict in self:
217
278
            yield conflict.as_stanza()
218
 
            
 
279
 
219
280
    def to_strings(self):
220
281
        """Generate strings for the provided conflicts"""
221
282
        for conflict in self:
226
287
        for conflict in self:
227
288
            if not conflict.has_files:
228
289
                continue
229
 
            for suffix in CONFLICT_SUFFIXES:
230
 
                try:
231
 
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
232
 
                except OSError, e:
233
 
                    if e.errno != errno.ENOENT:
234
 
                        raise
 
290
            conflict.cleanup(tree)
235
291
 
236
292
    def select_conflicts(self, tree, paths, ignore_misses=False,
237
293
                         recurse=False):
238
294
        """Select the conflicts associated with paths in a tree.
239
 
        
 
295
 
240
296
        File-ids are also used for this.
241
297
        :return: a pair of ConflictLists: (not_selected, selected)
242
298
        """
286
342
                    print "%s is not conflicted" % path
287
343
        return new_conflicts, selected_conflicts
288
344
 
289
 
 
 
345
 
290
346
class Conflict(object):
291
347
    """Base class for all types of conflict"""
292
348
 
 
349
    # FIXME: cleanup should take care of that ? -- vila 091229
293
350
    has_files = False
294
351
 
295
352
    def __init__(self, path, file_id=None):
344
401
        else:
345
402
            return None, conflict.typestring
346
403
 
 
404
    def _do(self, action, tree):
 
405
        """Apply the specified action to the conflict.
 
406
 
 
407
        :param action: The method name to call.
 
408
 
 
409
        :param tree: The tree passed as a parameter to the method.
 
410
        """
 
411
        meth = getattr(self, 'action_%s' % action, None)
 
412
        if meth is None:
 
413
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
 
414
        meth(tree)
 
415
 
 
416
    def associated_filenames(self):
 
417
        """The names of the files generated to help resolve the conflict."""
 
418
        raise NotImplementedError(self.associated_filenames)
 
419
 
 
420
    def cleanup(self, tree):
 
421
        for fname in self.associated_filenames():
 
422
            try:
 
423
                osutils.delete_any(tree.abspath(fname))
 
424
            except OSError, e:
 
425
                if e.errno != errno.ENOENT:
 
426
                    raise
 
427
 
 
428
    def action_done(self, tree):
 
429
        """Mark the conflict as solved once it has been handled."""
 
430
        # This method does nothing but simplifies the design of upper levels.
 
431
        pass
 
432
 
 
433
    def action_take_this(self, tree):
 
434
        raise NotImplementedError(self.action_take_this)
 
435
 
 
436
    def action_take_other(self, tree):
 
437
        raise NotImplementedError(self.action_take_other)
 
438
 
347
439
 
348
440
class PathConflict(Conflict):
349
441
    """A conflict was encountered merging file paths"""
353
445
    format = 'Path conflict: %(path)s / %(conflict_path)s'
354
446
 
355
447
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
448
 
356
449
    def __init__(self, path, conflict_path=None, file_id=None):
357
450
        Conflict.__init__(self, path, file_id)
358
451
        self.conflict_path = conflict_path
363
456
            s.add('conflict_path', self.conflict_path)
364
457
        return s
365
458
 
 
459
    def associated_filenames(self):
 
460
        # No additional files have been generated here
 
461
        return []
 
462
 
 
463
    def action_take_this(self, tree):
 
464
        tree.rename_one(self.conflict_path, self.path)
 
465
 
 
466
    def action_take_other(self, tree):
 
467
        # just acccept bzr proposal
 
468
        pass
 
469
 
366
470
 
367
471
class ContentsConflict(PathConflict):
368
472
    """The files are of different types, or not present"""
373
477
 
374
478
    format = 'Contents conflict in %(path)s'
375
479
 
376
 
 
 
480
    def associated_filenames(self):
 
481
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
 
482
 
 
483
    def _take_it(self, tt, suffix_to_remove):
 
484
        """Resolve the conflict.
 
485
 
 
486
        :param tt: The TreeTransform where the conflict is resolved.
 
487
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
 
488
 
 
489
        The resolution is symmetric, when taking THIS, OTHER is deleted and
 
490
        item.THIS is renamed into item and vice-versa.
 
491
        """
 
492
        try:
 
493
            # Delete 'item.THIS' or 'item.OTHER' depending on
 
494
            # suffix_to_remove
 
495
            tt.delete_contents(
 
496
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
 
497
        except errors.NoSuchFile:
 
498
            # There are valid cases where 'item.suffix_to_remove' either
 
499
            # never existed or was already deleted (including the case
 
500
            # where the user deleted it)
 
501
            pass
 
502
        # Rename 'item.suffix_to_remove' (note that if
 
503
        # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
504
        this_tid = tt.trans_id_file_id(self.file_id)
 
505
        parent_tid = tt.get_tree_parent(this_tid)
 
506
        tt.adjust_path(self.path, parent_tid, this_tid)
 
507
        tt.apply()
 
508
 
 
509
    def _take_it_with_cleanups(self, tree, suffix_to_remove):
 
510
        tt = transform.TreeTransform(tree)
 
511
        op = cleanup.OperationWithCleanups(self._take_it)
 
512
        op.add_cleanup(tt.finalize)
 
513
        op.run_simple(tt, suffix_to_remove)
 
514
 
 
515
    def action_take_this(self, tree):
 
516
        self._take_it_with_cleanups(tree, 'OTHER')
 
517
 
 
518
    def action_take_other(self, tree):
 
519
        self._take_it_with_cleanups(tree, 'THIS')
 
520
 
 
521
 
 
522
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
 
523
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
 
524
 
 
525
# TODO: There should be a base revid attribute to better inform the user about
 
526
# how the conflicts were generated.
377
527
class TextConflict(PathConflict):
378
528
    """The merge algorithm could not resolve all differences encountered."""
379
529
 
383
533
 
384
534
    format = 'Text conflict in %(path)s'
385
535
 
 
536
    def associated_filenames(self):
 
537
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
538
 
386
539
 
387
540
class HandledConflict(Conflict):
388
541
    """A path problem that has been provisionally resolved.
390
543
    """
391
544
 
392
545
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
393
 
    
 
546
 
394
547
    def __init__(self, action, path, file_id=None):
395
548
        Conflict.__init__(self, path, file_id)
396
549
        self.action = action
403
556
        s.add('action', self.action)
404
557
        return s
405
558
 
 
559
    def associated_filenames(self):
 
560
        # Nothing has been generated here
 
561
        return []
 
562
 
406
563
 
407
564
class HandledPathConflict(HandledConflict):
408
565
    """A provisionally-resolved path problem involving two paths.
415
572
    def __init__(self, action, path, conflict_path, file_id=None,
416
573
                 conflict_file_id=None):
417
574
        HandledConflict.__init__(self, action, path, file_id)
418
 
        self.conflict_path = conflict_path 
 
575
        self.conflict_path = conflict_path
419
576
        # warn turned off, because the factory blindly transfers the Stanza
420
577
        # values to __init__.
421
578
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
422
579
                                                     warn=False)
423
 
        
 
580
 
424
581
    def _cmp_list(self):
425
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
582
        return HandledConflict._cmp_list(self) + [self.conflict_path,
426
583
                                                  self.conflict_file_id]
427
584
 
428
585
    def as_stanza(self):
430
587
        s.add('conflict_path', self.conflict_path)
431
588
        if self.conflict_file_id is not None:
432
589
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
433
 
            
 
590
 
434
591
        return s
435
592
 
436
593
 
449
606
 
450
607
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
451
608
 
 
609
    def action_take_this(self, tree):
 
610
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
611
        tree.rename_one(self.path, self.conflict_path)
 
612
 
 
613
    def action_take_other(self, tree):
 
614
        tree.remove([self.path], force=True, keep_files=False)
 
615
 
452
616
 
453
617
class ParentLoop(HandledPathConflict):
454
618
    """An attempt to create an infinitely-looping directory structure.
455
619
    This is rare, but can be produced like so:
456
620
 
457
621
    tree A:
458
 
      mv foo/bar
 
622
      mv foo bar
459
623
    tree B:
460
 
      mv bar/foo
 
624
      mv bar foo
461
625
    merge A and B
462
626
    """
463
627
 
465
629
 
466
630
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
467
631
 
 
632
    def action_take_this(self, tree):
 
633
        # just acccept bzr proposal
 
634
        pass
 
635
 
 
636
    def action_take_other(self, tree):
 
637
        # FIXME: We shouldn't have to manipulate so many paths here (and there
 
638
        # is probably a bug or two...)
 
639
        base_path = osutils.basename(self.path)
 
640
        conflict_base_path = osutils.basename(self.conflict_path)
 
641
        tt = transform.TreeTransform(tree)
 
642
        try:
 
643
            p_tid = tt.trans_id_file_id(self.file_id)
 
644
            parent_tid = tt.get_tree_parent(p_tid)
 
645
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
 
646
            cparent_tid = tt.get_tree_parent(cp_tid)
 
647
            tt.adjust_path(base_path, cparent_tid, cp_tid)
 
648
            tt.adjust_path(conflict_base_path, parent_tid, p_tid)
 
649
            tt.apply()
 
650
        finally:
 
651
            tt.finalize()
 
652
 
468
653
 
469
654
class UnversionedParent(HandledConflict):
470
 
    """An attempt to version an file whose parent directory is not versioned.
 
655
    """An attempt to version a file whose parent directory is not versioned.
471
656
    Typically, the result of a merge where one tree unversioned the directory
472
657
    and the other added a versioned file to it.
473
658
    """
477
662
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
478
663
             ' children.  %(action)s.'
479
664
 
 
665
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
666
    # conflict shouldn't exist (the long story is that the conflict is
 
667
    # generated with another one that can be resolved properly) -- vila 091224
 
668
    def action_take_this(self, tree):
 
669
        pass
 
670
 
 
671
    def action_take_other(self, tree):
 
672
        pass
 
673
 
480
674
 
481
675
class MissingParent(HandledConflict):
482
676
    """An attempt to add files to a directory that is not present.
483
677
    Typically, the result of a merge where THIS deleted the directory and
484
678
    the OTHER added a file to it.
485
 
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
679
    See also: DeletingParent (same situation, THIS and OTHER reversed)
486
680
    """
487
681
 
488
682
    typestring = 'missing parent'
489
683
 
490
684
    format = 'Conflict adding files to %(path)s.  %(action)s.'
491
685
 
 
686
    def action_take_this(self, tree):
 
687
        tree.remove([self.path], force=True, keep_files=False)
 
688
 
 
689
    def action_take_other(self, tree):
 
690
        # just acccept bzr proposal
 
691
        pass
 
692
 
492
693
 
493
694
class DeletingParent(HandledConflict):
494
695
    """An attempt to add files to a directory that is not present.
501
702
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
502
703
             "%(action)s."
503
704
 
 
705
    # FIXME: It's a bit strange that the default action is not coherent with
 
706
    # MissingParent from the *user* pov.
 
707
 
 
708
    def action_take_this(self, tree):
 
709
        # just acccept bzr proposal
 
710
        pass
 
711
 
 
712
    def action_take_other(self, tree):
 
713
        tree.remove([self.path], force=True, keep_files=False)
 
714
 
 
715
 
 
716
class NonDirectoryParent(HandledConflict):
 
717
    """An attempt to add files to a directory that is not a directory or
 
718
    an attempt to change the kind of a directory with files.
 
719
    """
 
720
 
 
721
    typestring = 'non-directory parent'
 
722
 
 
723
    format = "Conflict: %(path)s is not a directory, but has files in it."\
 
724
             "  %(action)s."
 
725
 
 
726
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
727
 
 
728
    def action_take_this(self, tree):
 
729
        # FIXME: we should preserve that path when the conflict is generated !
 
730
        if self.path.endswith('.new'):
 
731
            conflict_path = self.path[:-(len('.new'))]
 
732
            tree.remove([self.path], force=True, keep_files=False)
 
733
            tree.add(conflict_path)
 
734
        else:
 
735
            raise NotImplementedError(self.action_take_this)
 
736
 
 
737
    def action_take_other(self, tree):
 
738
        # FIXME: we should preserve that path when the conflict is generated !
 
739
        if self.path.endswith('.new'):
 
740
            conflict_path = self.path[:-(len('.new'))]
 
741
            tree.remove([conflict_path], force=True, keep_files=False)
 
742
            tree.rename_one(self.path, conflict_path)
 
743
        else:
 
744
            raise NotImplementedError(self.action_take_other)
 
745
 
504
746
 
505
747
ctype = {}
506
748
 
511
753
    for conflict_type in conflict_types:
512
754
        ctype[conflict_type.typestring] = conflict_type
513
755
 
514
 
 
515
756
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
516
757
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
517
 
               DeletingParent,)
 
758
               DeletingParent, NonDirectoryParent)