/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/conflicts.py

  • Committer: Jelmer Vernooij
  • Date: 2017-07-23 22:06:41 UTC
  • mfrom: (6738 trunk)
  • mto: This revision was merged to the branch mainline in revision 6739.
  • Revision ID: jelmer@jelmer.uk-20170723220641-69eczax9bmv8d6kk
Merge trunk, address review comments.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
# TODO: 'brz resolve' should accept a directory name and work from that
18
18
# point down
19
19
 
20
 
import errno
 
20
from __future__ import absolute_import
 
21
 
21
22
import os
22
 
import re
23
23
 
24
24
from .lazy_import import lazy_import
25
25
lazy_import(globals(), """
 
26
import errno
26
27
 
27
28
from breezy import (
 
29
    cleanup,
 
30
    errors,
 
31
    osutils,
 
32
    rio,
 
33
    trace,
 
34
    transform,
28
35
    workingtree,
29
36
    )
30
37
from breezy.i18n import gettext, ngettext
31
38
""")
32
39
from . import (
33
40
    cache_utf8,
34
 
    errors,
35
41
    commands,
36
42
    option,
37
 
    osutils,
38
43
    registry,
39
 
    trace,
40
44
    )
41
45
 
42
46
 
 
47
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
48
 
 
49
 
43
50
class cmd_conflicts(commands.Command):
44
51
    __doc__ = """List files with conflicts.
45
52
 
55
62
    Use brz resolve when you have fixed a problem.
56
63
    """
57
64
    takes_options = [
58
 
        'directory',
59
 
        option.Option('text',
60
 
                      help='List paths of files with text conflicts.'),
 
65
            'directory',
 
66
            option.Option('text',
 
67
                          help='List paths of files with text conflicts.'),
61
68
        ]
62
69
    _see_also = ['resolve', 'conflict-types']
63
70
 
69
76
                    continue
70
77
                self.outf.write(conflict.path + '\n')
71
78
            else:
72
 
                self.outf.write(str(conflict) + '\n')
 
79
                self.outf.write(unicode(conflict) + '\n')
73
80
 
74
81
 
75
82
resolve_action_registry = registry.Registry()
76
83
 
77
84
 
78
85
resolve_action_registry.register(
79
 
    'auto', 'auto', 'Detect whether conflict has been resolved by user.')
80
 
resolve_action_registry.register(
81
86
    'done', 'done', 'Marks the conflict as resolved.')
82
87
resolve_action_registry.register(
83
88
    'take-this', 'take_this',
87
92
    'Resolve the conflict taking the merged version into account.')
88
93
resolve_action_registry.default_key = 'done'
89
94
 
90
 
 
91
95
class ResolveActionOption(option.RegistryOption):
92
96
 
93
97
    def __init__(self):
112
116
    aliases = ['resolved']
113
117
    takes_args = ['file*']
114
118
    takes_options = [
115
 
        'directory',
116
 
        option.Option('all', help='Resolve all conflicts in this tree.'),
117
 
        ResolveActionOption(),
118
 
        ]
 
119
            'directory',
 
120
            option.Option('all', help='Resolve all conflicts in this tree.'),
 
121
            ResolveActionOption(),
 
122
            ]
119
123
    _see_also = ['conflicts']
120
 
 
121
124
    def run(self, file_list=None, all=False, action=None, directory=None):
122
125
        if all:
123
126
            if file_list:
124
 
                raise errors.CommandError(gettext("If --all is specified,"
125
 
                                                  " no FILE may be provided"))
 
127
                raise errors.BzrCommandError(gettext("If --all is specified,"
 
128
                                             " no FILE may be provided"))
126
129
            if directory is None:
127
130
                directory = u'.'
128
131
            tree = workingtree.WorkingTree.open_containing(directory)[0]
131
134
        else:
132
135
            tree, file_list = workingtree.WorkingTree.open_containing_paths(
133
136
                file_list, directory)
134
 
            if action is None:
135
 
                if file_list is None:
 
137
            if file_list is None:
 
138
                if action is None:
 
139
                    # FIXME: There is a special case here related to the option
 
140
                    # handling that could be clearer and easier to discover by
 
141
                    # providing an --auto action (bug #344013 and #383396) and
 
142
                    # make it mandatory instead of implicit and active only
 
143
                    # when no file_list is provided -- vila 091229
136
144
                    action = 'auto'
137
 
                else:
 
145
            else:
 
146
                if action is None:
138
147
                    action = 'done'
139
 
        before, after = resolve(tree, file_list, action=action)
140
 
        # GZ 2012-07-27: Should unify UI below now that auto is less magical.
141
 
        if action == 'auto' and file_list is None:
142
 
            if after > 0:
143
 
                trace.note(
144
 
                    ngettext('%d conflict auto-resolved.',
145
 
                             '%d conflicts auto-resolved.', before - after),
146
 
                    before - after)
147
 
                trace.note(gettext('Remaining conflicts:'))
148
 
                for conflict in tree.conflicts():
149
 
                    trace.note(str(conflict))
150
 
                return 1
 
148
        if action == 'auto':
 
149
            if file_list is None:
 
150
                un_resolved, resolved = tree.auto_resolve()
 
151
                if len(un_resolved) > 0:
 
152
                    trace.note(ngettext('%d conflict auto-resolved.',
 
153
                        '%d conflicts auto-resolved.', len(resolved)),
 
154
                        len(resolved))
 
155
                    trace.note(gettext('Remaining conflicts:'))
 
156
                    for conflict in un_resolved:
 
157
                        trace.note(unicode(conflict))
 
158
                    return 1
 
159
                else:
 
160
                    trace.note(gettext('All conflicts resolved.'))
 
161
                    return 0
151
162
            else:
152
 
                trace.note(gettext('All conflicts resolved.'))
153
 
                return 0
 
163
                # FIXME: This can never occur but the block above needs some
 
164
                # refactoring to transfer tree.auto_resolve() to
 
165
                # conflict.auto(tree) --vila 091242
 
166
                pass
154
167
        else:
 
168
            before, after = resolve(tree, file_list, action=action)
155
169
            trace.note(ngettext('{0} conflict resolved, {1} remaining',
156
170
                                '{0} conflicts resolved, {1} remaining',
157
 
                                before - after).format(before - after, after))
 
171
                                before-after).format(before - after, after))
158
172
 
159
173
 
160
174
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
172
186
        paths do not have conflicts.
173
187
    :param action: How the conflict should be resolved,
174
188
    """
 
189
    tree.lock_tree_write()
175
190
    nb_conflicts_after = None
176
 
    with tree.lock_tree_write():
 
191
    try:
177
192
        tree_conflicts = tree.conflicts()
178
193
        nb_conflicts_before = len(tree_conflicts)
179
194
        if paths is None:
180
 
            new_conflicts = []
 
195
            new_conflicts = ConflictList()
181
196
            to_process = tree_conflicts
182
197
        else:
183
198
            new_conflicts, to_process = tree_conflicts.select_conflicts(
184
199
                tree, paths, ignore_misses, recursive)
185
200
        for conflict in to_process:
186
201
            try:
187
 
                conflict.do(action, tree)
 
202
                conflict._do(action, tree)
188
203
                conflict.cleanup(tree)
189
204
            except NotImplementedError:
190
205
                new_conflicts.append(conflict)
193
208
            tree.set_conflicts(new_conflicts)
194
209
        except errors.UnsupportedOperation:
195
210
            pass
 
211
    finally:
 
212
        tree.unlock()
196
213
    if nb_conflicts_after is None:
197
214
        nb_conflicts_after = nb_conflicts_before
198
215
    return nb_conflicts_before, nb_conflicts_after
230
247
    """List of conflicts.
231
248
 
232
249
    Typically obtained from WorkingTree.conflicts()
 
250
 
 
251
    Can be instantiated from stanzas or from Conflict subclasses.
233
252
    """
234
253
 
235
254
    def __init__(self, conflicts=None):
263
282
    def __repr__(self):
264
283
        return "ConflictList(%r)" % self.__list
265
284
 
 
285
    @staticmethod
 
286
    def from_stanzas(stanzas):
 
287
        """Produce a new ConflictList from an iterable of stanzas"""
 
288
        conflicts = ConflictList()
 
289
        for stanza in stanzas:
 
290
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
291
        return conflicts
 
292
 
 
293
    def to_stanzas(self):
 
294
        """Generator of stanzas"""
 
295
        for conflict in self:
 
296
            yield conflict.as_stanza()
 
297
 
266
298
    def to_strings(self):
267
299
        """Generate strings for the provided conflicts"""
268
300
        for conflict in self:
269
 
            yield str(conflict)
 
301
            yield unicode(conflict)
270
302
 
271
303
    def remove_files(self, tree):
272
304
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
279
311
                         recurse=False):
280
312
        """Select the conflicts associated with paths in a tree.
281
313
 
 
314
        File-ids are also used for this.
282
315
        :return: a pair of ConflictLists: (not_selected, selected)
283
316
        """
284
317
        path_set = set(paths)
 
318
        ids = {}
285
319
        selected_paths = set()
286
320
        new_conflicts = ConflictList()
287
321
        selected_conflicts = ConflictList()
 
322
        for path in paths:
 
323
            file_id = tree.path2id(path)
 
324
            if file_id is not None:
 
325
                ids[file_id] = path
288
326
 
289
327
        for conflict in self:
290
328
            selected = False
291
 
            if conflict.path in path_set:
 
329
            for key in ('path', 'conflict_path'):
 
330
                cpath = getattr(conflict, key, None)
 
331
                if cpath is None:
 
332
                    continue
 
333
                if cpath in path_set:
 
334
                    selected = True
 
335
                    selected_paths.add(cpath)
 
336
                if recurse:
 
337
                    if osutils.is_inside_any(path_set, cpath):
 
338
                        selected = True
 
339
                        selected_paths.add(cpath)
 
340
 
 
341
            for key in ('file_id', 'conflict_file_id'):
 
342
                cfile_id = getattr(conflict, key, None)
 
343
                if cfile_id is None:
 
344
                    continue
 
345
                try:
 
346
                    cpath = ids[cfile_id]
 
347
                except KeyError:
 
348
                    continue
292
349
                selected = True
293
 
                selected_paths.add(conflict.path)
294
 
            if recurse:
295
 
                if osutils.is_inside_any(path_set, conflict.path):
296
 
                    selected = True
297
 
                    selected_paths.add(conflict.path)
298
 
 
 
350
                selected_paths.add(cpath)
299
351
            if selected:
300
352
                selected_conflicts.append(conflict)
301
353
            else:
310
362
 
311
363
 
312
364
class Conflict(object):
313
 
    """Base class for conflicts."""
314
 
 
315
 
    typestring = None
316
 
 
317
 
    def __init__(self, path):
 
365
    """Base class for all types of conflict"""
 
366
 
 
367
    # FIXME: cleanup should take care of that ? -- vila 091229
 
368
    has_files = False
 
369
 
 
370
    def __init__(self, path, file_id=None):
318
371
        self.path = path
 
372
        # the factory blindly transfers the Stanza values to __init__ and
 
373
        # Stanza is purely a Unicode api.
 
374
        if isinstance(file_id, unicode):
 
375
            file_id = cache_utf8.encode(file_id)
 
376
        self.file_id = osutils.safe_file_id(file_id)
 
377
 
 
378
    def as_stanza(self):
 
379
        s = rio.Stanza(type=self.typestring, path=self.path)
 
380
        if self.file_id is not None:
 
381
            # Stanza requires Unicode apis
 
382
            s.add('file_id', self.file_id.decode('utf8'))
 
383
        return s
 
384
 
 
385
    def _cmp_list(self):
 
386
        return [type(self), self.path, self.file_id]
 
387
 
 
388
    def __cmp__(self, other):
 
389
        if getattr(other, "_cmp_list", None) is None:
 
390
            return -1
 
391
        return cmp(self._cmp_list(), other._cmp_list())
 
392
 
 
393
    def __hash__(self):
 
394
        return hash((type(self), self.path, self.file_id))
 
395
 
 
396
    def __eq__(self, other):
 
397
        return self.__cmp__(other) == 0
 
398
 
 
399
    def __ne__(self, other):
 
400
        return not self.__eq__(other)
 
401
 
 
402
    def __unicode__(self):
 
403
        return self.format % self.__dict__
 
404
 
 
405
    def __repr__(self):
 
406
        rdict = dict(self.__dict__)
 
407
        rdict['class'] = self.__class__.__name__
 
408
        return self.rformat % rdict
 
409
 
 
410
    @staticmethod
 
411
    def factory(type, **kwargs):
 
412
        global ctype
 
413
        return ctype[type](**kwargs)
 
414
 
 
415
    @staticmethod
 
416
    def sort_key(conflict):
 
417
        if conflict.path is not None:
 
418
            return conflict.path, conflict.typestring
 
419
        elif getattr(conflict, "conflict_path", None) is not None:
 
420
            return conflict.conflict_path, conflict.typestring
 
421
        else:
 
422
            return None, conflict.typestring
 
423
 
 
424
    def _do(self, action, tree):
 
425
        """Apply the specified action to the conflict.
 
426
 
 
427
        :param action: The method name to call.
 
428
 
 
429
        :param tree: The tree passed as a parameter to the method.
 
430
        """
 
431
        meth = getattr(self, 'action_%s' % action, None)
 
432
        if meth is None:
 
433
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
 
434
        meth(tree)
319
435
 
320
436
    def associated_filenames(self):
321
437
        """The names of the files generated to help resolve the conflict."""
329
445
                if e.errno != errno.ENOENT:
330
446
                    raise
331
447
 
332
 
    def do(self, action, tree):
333
 
        """Apply the specified action to the conflict.
334
 
 
335
 
        :param action: The method name to call.
336
 
 
337
 
        :param tree: The tree passed as a parameter to the method.
338
 
        """
339
 
        raise NotImplementedError(self.do)
340
 
 
341
 
    def describe(self):
342
 
        """Return a string description of this conflict."""
343
 
        raise NotImplementedError(self.describe)
 
448
    def action_done(self, tree):
 
449
        """Mark the conflict as solved once it has been handled."""
 
450
        # This method does nothing but simplifies the design of upper levels.
 
451
        pass
 
452
 
 
453
    def action_take_this(self, tree):
 
454
        raise NotImplementedError(self.action_take_this)
 
455
 
 
456
    def action_take_other(self, tree):
 
457
        raise NotImplementedError(self.action_take_other)
 
458
 
 
459
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
 
460
        tt = transform.TreeTransform(tree)
 
461
        op = cleanup.OperationWithCleanups(self._resolve)
 
462
        op.add_cleanup(tt.finalize)
 
463
        op.run_simple(tt, *args, **kwargs)
 
464
 
 
465
 
 
466
class PathConflict(Conflict):
 
467
    """A conflict was encountered merging file paths"""
 
468
 
 
469
    typestring = 'path conflict'
 
470
 
 
471
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
472
 
 
473
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
474
 
 
475
    def __init__(self, path, conflict_path=None, file_id=None):
 
476
        Conflict.__init__(self, path, file_id)
 
477
        self.conflict_path = conflict_path
 
478
 
 
479
    def as_stanza(self):
 
480
        s = Conflict.as_stanza(self)
 
481
        if self.conflict_path is not None:
 
482
            s.add('conflict_path', self.conflict_path)
 
483
        return s
 
484
 
 
485
    def associated_filenames(self):
 
486
        # No additional files have been generated here
 
487
        return []
 
488
 
 
489
    def _resolve(self, tt, file_id, path, winner):
 
490
        """Resolve the conflict.
 
491
 
 
492
        :param tt: The TreeTransform where the conflict is resolved.
 
493
        :param file_id: The retained file id.
 
494
        :param path: The retained path.
 
495
        :param winner: 'this' or 'other' indicates which side is the winner.
 
496
        """
 
497
        path_to_create = None
 
498
        if winner == 'this':
 
499
            if self.path == '<deleted>':
 
500
                return # Nothing to do
 
501
            if self.conflict_path == '<deleted>':
 
502
                path_to_create = self.path
 
503
                revid = tt._tree.get_parent_ids()[0]
 
504
        elif winner == 'other':
 
505
            if self.conflict_path == '<deleted>':
 
506
                return  # Nothing to do
 
507
            if self.path == '<deleted>':
 
508
                path_to_create = self.conflict_path
 
509
                # FIXME: If there are more than two parents we may need to
 
510
                # iterate. Taking the last parent is the safer bet in the mean
 
511
                # time. -- vila 20100309
 
512
                revid = tt._tree.get_parent_ids()[-1]
 
513
        else:
 
514
            # Programmer error
 
515
            raise AssertionError('bad winner: %r' % (winner,))
 
516
        if path_to_create is not None:
 
517
            tid = tt.trans_id_tree_path(path_to_create)
 
518
            transform.create_from_tree(
 
519
                tt, tid, self._revision_tree(tt._tree, revid), file_id)
 
520
            tt.version_file(file_id, tid)
 
521
        else:
 
522
            tid = tt.trans_id_file_id(file_id)
 
523
        # Adjust the path for the retained file id
 
524
        parent_tid = tt.get_tree_parent(tid)
 
525
        tt.adjust_path(osutils.basename(path), parent_tid, tid)
 
526
        tt.apply()
 
527
 
 
528
    def _revision_tree(self, tree, revid):
 
529
        return tree.branch.repository.revision_tree(revid)
 
530
 
 
531
    def _infer_file_id(self, tree):
 
532
        # Prior to bug #531967, file_id wasn't always set, there may still be
 
533
        # conflict files in the wild so we need to cope with them
 
534
        # Establish which path we should use to find back the file-id
 
535
        possible_paths = []
 
536
        for p in (self.path, self.conflict_path):
 
537
            if p == '<deleted>':
 
538
                # special hard-coded path 
 
539
                continue
 
540
            if p is not None:
 
541
                possible_paths.append(p)
 
542
        # Search the file-id in the parents with any path available
 
543
        file_id = None
 
544
        for revid in tree.get_parent_ids():
 
545
            revtree = self._revision_tree(tree, revid)
 
546
            for p in possible_paths:
 
547
                file_id = revtree.path2id(p)
 
548
                if file_id is not None:
 
549
                    return revtree, file_id
 
550
        return None, None
 
551
 
 
552
    def action_take_this(self, tree):
 
553
        if self.file_id is not None:
 
554
            self._resolve_with_cleanups(tree, self.file_id, self.path,
 
555
                                        winner='this')
 
556
        else:
 
557
            # Prior to bug #531967 we need to find back the file_id and restore
 
558
            # the content from there
 
559
            revtree, file_id = self._infer_file_id(tree)
 
560
            tree.revert([revtree.id2path(file_id)],
 
561
                        old_tree=revtree, backups=False)
 
562
 
 
563
    def action_take_other(self, tree):
 
564
        if self.file_id is not None:
 
565
            self._resolve_with_cleanups(tree, self.file_id,
 
566
                                        self.conflict_path,
 
567
                                        winner='other')
 
568
        else:
 
569
            # Prior to bug #531967 we need to find back the file_id and restore
 
570
            # the content from there
 
571
            revtree, file_id = self._infer_file_id(tree)
 
572
            tree.revert([revtree.id2path(file_id)],
 
573
                        old_tree=revtree, backups=False)
 
574
 
 
575
 
 
576
class ContentsConflict(PathConflict):
 
577
    """The files are of different types (or both binary), or not present"""
 
578
 
 
579
    has_files = True
 
580
 
 
581
    typestring = 'contents conflict'
 
582
 
 
583
    format = 'Contents conflict in %(path)s'
 
584
 
 
585
    def associated_filenames(self):
 
586
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
 
587
 
 
588
    def _resolve(self, tt, suffix_to_remove):
 
589
        """Resolve the conflict.
 
590
 
 
591
        :param tt: The TreeTransform where the conflict is resolved.
 
592
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
 
593
 
 
594
        The resolution is symmetric: when taking THIS, OTHER is deleted and
 
595
        item.THIS is renamed into item and vice-versa.
 
596
        """
 
597
        try:
 
598
            # Delete 'item.THIS' or 'item.OTHER' depending on
 
599
            # suffix_to_remove
 
600
            tt.delete_contents(
 
601
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
 
602
        except errors.NoSuchFile:
 
603
            # There are valid cases where 'item.suffix_to_remove' either
 
604
            # never existed or was already deleted (including the case
 
605
            # where the user deleted it)
 
606
            pass
 
607
        try:
 
608
            this_path = tt._tree.id2path(self.file_id)
 
609
        except errors.NoSuchId:
 
610
            # The file is not present anymore. This may happen if the user
 
611
            # deleted the file either manually or when resolving a conflict on
 
612
            # the parent.  We may raise some exception to indicate that the
 
613
            # conflict doesn't exist anymore and as such doesn't need to be
 
614
            # resolved ? -- vila 20110615 
 
615
            this_tid = None
 
616
        else:
 
617
            this_tid = tt.trans_id_tree_path(this_path)
 
618
        if this_tid is not None:
 
619
            # Rename 'item.suffix_to_remove' (note that if
 
620
            # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
621
            parent_tid = tt.get_tree_parent(this_tid)
 
622
            tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
 
623
            tt.apply()
 
624
 
 
625
    def action_take_this(self, tree):
 
626
        self._resolve_with_cleanups(tree, 'OTHER')
 
627
 
 
628
    def action_take_other(self, tree):
 
629
        self._resolve_with_cleanups(tree, 'THIS')
 
630
 
 
631
 
 
632
# TODO: There should be a base revid attribute to better inform the user about
 
633
# how the conflicts were generated.
 
634
class TextConflict(Conflict):
 
635
    """The merge algorithm could not resolve all differences encountered."""
 
636
 
 
637
    has_files = True
 
638
 
 
639
    typestring = 'text conflict'
 
640
 
 
641
    format = 'Text conflict in %(path)s'
 
642
 
 
643
    rformat = '%(class)s(%(path)r, %(file_id)r)'
 
644
 
 
645
    def associated_filenames(self):
 
646
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
647
 
 
648
    def _resolve(self, tt, winner_suffix):
 
649
        """Resolve the conflict by copying one of .THIS or .OTHER into file.
 
650
 
 
651
        :param tt: The TreeTransform where the conflict is resolved.
 
652
        :param winner_suffix: Either 'THIS' or 'OTHER'
 
653
 
 
654
        The resolution is symmetric, when taking THIS, item.THIS is renamed
 
655
        into item and vice-versa. This takes one of the files as a whole
 
656
        ignoring every difference that could have been merged cleanly.
 
657
        """
 
658
        # To avoid useless copies, we switch item and item.winner_suffix, only
 
659
        # item will exist after the conflict has been resolved anyway.
 
660
        item_tid = tt.trans_id_file_id(self.file_id)
 
661
        item_parent_tid = tt.get_tree_parent(item_tid)
 
662
        winner_path = self.path + '.' + winner_suffix
 
663
        winner_tid = tt.trans_id_tree_path(winner_path)
 
664
        winner_parent_tid = tt.get_tree_parent(winner_tid)
 
665
        # Switch the paths to preserve the content
 
666
        tt.adjust_path(osutils.basename(self.path),
 
667
                       winner_parent_tid, winner_tid)
 
668
        tt.adjust_path(osutils.basename(winner_path), item_parent_tid, item_tid)
 
669
        # Associate the file_id to the right content
 
670
        tt.unversion_file(item_tid)
 
671
        tt.version_file(self.file_id, winner_tid)
 
672
        tt.apply()
 
673
 
 
674
    def action_take_this(self, tree):
 
675
        self._resolve_with_cleanups(tree, 'THIS')
 
676
 
 
677
    def action_take_other(self, tree):
 
678
        self._resolve_with_cleanups(tree, 'OTHER')
 
679
 
 
680
 
 
681
class HandledConflict(Conflict):
 
682
    """A path problem that has been provisionally resolved.
 
683
    This is intended to be a base class.
 
684
    """
 
685
 
 
686
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
687
 
 
688
    def __init__(self, action, path, file_id=None):
 
689
        Conflict.__init__(self, path, file_id)
 
690
        self.action = action
 
691
 
 
692
    def _cmp_list(self):
 
693
        return Conflict._cmp_list(self) + [self.action]
 
694
 
 
695
    def as_stanza(self):
 
696
        s = Conflict.as_stanza(self)
 
697
        s.add('action', self.action)
 
698
        return s
 
699
 
 
700
    def associated_filenames(self):
 
701
        # Nothing has been generated here
 
702
        return []
 
703
 
 
704
 
 
705
class HandledPathConflict(HandledConflict):
 
706
    """A provisionally-resolved path problem involving two paths.
 
707
    This is intended to be a base class.
 
708
    """
 
709
 
 
710
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
711
        " %(file_id)r, %(conflict_file_id)r)"
 
712
 
 
713
    def __init__(self, action, path, conflict_path, file_id=None,
 
714
                 conflict_file_id=None):
 
715
        HandledConflict.__init__(self, action, path, file_id)
 
716
        self.conflict_path = conflict_path
 
717
        # the factory blindly transfers the Stanza values to __init__,
 
718
        # so they can be unicode.
 
719
        if isinstance(conflict_file_id, unicode):
 
720
            conflict_file_id = cache_utf8.encode(conflict_file_id)
 
721
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
 
722
 
 
723
    def _cmp_list(self):
 
724
        return HandledConflict._cmp_list(self) + [self.conflict_path,
 
725
                                                  self.conflict_file_id]
 
726
 
 
727
    def as_stanza(self):
 
728
        s = HandledConflict.as_stanza(self)
 
729
        s.add('conflict_path', self.conflict_path)
 
730
        if self.conflict_file_id is not None:
 
731
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
732
 
 
733
        return s
 
734
 
 
735
 
 
736
class DuplicateID(HandledPathConflict):
 
737
    """Two files want the same file_id."""
 
738
 
 
739
    typestring = 'duplicate id'
 
740
 
 
741
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
742
 
 
743
 
 
744
class DuplicateEntry(HandledPathConflict):
 
745
    """Two directory entries want to have the same name."""
 
746
 
 
747
    typestring = 'duplicate'
 
748
 
 
749
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
750
 
 
751
    def action_take_this(self, tree):
 
752
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
753
        tree.rename_one(self.path, self.conflict_path)
 
754
 
 
755
    def action_take_other(self, tree):
 
756
        tree.remove([self.path], force=True, keep_files=False)
 
757
 
 
758
 
 
759
class ParentLoop(HandledPathConflict):
 
760
    """An attempt to create an infinitely-looping directory structure.
 
761
    This is rare, but can be produced like so:
 
762
 
 
763
    tree A:
 
764
      mv foo bar
 
765
    tree B:
 
766
      mv bar foo
 
767
    merge A and B
 
768
    """
 
769
 
 
770
    typestring = 'parent loop'
 
771
 
 
772
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
773
 
 
774
    def action_take_this(self, tree):
 
775
        # just acccept brz proposal
 
776
        pass
 
777
 
 
778
    def action_take_other(self, tree):
 
779
        tt = transform.TreeTransform(tree)
 
780
        try:
 
781
            p_tid = tt.trans_id_file_id(self.file_id)
 
782
            parent_tid = tt.get_tree_parent(p_tid)
 
783
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
 
784
            cparent_tid = tt.get_tree_parent(cp_tid)
 
785
            tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
 
786
            tt.adjust_path(osutils.basename(self.conflict_path),
 
787
                           parent_tid, p_tid)
 
788
            tt.apply()
 
789
        finally:
 
790
            tt.finalize()
 
791
 
 
792
 
 
793
class UnversionedParent(HandledConflict):
 
794
    """An attempt to version a file whose parent directory is not versioned.
 
795
    Typically, the result of a merge where one tree unversioned the directory
 
796
    and the other added a versioned file to it.
 
797
    """
 
798
 
 
799
    typestring = 'unversioned parent'
 
800
 
 
801
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
802
             ' children.  %(action)s.'
 
803
 
 
804
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
805
    # conflict shouldn't exist (the long story is that the conflict is
 
806
    # generated with another one that can be resolved properly) -- vila 091224
 
807
    def action_take_this(self, tree):
 
808
        pass
 
809
 
 
810
    def action_take_other(self, tree):
 
811
        pass
 
812
 
 
813
 
 
814
class MissingParent(HandledConflict):
 
815
    """An attempt to add files to a directory that is not present.
 
816
    Typically, the result of a merge where THIS deleted the directory and
 
817
    the OTHER added a file to it.
 
818
    See also: DeletingParent (same situation, THIS and OTHER reversed)
 
819
    """
 
820
 
 
821
    typestring = 'missing parent'
 
822
 
 
823
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
824
 
 
825
    def action_take_this(self, tree):
 
826
        tree.remove([self.path], force=True, keep_files=False)
 
827
 
 
828
    def action_take_other(self, tree):
 
829
        # just acccept brz proposal
 
830
        pass
 
831
 
 
832
 
 
833
class DeletingParent(HandledConflict):
 
834
    """An attempt to add files to a directory that is not present.
 
835
    Typically, the result of a merge where one OTHER deleted the directory and
 
836
    the THIS added a file to it.
 
837
    """
 
838
 
 
839
    typestring = 'deleting parent'
 
840
 
 
841
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
842
             "%(action)s."
 
843
 
 
844
    # FIXME: It's a bit strange that the default action is not coherent with
 
845
    # MissingParent from the *user* pov.
 
846
 
 
847
    def action_take_this(self, tree):
 
848
        # just acccept brz proposal
 
849
        pass
 
850
 
 
851
    def action_take_other(self, tree):
 
852
        tree.remove([self.path], force=True, keep_files=False)
 
853
 
 
854
 
 
855
class NonDirectoryParent(HandledConflict):
 
856
    """An attempt to add files to a directory that is not a directory or
 
857
    an attempt to change the kind of a directory with files.
 
858
    """
 
859
 
 
860
    typestring = 'non-directory parent'
 
861
 
 
862
    format = "Conflict: %(path)s is not a directory, but has files in it."\
 
863
             "  %(action)s."
 
864
 
 
865
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
866
 
 
867
    def action_take_this(self, tree):
 
868
        # FIXME: we should preserve that path when the conflict is generated !
 
869
        if self.path.endswith('.new'):
 
870
            conflict_path = self.path[:-(len('.new'))]
 
871
            tree.remove([self.path], force=True, keep_files=False)
 
872
            tree.add(conflict_path)
 
873
        else:
 
874
            raise NotImplementedError(self.action_take_this)
 
875
 
 
876
    def action_take_other(self, tree):
 
877
        # FIXME: we should preserve that path when the conflict is generated !
 
878
        if self.path.endswith('.new'):
 
879
            conflict_path = self.path[:-(len('.new'))]
 
880
            tree.remove([conflict_path], force=True, keep_files=False)
 
881
            tree.rename_one(self.path, conflict_path)
 
882
        else:
 
883
            raise NotImplementedError(self.action_take_other)
 
884
 
 
885
 
 
886
ctype = {}
 
887
 
 
888
 
 
889
def register_types(*conflict_types):
 
890
    """Register a Conflict subclass for serialization purposes"""
 
891
    global ctype
 
892
    for conflict_type in conflict_types:
 
893
        ctype[conflict_type.typestring] = conflict_type
 
894
 
 
895
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
896
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
897
               DeletingParent, NonDirectoryParent)