~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Vincent Ladeuil
  • Date: 2010-09-24 09:56:50 UTC
  • mto: This revision was merged to the branch mainline in revision 5446.
  • Revision ID: v.ladeuil+lp@free.fr-20100924095650-okd49n2o18q9zkmb
Clarify SRU bug nomination.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007, 2009, 2010 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
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
 
18
# point down
 
19
 
 
20
import os
 
21
 
 
22
from bzrlib.lazy_import import lazy_import
 
23
lazy_import(globals(), """
 
24
import errno
 
25
 
 
26
from bzrlib import (
 
27
    cleanup,
 
28
    commands,
 
29
    errors,
 
30
    osutils,
 
31
    rio,
 
32
    trace,
 
33
    transform,
 
34
    workingtree,
 
35
    )
 
36
""")
 
37
from bzrlib import (
 
38
    option,
 
39
    registry,
 
40
    )
 
41
 
 
42
 
 
43
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
44
 
 
45
 
 
46
class cmd_conflicts(commands.Command):
 
47
    __doc__ = """List files with conflicts.
 
48
 
 
49
    Merge will do its best to combine the changes in two branches, but there
 
50
    are some kinds of problems only a human can fix.  When it encounters those,
 
51
    it will mark a conflict.  A conflict means that you need to fix something,
 
52
    before you should commit.
 
53
 
 
54
    Conflicts normally are listed as short, human-readable messages.  If --text
 
55
    is supplied, the pathnames of files with text conflicts are listed,
 
56
    instead.  (This is useful for editing all files with text conflicts.)
 
57
 
 
58
    Use bzr resolve when you have fixed a problem.
 
59
    """
 
60
    takes_options = [
 
61
            'directory',
 
62
            option.Option('text',
 
63
                          help='List paths of files with text conflicts.'),
 
64
        ]
 
65
    _see_also = ['resolve', 'conflict-types']
 
66
 
 
67
    def run(self, text=False, directory=u'.'):
 
68
        wt = workingtree.WorkingTree.open_containing(directory)[0]
 
69
        for conflict in wt.conflicts():
 
70
            if text:
 
71
                if conflict.typestring != 'text conflict':
 
72
                    continue
 
73
                self.outf.write(conflict.path + '\n')
 
74
            else:
 
75
                self.outf.write(str(conflict) + '\n')
 
76
 
 
77
 
 
78
resolve_action_registry = registry.Registry()
 
79
 
 
80
 
 
81
resolve_action_registry.register(
 
82
    'done', 'done', 'Marks the conflict as resolved' )
 
83
resolve_action_registry.register(
 
84
    'take-this', 'take_this',
 
85
    'Resolve the conflict preserving the version in the working tree' )
 
86
resolve_action_registry.register(
 
87
    'take-other', 'take_other',
 
88
    'Resolve the conflict taking the merged version into account' )
 
89
resolve_action_registry.default_key = 'done'
 
90
 
 
91
class ResolveActionOption(option.RegistryOption):
 
92
 
 
93
    def __init__(self):
 
94
        super(ResolveActionOption, self).__init__(
 
95
            'action', 'How to resolve the conflict.',
 
96
            value_switches=True,
 
97
            registry=resolve_action_registry)
 
98
 
 
99
 
 
100
class cmd_resolve(commands.Command):
 
101
    __doc__ = """Mark a conflict as resolved.
 
102
 
 
103
    Merge will do its best to combine the changes in two branches, but there
 
104
    are some kinds of problems only a human can fix.  When it encounters those,
 
105
    it will mark a conflict.  A conflict means that you need to fix something,
 
106
    before you should commit.
 
107
 
 
108
    Once you have fixed a problem, use "bzr resolve" to automatically mark
 
109
    text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
 
110
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
 
111
    """
 
112
    aliases = ['resolved']
 
113
    takes_args = ['file*']
 
114
    takes_options = [
 
115
            'directory',
 
116
            option.Option('all', help='Resolve all conflicts in this tree.'),
 
117
            ResolveActionOption(),
 
118
            ]
 
119
    _see_also = ['conflicts']
 
120
    def run(self, file_list=None, all=False, action=None, directory=u'.'):
 
121
        if all:
 
122
            if file_list:
 
123
                raise errors.BzrCommandError("If --all is specified,"
 
124
                                             " no FILE may be provided")
 
125
            tree = workingtree.WorkingTree.open_containing(directory)[0]
 
126
            if action is None:
 
127
                action = 'done'
 
128
        else:
 
129
            tree, file_list = workingtree.WorkingTree.open_containing_paths(
 
130
                file_list)
 
131
            if file_list is None:
 
132
                if action is None:
 
133
                    # FIXME: There is a special case here related to the option
 
134
                    # handling that could be clearer and easier to discover by
 
135
                    # providing an --auto action (bug #344013 and #383396) and
 
136
                    # make it mandatory instead of implicit and active only
 
137
                    # when no file_list is provided -- vila 091229
 
138
                    action = 'auto'
 
139
            else:
 
140
                if action is None:
 
141
                    action = 'done'
 
142
        if action == 'auto':
 
143
            if file_list is None:
 
144
                un_resolved, resolved = tree.auto_resolve()
 
145
                if len(un_resolved) > 0:
 
146
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
 
147
                    trace.note('Remaining conflicts:')
 
148
                    for conflict in un_resolved:
 
149
                        trace.note(conflict)
 
150
                    return 1
 
151
                else:
 
152
                    trace.note('All conflicts resolved.')
 
153
                    return 0
 
154
            else:
 
155
                # FIXME: This can never occur but the block above needs some
 
156
                # refactoring to transfer tree.auto_resolve() to
 
157
                # conflict.auto(tree) --vila 091242
 
158
                pass
 
159
        else:
 
160
            resolve(tree, file_list, action=action)
 
161
 
 
162
 
 
163
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
 
164
            action='done'):
 
165
    """Resolve some or all of the conflicts in a working tree.
 
166
 
 
167
    :param paths: If None, resolve all conflicts.  Otherwise, select only
 
168
        specified conflicts.
 
169
    :param recursive: If True, then elements of paths which are directories
 
170
        have all their children resolved, etc.  When invoked as part of
 
171
        recursive commands like revert, this should be True.  For commands
 
172
        or applications wishing finer-grained control, like the resolve
 
173
        command, this should be False.
 
174
    :param ignore_misses: If False, warnings will be printed if the supplied
 
175
        paths do not have conflicts.
 
176
    :param action: How the conflict should be resolved,
 
177
    """
 
178
    tree.lock_tree_write()
 
179
    try:
 
180
        tree_conflicts = tree.conflicts()
 
181
        if paths is None:
 
182
            new_conflicts = ConflictList()
 
183
            to_process = tree_conflicts
 
184
        else:
 
185
            new_conflicts, to_process = tree_conflicts.select_conflicts(
 
186
                tree, paths, ignore_misses, recursive)
 
187
        for conflict in to_process:
 
188
            try:
 
189
                conflict._do(action, tree)
 
190
                conflict.cleanup(tree)
 
191
            except NotImplementedError:
 
192
                new_conflicts.append(conflict)
 
193
        try:
 
194
            tree.set_conflicts(new_conflicts)
 
195
        except errors.UnsupportedOperation:
 
196
            pass
 
197
    finally:
 
198
        tree.unlock()
 
199
 
 
200
 
 
201
def restore(filename):
 
202
    """Restore a conflicted file to the state it was in before merging.
 
203
 
 
204
    Only text restoration is supported at present.
 
205
    """
 
206
    conflicted = False
 
207
    try:
 
208
        osutils.rename(filename + ".THIS", filename)
 
209
        conflicted = True
 
210
    except OSError, e:
 
211
        if e.errno != errno.ENOENT:
 
212
            raise
 
213
    try:
 
214
        os.unlink(filename + ".BASE")
 
215
        conflicted = True
 
216
    except OSError, e:
 
217
        if e.errno != errno.ENOENT:
 
218
            raise
 
219
    try:
 
220
        os.unlink(filename + ".OTHER")
 
221
        conflicted = True
 
222
    except OSError, e:
 
223
        if e.errno != errno.ENOENT:
 
224
            raise
 
225
    if not conflicted:
 
226
        raise errors.NotConflicted(filename)
 
227
 
 
228
 
 
229
class ConflictList(object):
 
230
    """List of conflicts.
 
231
 
 
232
    Typically obtained from WorkingTree.conflicts()
 
233
 
 
234
    Can be instantiated from stanzas or from Conflict subclasses.
 
235
    """
 
236
 
 
237
    def __init__(self, conflicts=None):
 
238
        object.__init__(self)
 
239
        if conflicts is None:
 
240
            self.__list = []
 
241
        else:
 
242
            self.__list = conflicts
 
243
 
 
244
    def is_empty(self):
 
245
        return len(self.__list) == 0
 
246
 
 
247
    def __len__(self):
 
248
        return len(self.__list)
 
249
 
 
250
    def __iter__(self):
 
251
        return iter(self.__list)
 
252
 
 
253
    def __getitem__(self, key):
 
254
        return self.__list[key]
 
255
 
 
256
    def append(self, conflict):
 
257
        return self.__list.append(conflict)
 
258
 
 
259
    def __eq__(self, other_list):
 
260
        return list(self) == list(other_list)
 
261
 
 
262
    def __ne__(self, other_list):
 
263
        return not (self == other_list)
 
264
 
 
265
    def __repr__(self):
 
266
        return "ConflictList(%r)" % self.__list
 
267
 
 
268
    @staticmethod
 
269
    def from_stanzas(stanzas):
 
270
        """Produce a new ConflictList from an iterable of stanzas"""
 
271
        conflicts = ConflictList()
 
272
        for stanza in stanzas:
 
273
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
274
        return conflicts
 
275
 
 
276
    def to_stanzas(self):
 
277
        """Generator of stanzas"""
 
278
        for conflict in self:
 
279
            yield conflict.as_stanza()
 
280
 
 
281
    def to_strings(self):
 
282
        """Generate strings for the provided conflicts"""
 
283
        for conflict in self:
 
284
            yield str(conflict)
 
285
 
 
286
    def remove_files(self, tree):
 
287
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
 
288
        for conflict in self:
 
289
            if not conflict.has_files:
 
290
                continue
 
291
            conflict.cleanup(tree)
 
292
 
 
293
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
294
                         recurse=False):
 
295
        """Select the conflicts associated with paths in a tree.
 
296
 
 
297
        File-ids are also used for this.
 
298
        :return: a pair of ConflictLists: (not_selected, selected)
 
299
        """
 
300
        path_set = set(paths)
 
301
        ids = {}
 
302
        selected_paths = set()
 
303
        new_conflicts = ConflictList()
 
304
        selected_conflicts = ConflictList()
 
305
        for path in paths:
 
306
            file_id = tree.path2id(path)
 
307
            if file_id is not None:
 
308
                ids[file_id] = path
 
309
 
 
310
        for conflict in self:
 
311
            selected = False
 
312
            for key in ('path', 'conflict_path'):
 
313
                cpath = getattr(conflict, key, None)
 
314
                if cpath is None:
 
315
                    continue
 
316
                if cpath in path_set:
 
317
                    selected = True
 
318
                    selected_paths.add(cpath)
 
319
                if recurse:
 
320
                    if osutils.is_inside_any(path_set, cpath):
 
321
                        selected = True
 
322
                        selected_paths.add(cpath)
 
323
 
 
324
            for key in ('file_id', 'conflict_file_id'):
 
325
                cfile_id = getattr(conflict, key, None)
 
326
                if cfile_id is None:
 
327
                    continue
 
328
                try:
 
329
                    cpath = ids[cfile_id]
 
330
                except KeyError:
 
331
                    continue
 
332
                selected = True
 
333
                selected_paths.add(cpath)
 
334
            if selected:
 
335
                selected_conflicts.append(conflict)
 
336
            else:
 
337
                new_conflicts.append(conflict)
 
338
        if ignore_misses is not True:
 
339
            for path in [p for p in paths if p not in selected_paths]:
 
340
                if not os.path.exists(tree.abspath(path)):
 
341
                    print "%s does not exist" % path
 
342
                else:
 
343
                    print "%s is not conflicted" % path
 
344
        return new_conflicts, selected_conflicts
 
345
 
 
346
 
 
347
class Conflict(object):
 
348
    """Base class for all types of conflict"""
 
349
 
 
350
    # FIXME: cleanup should take care of that ? -- vila 091229
 
351
    has_files = False
 
352
 
 
353
    def __init__(self, path, file_id=None):
 
354
        self.path = path
 
355
        # warn turned off, because the factory blindly transfers the Stanza
 
356
        # values to __init__ and Stanza is purely a Unicode api.
 
357
        self.file_id = osutils.safe_file_id(file_id, warn=False)
 
358
 
 
359
    def as_stanza(self):
 
360
        s = rio.Stanza(type=self.typestring, path=self.path)
 
361
        if self.file_id is not None:
 
362
            # Stanza requires Unicode apis
 
363
            s.add('file_id', self.file_id.decode('utf8'))
 
364
        return s
 
365
 
 
366
    def _cmp_list(self):
 
367
        return [type(self), self.path, self.file_id]
 
368
 
 
369
    def __cmp__(self, other):
 
370
        if getattr(other, "_cmp_list", None) is None:
 
371
            return -1
 
372
        return cmp(self._cmp_list(), other._cmp_list())
 
373
 
 
374
    def __hash__(self):
 
375
        return hash((type(self), self.path, self.file_id))
 
376
 
 
377
    def __eq__(self, other):
 
378
        return self.__cmp__(other) == 0
 
379
 
 
380
    def __ne__(self, other):
 
381
        return not self.__eq__(other)
 
382
 
 
383
    def __str__(self):
 
384
        return self.format % self.__dict__
 
385
 
 
386
    def __repr__(self):
 
387
        rdict = dict(self.__dict__)
 
388
        rdict['class'] = self.__class__.__name__
 
389
        return self.rformat % rdict
 
390
 
 
391
    @staticmethod
 
392
    def factory(type, **kwargs):
 
393
        global ctype
 
394
        return ctype[type](**kwargs)
 
395
 
 
396
    @staticmethod
 
397
    def sort_key(conflict):
 
398
        if conflict.path is not None:
 
399
            return conflict.path, conflict.typestring
 
400
        elif getattr(conflict, "conflict_path", None) is not None:
 
401
            return conflict.conflict_path, conflict.typestring
 
402
        else:
 
403
            return None, conflict.typestring
 
404
 
 
405
    def _do(self, action, tree):
 
406
        """Apply the specified action to the conflict.
 
407
 
 
408
        :param action: The method name to call.
 
409
 
 
410
        :param tree: The tree passed as a parameter to the method.
 
411
        """
 
412
        meth = getattr(self, 'action_%s' % action, None)
 
413
        if meth is None:
 
414
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
 
415
        meth(tree)
 
416
 
 
417
    def associated_filenames(self):
 
418
        """The names of the files generated to help resolve the conflict."""
 
419
        raise NotImplementedError(self.associated_filenames)
 
420
 
 
421
    def cleanup(self, tree):
 
422
        for fname in self.associated_filenames():
 
423
            try:
 
424
                osutils.delete_any(tree.abspath(fname))
 
425
            except OSError, e:
 
426
                if e.errno != errno.ENOENT:
 
427
                    raise
 
428
 
 
429
    def action_done(self, tree):
 
430
        """Mark the conflict as solved once it has been handled."""
 
431
        # This method does nothing but simplifies the design of upper levels.
 
432
        pass
 
433
 
 
434
    def action_take_this(self, tree):
 
435
        raise NotImplementedError(self.action_take_this)
 
436
 
 
437
    def action_take_other(self, tree):
 
438
        raise NotImplementedError(self.action_take_other)
 
439
 
 
440
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
 
441
        tt = transform.TreeTransform(tree)
 
442
        op = cleanup.OperationWithCleanups(self._resolve)
 
443
        op.add_cleanup(tt.finalize)
 
444
        op.run_simple(tt, *args, **kwargs)
 
445
 
 
446
 
 
447
class PathConflict(Conflict):
 
448
    """A conflict was encountered merging file paths"""
 
449
 
 
450
    typestring = 'path conflict'
 
451
 
 
452
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
453
 
 
454
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
455
 
 
456
    def __init__(self, path, conflict_path=None, file_id=None):
 
457
        Conflict.__init__(self, path, file_id)
 
458
        self.conflict_path = conflict_path
 
459
 
 
460
    def as_stanza(self):
 
461
        s = Conflict.as_stanza(self)
 
462
        if self.conflict_path is not None:
 
463
            s.add('conflict_path', self.conflict_path)
 
464
        return s
 
465
 
 
466
    def associated_filenames(self):
 
467
        # No additional files have been generated here
 
468
        return []
 
469
 
 
470
    def _resolve(self, tt, file_id, path, winner):
 
471
        """Resolve the conflict.
 
472
 
 
473
        :param tt: The TreeTransform where the conflict is resolved.
 
474
        :param file_id: The retained file id.
 
475
        :param path: The retained path.
 
476
        :param winner: 'this' or 'other' indicates which side is the winner.
 
477
        """
 
478
        path_to_create = None
 
479
        if winner == 'this':
 
480
            if self.path == '<deleted>':
 
481
                return # Nothing to do
 
482
            if self.conflict_path == '<deleted>':
 
483
                path_to_create = self.path
 
484
                revid = tt._tree.get_parent_ids()[0]
 
485
        elif winner == 'other':
 
486
            if self.conflict_path == '<deleted>':
 
487
                return  # Nothing to do
 
488
            if self.path == '<deleted>':
 
489
                path_to_create = self.conflict_path
 
490
                # FIXME: If there are more than two parents we may need to
 
491
                # iterate. Taking the last parent is the safer bet in the mean
 
492
                # time. -- vila 20100309
 
493
                revid = tt._tree.get_parent_ids()[-1]
 
494
        else:
 
495
            # Programmer error
 
496
            raise AssertionError('bad winner: %r' % (winner,))
 
497
        if path_to_create is not None:
 
498
            tid = tt.trans_id_tree_path(path_to_create)
 
499
            transform.create_from_tree(
 
500
                tt, tt.trans_id_tree_path(path_to_create),
 
501
                self._revision_tree(tt._tree, revid), file_id)
 
502
            tt.version_file(file_id, tid)
 
503
 
 
504
        # Adjust the path for the retained file id
 
505
        tid = tt.trans_id_file_id(file_id)
 
506
        parent_tid = tt.get_tree_parent(tid)
 
507
        tt.adjust_path(path, parent_tid, tid)
 
508
        tt.apply()
 
509
 
 
510
    def _revision_tree(self, tree, revid):
 
511
        return tree.branch.repository.revision_tree(revid)
 
512
 
 
513
    def _infer_file_id(self, tree):
 
514
        # Prior to bug #531967, file_id wasn't always set, there may still be
 
515
        # conflict files in the wild so we need to cope with them
 
516
        # Establish which path we should use to find back the file-id
 
517
        possible_paths = []
 
518
        for p in (self.path, self.conflict_path):
 
519
            if p == '<deleted>':
 
520
                # special hard-coded path 
 
521
                continue
 
522
            if p is not None:
 
523
                possible_paths.append(p)
 
524
        # Search the file-id in the parents with any path available
 
525
        file_id = None
 
526
        for revid in tree.get_parent_ids():
 
527
            revtree = self._revision_tree(tree, revid)
 
528
            for p in possible_paths:
 
529
                file_id = revtree.path2id(p)
 
530
                if file_id is not None:
 
531
                    return revtree, file_id
 
532
        return None, None
 
533
 
 
534
    def action_take_this(self, tree):
 
535
        if self.file_id is not None:
 
536
            self._resolve_with_cleanups(tree, self.file_id, self.path,
 
537
                                        winner='this')
 
538
        else:
 
539
            # Prior to bug #531967 we need to find back the file_id and restore
 
540
            # the content from there
 
541
            revtree, file_id = self._infer_file_id(tree)
 
542
            tree.revert([revtree.id2path(file_id)],
 
543
                        old_tree=revtree, backups=False)
 
544
 
 
545
    def action_take_other(self, tree):
 
546
        if self.file_id is not None:
 
547
            self._resolve_with_cleanups(tree, self.file_id,
 
548
                                        self.conflict_path,
 
549
                                        winner='other')
 
550
        else:
 
551
            # Prior to bug #531967 we need to find back the file_id and restore
 
552
            # the content from there
 
553
            revtree, file_id = self._infer_file_id(tree)
 
554
            tree.revert([revtree.id2path(file_id)],
 
555
                        old_tree=revtree, backups=False)
 
556
 
 
557
 
 
558
class ContentsConflict(PathConflict):
 
559
    """The files are of different types (or both binary), or not present"""
 
560
 
 
561
    has_files = True
 
562
 
 
563
    typestring = 'contents conflict'
 
564
 
 
565
    format = 'Contents conflict in %(path)s'
 
566
 
 
567
    def associated_filenames(self):
 
568
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
 
569
 
 
570
    def _resolve(self, tt, suffix_to_remove):
 
571
        """Resolve the conflict.
 
572
 
 
573
        :param tt: The TreeTransform where the conflict is resolved.
 
574
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
 
575
 
 
576
        The resolution is symmetric, when taking THIS, OTHER is deleted and
 
577
        item.THIS is renamed into item and vice-versa.
 
578
        """
 
579
        try:
 
580
            # Delete 'item.THIS' or 'item.OTHER' depending on
 
581
            # suffix_to_remove
 
582
            tt.delete_contents(
 
583
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
 
584
        except errors.NoSuchFile:
 
585
            # There are valid cases where 'item.suffix_to_remove' either
 
586
            # never existed or was already deleted (including the case
 
587
            # where the user deleted it)
 
588
            pass
 
589
        # Rename 'item.suffix_to_remove' (note that if
 
590
        # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
591
        this_tid = tt.trans_id_file_id(self.file_id)
 
592
        parent_tid = tt.get_tree_parent(this_tid)
 
593
        tt.adjust_path(self.path, parent_tid, this_tid)
 
594
        tt.apply()
 
595
 
 
596
    def action_take_this(self, tree):
 
597
        self._resolve_with_cleanups(tree, 'OTHER')
 
598
 
 
599
    def action_take_other(self, tree):
 
600
        self._resolve_with_cleanups(tree, 'THIS')
 
601
 
 
602
 
 
603
# FIXME: TextConflict is about a single file-id, there never is a conflict_path
 
604
# attribute so we shouldn't inherit from PathConflict but simply from Conflict
 
605
 
 
606
# TODO: There should be a base revid attribute to better inform the user about
 
607
# how the conflicts were generated.
 
608
class TextConflict(PathConflict):
 
609
    """The merge algorithm could not resolve all differences encountered."""
 
610
 
 
611
    has_files = True
 
612
 
 
613
    typestring = 'text conflict'
 
614
 
 
615
    format = 'Text conflict in %(path)s'
 
616
 
 
617
    def associated_filenames(self):
 
618
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
619
 
 
620
 
 
621
class HandledConflict(Conflict):
 
622
    """A path problem that has been provisionally resolved.
 
623
    This is intended to be a base class.
 
624
    """
 
625
 
 
626
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
627
 
 
628
    def __init__(self, action, path, file_id=None):
 
629
        Conflict.__init__(self, path, file_id)
 
630
        self.action = action
 
631
 
 
632
    def _cmp_list(self):
 
633
        return Conflict._cmp_list(self) + [self.action]
 
634
 
 
635
    def as_stanza(self):
 
636
        s = Conflict.as_stanza(self)
 
637
        s.add('action', self.action)
 
638
        return s
 
639
 
 
640
    def associated_filenames(self):
 
641
        # Nothing has been generated here
 
642
        return []
 
643
 
 
644
 
 
645
class HandledPathConflict(HandledConflict):
 
646
    """A provisionally-resolved path problem involving two paths.
 
647
    This is intended to be a base class.
 
648
    """
 
649
 
 
650
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
651
        " %(file_id)r, %(conflict_file_id)r)"
 
652
 
 
653
    def __init__(self, action, path, conflict_path, file_id=None,
 
654
                 conflict_file_id=None):
 
655
        HandledConflict.__init__(self, action, path, file_id)
 
656
        self.conflict_path = conflict_path
 
657
        # warn turned off, because the factory blindly transfers the Stanza
 
658
        # values to __init__.
 
659
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
 
660
                                                     warn=False)
 
661
 
 
662
    def _cmp_list(self):
 
663
        return HandledConflict._cmp_list(self) + [self.conflict_path,
 
664
                                                  self.conflict_file_id]
 
665
 
 
666
    def as_stanza(self):
 
667
        s = HandledConflict.as_stanza(self)
 
668
        s.add('conflict_path', self.conflict_path)
 
669
        if self.conflict_file_id is not None:
 
670
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
671
 
 
672
        return s
 
673
 
 
674
 
 
675
class DuplicateID(HandledPathConflict):
 
676
    """Two files want the same file_id."""
 
677
 
 
678
    typestring = 'duplicate id'
 
679
 
 
680
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
681
 
 
682
 
 
683
class DuplicateEntry(HandledPathConflict):
 
684
    """Two directory entries want to have the same name."""
 
685
 
 
686
    typestring = 'duplicate'
 
687
 
 
688
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
689
 
 
690
    def action_take_this(self, tree):
 
691
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
692
        tree.rename_one(self.path, self.conflict_path)
 
693
 
 
694
    def action_take_other(self, tree):
 
695
        tree.remove([self.path], force=True, keep_files=False)
 
696
 
 
697
 
 
698
class ParentLoop(HandledPathConflict):
 
699
    """An attempt to create an infinitely-looping directory structure.
 
700
    This is rare, but can be produced like so:
 
701
 
 
702
    tree A:
 
703
      mv foo bar
 
704
    tree B:
 
705
      mv bar foo
 
706
    merge A and B
 
707
    """
 
708
 
 
709
    typestring = 'parent loop'
 
710
 
 
711
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
712
 
 
713
    def action_take_this(self, tree):
 
714
        # just acccept bzr proposal
 
715
        pass
 
716
 
 
717
    def action_take_other(self, tree):
 
718
        # FIXME: We shouldn't have to manipulate so many paths here (and there
 
719
        # is probably a bug or two...)
 
720
        base_path = osutils.basename(self.path)
 
721
        conflict_base_path = osutils.basename(self.conflict_path)
 
722
        tt = transform.TreeTransform(tree)
 
723
        try:
 
724
            p_tid = tt.trans_id_file_id(self.file_id)
 
725
            parent_tid = tt.get_tree_parent(p_tid)
 
726
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
 
727
            cparent_tid = tt.get_tree_parent(cp_tid)
 
728
            tt.adjust_path(base_path, cparent_tid, cp_tid)
 
729
            tt.adjust_path(conflict_base_path, parent_tid, p_tid)
 
730
            tt.apply()
 
731
        finally:
 
732
            tt.finalize()
 
733
 
 
734
 
 
735
class UnversionedParent(HandledConflict):
 
736
    """An attempt to version a file whose parent directory is not versioned.
 
737
    Typically, the result of a merge where one tree unversioned the directory
 
738
    and the other added a versioned file to it.
 
739
    """
 
740
 
 
741
    typestring = 'unversioned parent'
 
742
 
 
743
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
744
             ' children.  %(action)s.'
 
745
 
 
746
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
747
    # conflict shouldn't exist (the long story is that the conflict is
 
748
    # generated with another one that can be resolved properly) -- vila 091224
 
749
    def action_take_this(self, tree):
 
750
        pass
 
751
 
 
752
    def action_take_other(self, tree):
 
753
        pass
 
754
 
 
755
 
 
756
class MissingParent(HandledConflict):
 
757
    """An attempt to add files to a directory that is not present.
 
758
    Typically, the result of a merge where THIS deleted the directory and
 
759
    the OTHER added a file to it.
 
760
    See also: DeletingParent (same situation, THIS and OTHER reversed)
 
761
    """
 
762
 
 
763
    typestring = 'missing parent'
 
764
 
 
765
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
766
 
 
767
    def action_take_this(self, tree):
 
768
        tree.remove([self.path], force=True, keep_files=False)
 
769
 
 
770
    def action_take_other(self, tree):
 
771
        # just acccept bzr proposal
 
772
        pass
 
773
 
 
774
 
 
775
class DeletingParent(HandledConflict):
 
776
    """An attempt to add files to a directory that is not present.
 
777
    Typically, the result of a merge where one OTHER deleted the directory and
 
778
    the THIS added a file to it.
 
779
    """
 
780
 
 
781
    typestring = 'deleting parent'
 
782
 
 
783
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
784
             "%(action)s."
 
785
 
 
786
    # FIXME: It's a bit strange that the default action is not coherent with
 
787
    # MissingParent from the *user* pov.
 
788
 
 
789
    def action_take_this(self, tree):
 
790
        # just acccept bzr proposal
 
791
        pass
 
792
 
 
793
    def action_take_other(self, tree):
 
794
        tree.remove([self.path], force=True, keep_files=False)
 
795
 
 
796
 
 
797
class NonDirectoryParent(HandledConflict):
 
798
    """An attempt to add files to a directory that is not a directory or
 
799
    an attempt to change the kind of a directory with files.
 
800
    """
 
801
 
 
802
    typestring = 'non-directory parent'
 
803
 
 
804
    format = "Conflict: %(path)s is not a directory, but has files in it."\
 
805
             "  %(action)s."
 
806
 
 
807
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
808
 
 
809
    def action_take_this(self, tree):
 
810
        # FIXME: we should preserve that path when the conflict is generated !
 
811
        if self.path.endswith('.new'):
 
812
            conflict_path = self.path[:-(len('.new'))]
 
813
            tree.remove([self.path], force=True, keep_files=False)
 
814
            tree.add(conflict_path)
 
815
        else:
 
816
            raise NotImplementedError(self.action_take_this)
 
817
 
 
818
    def action_take_other(self, tree):
 
819
        # FIXME: we should preserve that path when the conflict is generated !
 
820
        if self.path.endswith('.new'):
 
821
            conflict_path = self.path[:-(len('.new'))]
 
822
            tree.remove([conflict_path], force=True, keep_files=False)
 
823
            tree.rename_one(self.path, conflict_path)
 
824
        else:
 
825
            raise NotImplementedError(self.action_take_other)
 
826
 
 
827
 
 
828
ctype = {}
 
829
 
 
830
 
 
831
def register_types(*conflict_types):
 
832
    """Register a Conflict subclass for serialization purposes"""
 
833
    global ctype
 
834
    for conflict_type in conflict_types:
 
835
        ctype[conflict_type.typestring] = conflict_type
 
836
 
 
837
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
838
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
839
               DeletingParent, NonDirectoryParent)