~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Martin von Gagern
  • Date: 2010-04-20 08:47:38 UTC
  • mfrom: (5167 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5195.
  • Revision ID: martin.vgagern@gmx.net-20100420084738-ygymnqmdllzrhpfn
merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2007 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, recursive=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'):
121
164
    """Resolve some or all of the conflicts in a working tree.
122
165
 
123
166
    :param paths: If None, resolve all conflicts.  Otherwise, select only
127
170
        recursive commands like revert, this should be True.  For commands
128
171
        or applications wishing finer-grained control, like the resolve
129
172
        command, this should be False.
130
 
    :ignore_misses: If False, warnings will be printed if the supplied paths
131
 
        do not have conflicts.
 
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,
132
176
    """
133
177
    tree.lock_tree_write()
134
178
    try:
135
179
        tree_conflicts = tree.conflicts()
136
180
        if paths is None:
137
181
            new_conflicts = ConflictList()
138
 
            selected_conflicts = tree_conflicts
 
182
            to_process = tree_conflicts
139
183
        else:
140
 
            new_conflicts, selected_conflicts = \
141
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses,
142
 
                    recursive)
 
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)
143
192
        try:
144
193
            tree.set_conflicts(new_conflicts)
145
194
        except errors.UnsupportedOperation:
146
195
            pass
147
 
        selected_conflicts.remove_files(tree)
148
196
    finally:
149
197
        tree.unlock()
150
198
 
151
199
 
152
200
def restore(filename):
153
 
    """\
154
 
    Restore a conflicted file to the state it was in before merging.
155
 
    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.
156
204
    """
157
205
    conflicted = False
158
206
    try:
228
276
        """Generator of stanzas"""
229
277
        for conflict in self:
230
278
            yield conflict.as_stanza()
231
 
            
 
279
 
232
280
    def to_strings(self):
233
281
        """Generate strings for the provided conflicts"""
234
282
        for conflict in self:
239
287
        for conflict in self:
240
288
            if not conflict.has_files:
241
289
                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
 
290
            conflict.cleanup(tree)
248
291
 
249
292
    def select_conflicts(self, tree, paths, ignore_misses=False,
250
293
                         recurse=False):
251
294
        """Select the conflicts associated with paths in a tree.
252
 
        
 
295
 
253
296
        File-ids are also used for this.
254
297
        :return: a pair of ConflictLists: (not_selected, selected)
255
298
        """
299
342
                    print "%s is not conflicted" % path
300
343
        return new_conflicts, selected_conflicts
301
344
 
302
 
 
 
345
 
303
346
class Conflict(object):
304
347
    """Base class for all types of conflict"""
305
348
 
 
349
    # FIXME: cleanup should take care of that ? -- vila 091229
306
350
    has_files = False
307
351
 
308
352
    def __init__(self, path, file_id=None):
357
401
        else:
358
402
            return None, conflict.typestring
359
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
 
 
439
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
 
440
        tt = transform.TreeTransform(tree)
 
441
        op = cleanup.OperationWithCleanups(self._resolve)
 
442
        op.add_cleanup(tt.finalize)
 
443
        op.run_simple(tt, *args, **kwargs)
 
444
 
360
445
 
361
446
class PathConflict(Conflict):
362
447
    """A conflict was encountered merging file paths"""
366
451
    format = 'Path conflict: %(path)s / %(conflict_path)s'
367
452
 
368
453
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
454
 
369
455
    def __init__(self, path, conflict_path=None, file_id=None):
370
456
        Conflict.__init__(self, path, file_id)
371
457
        self.conflict_path = conflict_path
376
462
            s.add('conflict_path', self.conflict_path)
377
463
        return s
378
464
 
 
465
    def associated_filenames(self):
 
466
        # No additional files have been generated here
 
467
        return []
 
468
 
 
469
    def _resolve(self, tt, file_id, path, winner):
 
470
        """Resolve the conflict.
 
471
 
 
472
        :param tt: The TreeTransform where the conflict is resolved.
 
473
        :param file_id: The retained file id.
 
474
        :param path: The retained path.
 
475
        :param winner: 'this' or 'other' indicates which side is the winner.
 
476
        """
 
477
        path_to_create = None
 
478
        if winner == 'this':
 
479
            if self.path == '<deleted>':
 
480
                return # Nothing to do
 
481
            if self.conflict_path == '<deleted>':
 
482
                path_to_create = self.path
 
483
                revid = tt._tree.get_parent_ids()[0]
 
484
        elif winner == 'other':
 
485
            if self.conflict_path == '<deleted>':
 
486
                return  # Nothing to do
 
487
            if self.path == '<deleted>':
 
488
                path_to_create = self.conflict_path
 
489
                # FIXME: If there are more than two parents we may need to
 
490
                # iterate. Taking the last parent is the safer bet in the mean
 
491
                # time. -- vila 20100309
 
492
                revid = tt._tree.get_parent_ids()[-1]
 
493
        else:
 
494
            # Programmer error
 
495
            raise AssertionError('bad winner: %r' % (winner,))
 
496
        if path_to_create is not None:
 
497
            tid = tt.trans_id_tree_path(path_to_create)
 
498
            transform.create_from_tree(
 
499
                tt, tt.trans_id_tree_path(path_to_create),
 
500
                self._revision_tree(tt._tree, revid), file_id)
 
501
            tt.version_file(file_id, tid)
 
502
 
 
503
        # Adjust the path for the retained file id
 
504
        tid = tt.trans_id_file_id(file_id)
 
505
        parent_tid = tt.get_tree_parent(tid)
 
506
        tt.adjust_path(path, parent_tid, tid)
 
507
        tt.apply()
 
508
 
 
509
    def _revision_tree(self, tree, revid):
 
510
        return tree.branch.repository.revision_tree(revid)
 
511
 
 
512
    def _infer_file_id(self, tree):
 
513
        # Prior to bug #531967, file_id wasn't always set, there may still be
 
514
        # conflict files in the wild so we need to cope with them
 
515
        # Establish which path we should use to find back the file-id
 
516
        possible_paths = []
 
517
        for p in (self.path, self.conflict_path):
 
518
            if p == '<deleted>':
 
519
                # special hard-coded path 
 
520
                continue
 
521
            if p is not None:
 
522
                possible_paths.append(p)
 
523
        # Search the file-id in the parents with any path available
 
524
        file_id = None
 
525
        for revid in tree.get_parent_ids():
 
526
            revtree = self._revision_tree(tree, revid)
 
527
            for p in possible_paths:
 
528
                file_id = revtree.path2id(p)
 
529
                if file_id is not None:
 
530
                    return revtree, file_id
 
531
        return None, None
 
532
 
 
533
    def action_take_this(self, tree):
 
534
        if self.file_id is not None:
 
535
            self._resolve_with_cleanups(tree, self.file_id, self.path,
 
536
                                        winner='this')
 
537
        else:
 
538
            # Prior to bug #531967 we need to find back the file_id and restore
 
539
            # the content from there
 
540
            revtree, file_id = self._infer_file_id(tree)
 
541
            tree.revert([revtree.id2path(file_id)],
 
542
                        old_tree=revtree, backups=False)
 
543
 
 
544
    def action_take_other(self, tree):
 
545
        if self.file_id is not None:
 
546
            self._resolve_with_cleanups(tree, self.file_id,
 
547
                                        self.conflict_path,
 
548
                                        winner='other')
 
549
        else:
 
550
            # Prior to bug #531967 we need to find back the file_id and restore
 
551
            # the content from there
 
552
            revtree, file_id = self._infer_file_id(tree)
 
553
            tree.revert([revtree.id2path(file_id)],
 
554
                        old_tree=revtree, backups=False)
 
555
 
379
556
 
380
557
class ContentsConflict(PathConflict):
381
 
    """The files are of different types, or not present"""
 
558
    """The files are of different types (or both binary), or not present"""
382
559
 
383
560
    has_files = True
384
561
 
386
563
 
387
564
    format = 'Contents conflict in %(path)s'
388
565
 
389
 
 
 
566
    def associated_filenames(self):
 
567
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
 
568
 
 
569
    def _resolve(self, tt, suffix_to_remove):
 
570
        """Resolve the conflict.
 
571
 
 
572
        :param tt: The TreeTransform where the conflict is resolved.
 
573
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
 
574
 
 
575
        The resolution is symmetric, when taking THIS, OTHER is deleted and
 
576
        item.THIS is renamed into item and vice-versa.
 
577
        """
 
578
        try:
 
579
            # Delete 'item.THIS' or 'item.OTHER' depending on
 
580
            # suffix_to_remove
 
581
            tt.delete_contents(
 
582
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
 
583
        except errors.NoSuchFile:
 
584
            # There are valid cases where 'item.suffix_to_remove' either
 
585
            # never existed or was already deleted (including the case
 
586
            # where the user deleted it)
 
587
            pass
 
588
        # Rename 'item.suffix_to_remove' (note that if
 
589
        # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
590
        this_tid = tt.trans_id_file_id(self.file_id)
 
591
        parent_tid = tt.get_tree_parent(this_tid)
 
592
        tt.adjust_path(self.path, parent_tid, this_tid)
 
593
        tt.apply()
 
594
 
 
595
    def action_take_this(self, tree):
 
596
        self._resolve_with_cleanups(tree, 'OTHER')
 
597
 
 
598
    def action_take_other(self, tree):
 
599
        self._resolve_with_cleanups(tree, 'THIS')
 
600
 
 
601
 
 
602
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
 
603
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
 
604
 
 
605
# TODO: There should be a base revid attribute to better inform the user about
 
606
# how the conflicts were generated.
390
607
class TextConflict(PathConflict):
391
608
    """The merge algorithm could not resolve all differences encountered."""
392
609
 
396
613
 
397
614
    format = 'Text conflict in %(path)s'
398
615
 
 
616
    def associated_filenames(self):
 
617
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
618
 
399
619
 
400
620
class HandledConflict(Conflict):
401
621
    """A path problem that has been provisionally resolved.
403
623
    """
404
624
 
405
625
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
406
 
    
 
626
 
407
627
    def __init__(self, action, path, file_id=None):
408
628
        Conflict.__init__(self, path, file_id)
409
629
        self.action = action
416
636
        s.add('action', self.action)
417
637
        return s
418
638
 
 
639
    def associated_filenames(self):
 
640
        # Nothing has been generated here
 
641
        return []
 
642
 
419
643
 
420
644
class HandledPathConflict(HandledConflict):
421
645
    """A provisionally-resolved path problem involving two paths.
428
652
    def __init__(self, action, path, conflict_path, file_id=None,
429
653
                 conflict_file_id=None):
430
654
        HandledConflict.__init__(self, action, path, file_id)
431
 
        self.conflict_path = conflict_path 
 
655
        self.conflict_path = conflict_path
432
656
        # warn turned off, because the factory blindly transfers the Stanza
433
657
        # values to __init__.
434
658
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
435
659
                                                     warn=False)
436
 
        
 
660
 
437
661
    def _cmp_list(self):
438
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
662
        return HandledConflict._cmp_list(self) + [self.conflict_path,
439
663
                                                  self.conflict_file_id]
440
664
 
441
665
    def as_stanza(self):
443
667
        s.add('conflict_path', self.conflict_path)
444
668
        if self.conflict_file_id is not None:
445
669
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
446
 
            
 
670
 
447
671
        return s
448
672
 
449
673
 
462
686
 
463
687
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
464
688
 
 
689
    def action_take_this(self, tree):
 
690
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
691
        tree.rename_one(self.path, self.conflict_path)
 
692
 
 
693
    def action_take_other(self, tree):
 
694
        tree.remove([self.path], force=True, keep_files=False)
 
695
 
465
696
 
466
697
class ParentLoop(HandledPathConflict):
467
698
    """An attempt to create an infinitely-looping directory structure.
468
699
    This is rare, but can be produced like so:
469
700
 
470
701
    tree A:
471
 
      mv foo/bar
 
702
      mv foo bar
472
703
    tree B:
473
 
      mv bar/foo
 
704
      mv bar foo
474
705
    merge A and B
475
706
    """
476
707
 
477
708
    typestring = 'parent loop'
478
709
 
479
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
710
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
711
 
 
712
    def action_take_this(self, tree):
 
713
        # just acccept bzr proposal
 
714
        pass
 
715
 
 
716
    def action_take_other(self, tree):
 
717
        # FIXME: We shouldn't have to manipulate so many paths here (and there
 
718
        # is probably a bug or two...)
 
719
        base_path = osutils.basename(self.path)
 
720
        conflict_base_path = osutils.basename(self.conflict_path)
 
721
        tt = transform.TreeTransform(tree)
 
722
        try:
 
723
            p_tid = tt.trans_id_file_id(self.file_id)
 
724
            parent_tid = tt.get_tree_parent(p_tid)
 
725
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
 
726
            cparent_tid = tt.get_tree_parent(cp_tid)
 
727
            tt.adjust_path(base_path, cparent_tid, cp_tid)
 
728
            tt.adjust_path(conflict_base_path, parent_tid, p_tid)
 
729
            tt.apply()
 
730
        finally:
 
731
            tt.finalize()
480
732
 
481
733
 
482
734
class UnversionedParent(HandledConflict):
483
 
    """An attempt to version an file whose parent directory is not versioned.
 
735
    """An attempt to version a file whose parent directory is not versioned.
484
736
    Typically, the result of a merge where one tree unversioned the directory
485
737
    and the other added a versioned file to it.
486
738
    """
490
742
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
491
743
             ' children.  %(action)s.'
492
744
 
 
745
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
746
    # conflict shouldn't exist (the long story is that the conflict is
 
747
    # generated with another one that can be resolved properly) -- vila 091224
 
748
    def action_take_this(self, tree):
 
749
        pass
 
750
 
 
751
    def action_take_other(self, tree):
 
752
        pass
 
753
 
493
754
 
494
755
class MissingParent(HandledConflict):
495
756
    """An attempt to add files to a directory that is not present.
496
757
    Typically, the result of a merge where THIS deleted the directory and
497
758
    the OTHER added a file to it.
498
 
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
759
    See also: DeletingParent (same situation, THIS and OTHER reversed)
499
760
    """
500
761
 
501
762
    typestring = 'missing parent'
502
763
 
503
764
    format = 'Conflict adding files to %(path)s.  %(action)s.'
504
765
 
 
766
    def action_take_this(self, tree):
 
767
        tree.remove([self.path], force=True, keep_files=False)
 
768
 
 
769
    def action_take_other(self, tree):
 
770
        # just acccept bzr proposal
 
771
        pass
 
772
 
505
773
 
506
774
class DeletingParent(HandledConflict):
507
775
    """An attempt to add files to a directory that is not present.
514
782
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
515
783
             "%(action)s."
516
784
 
 
785
    # FIXME: It's a bit strange that the default action is not coherent with
 
786
    # MissingParent from the *user* pov.
 
787
 
 
788
    def action_take_this(self, tree):
 
789
        # just acccept bzr proposal
 
790
        pass
 
791
 
 
792
    def action_take_other(self, tree):
 
793
        tree.remove([self.path], force=True, keep_files=False)
 
794
 
517
795
 
518
796
class NonDirectoryParent(HandledConflict):
519
 
    """An attempt to add files to a directory that is not a director or
 
797
    """An attempt to add files to a directory that is not a directory or
520
798
    an attempt to change the kind of a directory with files.
521
799
    """
522
800
 
525
803
    format = "Conflict: %(path)s is not a directory, but has files in it."\
526
804
             "  %(action)s."
527
805
 
 
806
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
807
 
 
808
    def action_take_this(self, tree):
 
809
        # FIXME: we should preserve that path when the conflict is generated !
 
810
        if self.path.endswith('.new'):
 
811
            conflict_path = self.path[:-(len('.new'))]
 
812
            tree.remove([self.path], force=True, keep_files=False)
 
813
            tree.add(conflict_path)
 
814
        else:
 
815
            raise NotImplementedError(self.action_take_this)
 
816
 
 
817
    def action_take_other(self, tree):
 
818
        # FIXME: we should preserve that path when the conflict is generated !
 
819
        if self.path.endswith('.new'):
 
820
            conflict_path = self.path[:-(len('.new'))]
 
821
            tree.remove([conflict_path], force=True, keep_files=False)
 
822
            tree.rename_one(self.path, conflict_path)
 
823
        else:
 
824
            raise NotImplementedError(self.action_take_other)
 
825
 
 
826
 
528
827
ctype = {}
529
828
 
530
829
 
534
833
    for conflict_type in conflict_types:
535
834
        ctype[conflict_type.typestring] = conflict_type
536
835
 
537
 
 
538
836
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
539
837
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
540
838
               DeletingParent, NonDirectoryParent)