~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: John Arbash Meinel
  • Date: 2008-11-25 17:15:26 UTC
  • mto: This revision was merged to the branch mainline in revision 3851.
  • Revision ID: john@arbash-meinel.com-20081125171526-pi2g4m1w70pkie1f
Add a bit of help text when supplying --help.

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
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
# TODO: 'bzr resolve' should accept a directory name and work from that
 
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 
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(), """
26
27
 
27
28
from bzrlib import (
28
29
    builtins,
29
 
    cleanup,
30
30
    commands,
31
31
    errors,
32
32
    osutils,
33
33
    rio,
34
34
    trace,
35
 
    transform,
36
 
    workingtree,
37
35
    )
38
36
""")
39
 
from bzrlib import (
40
 
    option,
41
 
    registry,
42
 
    )
 
37
from bzrlib.option import Option
43
38
 
44
39
 
45
40
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
58
53
    instead.  (This is useful for editing all files with text conflicts.)
59
54
 
60
55
    Use bzr resolve when you have fixed a problem.
 
56
 
 
57
    See also bzr resolve.
61
58
    """
62
59
    takes_options = [
63
 
            option.Option('text',
64
 
                          help='List paths of files with text conflicts.'),
 
60
            Option('text',
 
61
                   help='List paths of files with text conflicts.'),
65
62
        ]
66
 
    _see_also = ['resolve', 'conflict-types']
67
63
 
68
64
    def run(self, text=False):
69
 
        wt = workingtree.WorkingTree.open_containing(u'.')[0]
 
65
        from bzrlib.workingtree import WorkingTree
 
66
        wt = WorkingTree.open_containing(u'.')[0]
70
67
        for conflict in wt.conflicts():
71
68
            if text:
72
69
                if conflict.typestring != 'text conflict':
76
73
                self.outf.write(str(conflict) + '\n')
77
74
 
78
75
 
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
 
 
101
76
class cmd_resolve(commands.Command):
102
77
    """Mark a conflict as resolved.
103
78
 
107
82
    before you should commit.
108
83
 
109
84
    Once you have fixed a problem, use "bzr resolve" to automatically mark
110
 
    text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
 
85
    text conflicts as fixed, resolve FILE to mark a specific conflict as
111
86
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
 
87
 
 
88
    See also bzr conflicts.
112
89
    """
113
90
    aliases = ['resolved']
114
91
    takes_args = ['file*']
115
92
    takes_options = [
116
 
            option.Option('all', help='Resolve all conflicts in this tree.'),
117
 
            ResolveActionOption(),
 
93
            Option('all', help='Resolve all conflicts in this tree.'),
118
94
            ]
119
 
    _see_also = ['conflicts']
120
 
    def run(self, file_list=None, all=False, action=None):
 
95
    def run(self, file_list=None, all=False):
 
96
        from bzrlib.workingtree import WorkingTree
121
97
        if all:
122
98
            if file_list:
123
99
                raise errors.BzrCommandError("If --all is specified,"
124
100
                                             " no FILE may be provided")
125
 
            tree = workingtree.WorkingTree.open_containing('.')[0]
126
 
            if action is None:
127
 
                action = 'done'
 
101
            tree = WorkingTree.open_containing('.')[0]
 
102
            resolve(tree)
128
103
        else:
129
104
            tree, file_list = builtins.tree_files(file_list)
130
105
            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:
143
106
                un_resolved, resolved = tree.auto_resolve()
144
107
                if len(un_resolved) > 0:
145
108
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
151
114
                    trace.note('All conflicts resolved.')
152
115
                    return 0
153
116
            else:
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'):
 
117
                resolve(tree, file_list)
 
118
 
 
119
 
 
120
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
164
121
    """Resolve some or all of the conflicts in a working tree.
165
122
 
166
123
    :param paths: If None, resolve all conflicts.  Otherwise, select only
170
127
        recursive commands like revert, this should be True.  For commands
171
128
        or applications wishing finer-grained control, like the resolve
172
129
        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,
 
130
    :ignore_misses: If False, warnings will be printed if the supplied paths
 
131
        do not have conflicts.
176
132
    """
177
133
    tree.lock_tree_write()
178
134
    try:
179
135
        tree_conflicts = tree.conflicts()
180
136
        if paths is None:
181
137
            new_conflicts = ConflictList()
182
 
            to_process = tree_conflicts
 
138
            selected_conflicts = tree_conflicts
183
139
        else:
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)
 
140
            new_conflicts, selected_conflicts = \
 
141
                tree_conflicts.select_conflicts(tree, paths, ignore_misses,
 
142
                    recursive)
192
143
        try:
193
144
            tree.set_conflicts(new_conflicts)
194
145
        except errors.UnsupportedOperation:
195
146
            pass
 
147
        selected_conflicts.remove_files(tree)
196
148
    finally:
197
149
        tree.unlock()
198
150
 
199
151
 
200
152
def restore(filename):
201
 
    """Restore a conflicted file to the state it was in before merging.
202
 
 
203
 
    Only text restoration is supported at present.
 
153
    """\
 
154
    Restore a conflicted file to the state it was in before merging.
 
155
    Only text restoration supported at present.
204
156
    """
205
157
    conflicted = False
206
158
    try:
276
228
        """Generator of stanzas"""
277
229
        for conflict in self:
278
230
            yield conflict.as_stanza()
279
 
 
 
231
            
280
232
    def to_strings(self):
281
233
        """Generate strings for the provided conflicts"""
282
234
        for conflict in self:
287
239
        for conflict in self:
288
240
            if not conflict.has_files:
289
241
                continue
290
 
            conflict.cleanup(tree)
 
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
291
248
 
292
249
    def select_conflicts(self, tree, paths, ignore_misses=False,
293
250
                         recurse=False):
294
251
        """Select the conflicts associated with paths in a tree.
295
 
 
 
252
        
296
253
        File-ids are also used for this.
297
254
        :return: a pair of ConflictLists: (not_selected, selected)
298
255
        """
342
299
                    print "%s is not conflicted" % path
343
300
        return new_conflicts, selected_conflicts
344
301
 
345
 
 
 
302
 
346
303
class Conflict(object):
347
304
    """Base class for all types of conflict"""
348
305
 
349
 
    # FIXME: cleanup should take care of that ? -- vila 091229
350
306
    has_files = False
351
307
 
352
308
    def __init__(self, path, file_id=None):
401
357
        else:
402
358
            return None, conflict.typestring
403
359
 
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
 
 
439
360
 
440
361
class PathConflict(Conflict):
441
362
    """A conflict was encountered merging file paths"""
445
366
    format = 'Path conflict: %(path)s / %(conflict_path)s'
446
367
 
447
368
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
448
 
 
449
369
    def __init__(self, path, conflict_path=None, file_id=None):
450
370
        Conflict.__init__(self, path, file_id)
451
371
        self.conflict_path = conflict_path
456
376
            s.add('conflict_path', self.conflict_path)
457
377
        return s
458
378
 
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
 
 
470
379
 
471
380
class ContentsConflict(PathConflict):
472
381
    """The files are of different types, or not present"""
477
386
 
478
387
    format = 'Contents conflict in %(path)s'
479
388
 
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.
 
389
 
527
390
class TextConflict(PathConflict):
528
391
    """The merge algorithm could not resolve all differences encountered."""
529
392
 
533
396
 
534
397
    format = 'Text conflict in %(path)s'
535
398
 
536
 
    def associated_filenames(self):
537
 
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
538
 
 
539
399
 
540
400
class HandledConflict(Conflict):
541
401
    """A path problem that has been provisionally resolved.
543
403
    """
544
404
 
545
405
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
546
 
 
 
406
    
547
407
    def __init__(self, action, path, file_id=None):
548
408
        Conflict.__init__(self, path, file_id)
549
409
        self.action = action
556
416
        s.add('action', self.action)
557
417
        return s
558
418
 
559
 
    def associated_filenames(self):
560
 
        # Nothing has been generated here
561
 
        return []
562
 
 
563
419
 
564
420
class HandledPathConflict(HandledConflict):
565
421
    """A provisionally-resolved path problem involving two paths.
572
428
    def __init__(self, action, path, conflict_path, file_id=None,
573
429
                 conflict_file_id=None):
574
430
        HandledConflict.__init__(self, action, path, file_id)
575
 
        self.conflict_path = conflict_path
 
431
        self.conflict_path = conflict_path 
576
432
        # warn turned off, because the factory blindly transfers the Stanza
577
433
        # values to __init__.
578
434
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
579
435
                                                     warn=False)
580
 
 
 
436
        
581
437
    def _cmp_list(self):
582
 
        return HandledConflict._cmp_list(self) + [self.conflict_path,
 
438
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
583
439
                                                  self.conflict_file_id]
584
440
 
585
441
    def as_stanza(self):
587
443
        s.add('conflict_path', self.conflict_path)
588
444
        if self.conflict_file_id is not None:
589
445
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
590
 
 
 
446
            
591
447
        return s
592
448
 
593
449
 
606
462
 
607
463
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
608
464
 
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
 
 
616
465
 
617
466
class ParentLoop(HandledPathConflict):
618
467
    """An attempt to create an infinitely-looping directory structure.
619
468
    This is rare, but can be produced like so:
620
469
 
621
470
    tree A:
622
 
      mv foo bar
 
471
      mv foo/bar
623
472
    tree B:
624
 
      mv bar foo
 
473
      mv bar/foo
625
474
    merge A and B
626
475
    """
627
476
 
629
478
 
630
479
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
631
480
 
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
 
 
653
481
 
654
482
class UnversionedParent(HandledConflict):
655
 
    """An attempt to version a file whose parent directory is not versioned.
 
483
    """An attempt to version an file whose parent directory is not versioned.
656
484
    Typically, the result of a merge where one tree unversioned the directory
657
485
    and the other added a versioned file to it.
658
486
    """
662
490
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
663
491
             ' children.  %(action)s.'
664
492
 
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
 
 
674
493
 
675
494
class MissingParent(HandledConflict):
676
495
    """An attempt to add files to a directory that is not present.
677
496
    Typically, the result of a merge where THIS deleted the directory and
678
497
    the OTHER added a file to it.
679
 
    See also: DeletingParent (same situation, THIS and OTHER reversed)
 
498
    See also: DeletingParent (same situation, reversed THIS and OTHER)
680
499
    """
681
500
 
682
501
    typestring = 'missing parent'
683
502
 
684
503
    format = 'Conflict adding files to %(path)s.  %(action)s.'
685
504
 
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
 
 
693
505
 
694
506
class DeletingParent(HandledConflict):
695
507
    """An attempt to add files to a directory that is not present.
702
514
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
703
515
             "%(action)s."
704
516
 
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
517
 
716
518
class NonDirectoryParent(HandledConflict):
717
 
    """An attempt to add files to a directory that is not a directory or
 
519
    """An attempt to add files to a directory that is not a director or
718
520
    an attempt to change the kind of a directory with files.
719
521
    """
720
522
 
723
525
    format = "Conflict: %(path)s is not a directory, but has files in it."\
724
526
             "  %(action)s."
725
527
 
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
 
 
746
 
 
747
528
ctype = {}
748
529
 
749
530
 
753
534
    for conflict_type in conflict_types:
754
535
        ctype[conflict_type.typestring] = conflict_type
755
536
 
 
537
 
756
538
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
757
539
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
758
540
               DeletingParent, NonDirectoryParent)