/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: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 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: 'brz resolve' should accept a directory name and work from that
 
18
# point down
 
19
 
 
20
from __future__ import absolute_import
 
21
 
 
22
import os
 
23
import re
 
24
 
 
25
from .lazy_import import lazy_import
 
26
lazy_import(globals(), """
 
27
import errno
 
28
 
 
29
from breezy import (
 
30
    osutils,
 
31
    rio,
 
32
    trace,
 
33
    transform,
 
34
    workingtree,
 
35
    )
 
36
from breezy.i18n import gettext, ngettext
 
37
""")
 
38
from . import (
 
39
    cache_utf8,
 
40
    errors,
 
41
    commands,
 
42
    option,
 
43
    registry,
 
44
    )
 
45
 
 
46
 
 
47
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
48
 
 
49
 
 
50
class cmd_conflicts(commands.Command):
 
51
    __doc__ = """List files with conflicts.
 
52
 
 
53
    Merge will do its best to combine the changes in two branches, but there
 
54
    are some kinds of problems only a human can fix.  When it encounters those,
 
55
    it will mark a conflict.  A conflict means that you need to fix something,
 
56
    before you can commit.
 
57
 
 
58
    Conflicts normally are listed as short, human-readable messages.  If --text
 
59
    is supplied, the pathnames of files with text conflicts are listed,
 
60
    instead.  (This is useful for editing all files with text conflicts.)
 
61
 
 
62
    Use brz resolve when you have fixed a problem.
 
63
    """
 
64
    takes_options = [
 
65
        'directory',
 
66
        option.Option('text',
 
67
                      help='List paths of files with text conflicts.'),
 
68
        ]
 
69
    _see_also = ['resolve', 'conflict-types']
 
70
 
 
71
    def run(self, text=False, directory=u'.'):
 
72
        wt = workingtree.WorkingTree.open_containing(directory)[0]
 
73
        for conflict in wt.conflicts():
 
74
            if text:
 
75
                if conflict.typestring != 'text conflict':
 
76
                    continue
 
77
                self.outf.write(conflict.path + '\n')
 
78
            else:
 
79
                self.outf.write(str(conflict) + '\n')
 
80
 
 
81
 
 
82
resolve_action_registry = registry.Registry()
 
83
 
 
84
 
 
85
resolve_action_registry.register(
 
86
    'auto', 'auto', 'Detect whether conflict has been resolved by user.')
 
87
resolve_action_registry.register(
 
88
    'done', 'done', 'Marks the conflict as resolved.')
 
89
resolve_action_registry.register(
 
90
    'take-this', 'take_this',
 
91
    'Resolve the conflict preserving the version in the working tree.')
 
92
resolve_action_registry.register(
 
93
    'take-other', 'take_other',
 
94
    'Resolve the conflict taking the merged version into account.')
 
95
resolve_action_registry.default_key = 'done'
 
96
 
 
97
 
 
98
class ResolveActionOption(option.RegistryOption):
 
99
 
 
100
    def __init__(self):
 
101
        super(ResolveActionOption, self).__init__(
 
102
            'action', 'How to resolve the conflict.',
 
103
            value_switches=True,
 
104
            registry=resolve_action_registry)
 
105
 
 
106
 
 
107
class cmd_resolve(commands.Command):
 
108
    __doc__ = """Mark a conflict as resolved.
 
109
 
 
110
    Merge will do its best to combine the changes in two branches, but there
 
111
    are some kinds of problems only a human can fix.  When it encounters those,
 
112
    it will mark a conflict.  A conflict means that you need to fix something,
 
113
    before you can commit.
 
114
 
 
115
    Once you have fixed a problem, use "brz resolve" to automatically mark
 
116
    text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
 
117
    resolved, or "brz resolve --all" to mark all conflicts as resolved.
 
118
    """
 
119
    aliases = ['resolved']
 
120
    takes_args = ['file*']
 
121
    takes_options = [
 
122
        'directory',
 
123
        option.Option('all', help='Resolve all conflicts in this tree.'),
 
124
        ResolveActionOption(),
 
125
        ]
 
126
    _see_also = ['conflicts']
 
127
 
 
128
    def run(self, file_list=None, all=False, action=None, directory=None):
 
129
        if all:
 
130
            if file_list:
 
131
                raise errors.BzrCommandError(gettext("If --all is specified,"
 
132
                                                     " no FILE may be provided"))
 
133
            if directory is None:
 
134
                directory = u'.'
 
135
            tree = workingtree.WorkingTree.open_containing(directory)[0]
 
136
            if action is None:
 
137
                action = 'done'
 
138
        else:
 
139
            tree, file_list = workingtree.WorkingTree.open_containing_paths(
 
140
                file_list, directory)
 
141
            if action is None:
 
142
                if file_list is None:
 
143
                    action = 'auto'
 
144
                else:
 
145
                    action = 'done'
 
146
        before, after = resolve(tree, file_list, action=action)
 
147
        # GZ 2012-07-27: Should unify UI below now that auto is less magical.
 
148
        if action == 'auto' and file_list is None:
 
149
            if after > 0:
 
150
                trace.note(
 
151
                    ngettext('%d conflict auto-resolved.',
 
152
                             '%d conflicts auto-resolved.', before - after),
 
153
                    before - after)
 
154
                trace.note(gettext('Remaining conflicts:'))
 
155
                for conflict in tree.conflicts():
 
156
                    trace.note(str(conflict))
 
157
                return 1
 
158
            else:
 
159
                trace.note(gettext('All conflicts resolved.'))
 
160
                return 0
 
161
        else:
 
162
            trace.note(ngettext('{0} conflict resolved, {1} remaining',
 
163
                                '{0} conflicts resolved, {1} remaining',
 
164
                                before - after).format(before - after, after))
 
165
 
 
166
 
 
167
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
 
168
            action='done'):
 
169
    """Resolve some or all of the conflicts in a working tree.
 
170
 
 
171
    :param paths: If None, resolve all conflicts.  Otherwise, select only
 
172
        specified conflicts.
 
173
    :param recursive: If True, then elements of paths which are directories
 
174
        have all their children resolved, etc.  When invoked as part of
 
175
        recursive commands like revert, this should be True.  For commands
 
176
        or applications wishing finer-grained control, like the resolve
 
177
        command, this should be False.
 
178
    :param ignore_misses: If False, warnings will be printed if the supplied
 
179
        paths do not have conflicts.
 
180
    :param action: How the conflict should be resolved,
 
181
    """
 
182
    nb_conflicts_after = None
 
183
    with tree.lock_tree_write():
 
184
        tree_conflicts = tree.conflicts()
 
185
        nb_conflicts_before = len(tree_conflicts)
 
186
        if paths is None:
 
187
            new_conflicts = ConflictList()
 
188
            to_process = tree_conflicts
 
189
        else:
 
190
            new_conflicts, to_process = tree_conflicts.select_conflicts(
 
191
                tree, paths, ignore_misses, recursive)
 
192
        for conflict in to_process:
 
193
            try:
 
194
                conflict._do(action, tree)
 
195
                conflict.cleanup(tree)
 
196
            except NotImplementedError:
 
197
                new_conflicts.append(conflict)
 
198
        try:
 
199
            nb_conflicts_after = len(new_conflicts)
 
200
            tree.set_conflicts(new_conflicts)
 
201
        except errors.UnsupportedOperation:
 
202
            pass
 
203
    if nb_conflicts_after is None:
 
204
        nb_conflicts_after = nb_conflicts_before
 
205
    return nb_conflicts_before, nb_conflicts_after
 
206
 
 
207
 
 
208
def restore(filename):
 
209
    """Restore a conflicted file to the state it was in before merging.
 
210
 
 
211
    Only text restoration is supported at present.
 
212
    """
 
213
    conflicted = False
 
214
    try:
 
215
        osutils.rename(filename + ".THIS", filename)
 
216
        conflicted = True
 
217
    except OSError as e:
 
218
        if e.errno != errno.ENOENT:
 
219
            raise
 
220
    try:
 
221
        os.unlink(filename + ".BASE")
 
222
        conflicted = True
 
223
    except OSError as e:
 
224
        if e.errno != errno.ENOENT:
 
225
            raise
 
226
    try:
 
227
        os.unlink(filename + ".OTHER")
 
228
        conflicted = True
 
229
    except OSError as e:
 
230
        if e.errno != errno.ENOENT:
 
231
            raise
 
232
    if not conflicted:
 
233
        raise errors.NotConflicted(filename)
 
234
 
 
235
 
 
236
class ConflictList(object):
 
237
    """List of conflicts.
 
238
 
 
239
    Typically obtained from WorkingTree.conflicts()
 
240
 
 
241
    Can be instantiated from stanzas or from Conflict subclasses.
 
242
    """
 
243
 
 
244
    def __init__(self, conflicts=None):
 
245
        object.__init__(self)
 
246
        if conflicts is None:
 
247
            self.__list = []
 
248
        else:
 
249
            self.__list = conflicts
 
250
 
 
251
    def is_empty(self):
 
252
        return len(self.__list) == 0
 
253
 
 
254
    def __len__(self):
 
255
        return len(self.__list)
 
256
 
 
257
    def __iter__(self):
 
258
        return iter(self.__list)
 
259
 
 
260
    def __getitem__(self, key):
 
261
        return self.__list[key]
 
262
 
 
263
    def append(self, conflict):
 
264
        return self.__list.append(conflict)
 
265
 
 
266
    def __eq__(self, other_list):
 
267
        return list(self) == list(other_list)
 
268
 
 
269
    def __ne__(self, other_list):
 
270
        return not (self == other_list)
 
271
 
 
272
    def __repr__(self):
 
273
        return "ConflictList(%r)" % self.__list
 
274
 
 
275
    @staticmethod
 
276
    def from_stanzas(stanzas):
 
277
        """Produce a new ConflictList from an iterable of stanzas"""
 
278
        conflicts = ConflictList()
 
279
        for stanza in stanzas:
 
280
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
281
        return conflicts
 
282
 
 
283
    def to_stanzas(self):
 
284
        """Generator of stanzas"""
 
285
        for conflict in self:
 
286
            yield conflict.as_stanza()
 
287
 
 
288
    def to_strings(self):
 
289
        """Generate strings for the provided conflicts"""
 
290
        for conflict in self:
 
291
            yield str(conflict)
 
292
 
 
293
    def remove_files(self, tree):
 
294
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
 
295
        for conflict in self:
 
296
            if not conflict.has_files:
 
297
                continue
 
298
            conflict.cleanup(tree)
 
299
 
 
300
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
301
                         recurse=False):
 
302
        """Select the conflicts associated with paths in a tree.
 
303
 
 
304
        File-ids are also used for this.
 
305
        :return: a pair of ConflictLists: (not_selected, selected)
 
306
        """
 
307
        path_set = set(paths)
 
308
        ids = {}
 
309
        selected_paths = set()
 
310
        new_conflicts = ConflictList()
 
311
        selected_conflicts = ConflictList()
 
312
        for path in paths:
 
313
            file_id = tree.path2id(path)
 
314
            if file_id is not None:
 
315
                ids[file_id] = path
 
316
 
 
317
        for conflict in self:
 
318
            selected = False
 
319
            for key in ('path', 'conflict_path'):
 
320
                cpath = getattr(conflict, key, None)
 
321
                if cpath is None:
 
322
                    continue
 
323
                if cpath in path_set:
 
324
                    selected = True
 
325
                    selected_paths.add(cpath)
 
326
                if recurse:
 
327
                    if osutils.is_inside_any(path_set, cpath):
 
328
                        selected = True
 
329
                        selected_paths.add(cpath)
 
330
 
 
331
            for key in ('file_id', 'conflict_file_id'):
 
332
                cfile_id = getattr(conflict, key, None)
 
333
                if cfile_id is None:
 
334
                    continue
 
335
                try:
 
336
                    cpath = ids[cfile_id]
 
337
                except KeyError:
 
338
                    continue
 
339
                selected = True
 
340
                selected_paths.add(cpath)
 
341
            if selected:
 
342
                selected_conflicts.append(conflict)
 
343
            else:
 
344
                new_conflicts.append(conflict)
 
345
        if ignore_misses is not True:
 
346
            for path in [p for p in paths if p not in selected_paths]:
 
347
                if not os.path.exists(tree.abspath(path)):
 
348
                    print("%s does not exist" % path)
 
349
                else:
 
350
                    print("%s is not conflicted" % path)
 
351
        return new_conflicts, selected_conflicts
 
352
 
 
353
 
 
354
class Conflict(object):
 
355
    """Base class for all types of conflict"""
 
356
 
 
357
    # FIXME: cleanup should take care of that ? -- vila 091229
 
358
    has_files = False
 
359
 
 
360
    def __init__(self, path, file_id=None):
 
361
        self.path = path
 
362
        # the factory blindly transfers the Stanza values to __init__ and
 
363
        # Stanza is purely a Unicode api.
 
364
        if isinstance(file_id, str):
 
365
            file_id = cache_utf8.encode(file_id)
 
366
        self.file_id = osutils.safe_file_id(file_id)
 
367
 
 
368
    def as_stanza(self):
 
369
        s = rio.Stanza(type=self.typestring, path=self.path)
 
370
        if self.file_id is not None:
 
371
            # Stanza requires Unicode apis
 
372
            s.add('file_id', self.file_id.decode('utf8'))
 
373
        return s
 
374
 
 
375
    def _cmp_list(self):
 
376
        return [type(self), self.path, self.file_id]
 
377
 
 
378
    def __cmp__(self, other):
 
379
        if getattr(other, "_cmp_list", None) is None:
 
380
            return -1
 
381
        x = self._cmp_list()
 
382
        y = other._cmp_list()
 
383
        return (x > y) - (x < y)
 
384
 
 
385
    def __hash__(self):
 
386
        return hash((type(self), self.path, self.file_id))
 
387
 
 
388
    def __eq__(self, other):
 
389
        return self.__cmp__(other) == 0
 
390
 
 
391
    def __ne__(self, other):
 
392
        return not self.__eq__(other)
 
393
 
 
394
    def __unicode__(self):
 
395
        return self.describe()
 
396
 
 
397
    def __str__(self):
 
398
        return self.describe()
 
399
 
 
400
    def describe(self):
 
401
        return self.format % self.__dict__
 
402
 
 
403
    def __repr__(self):
 
404
        rdict = dict(self.__dict__)
 
405
        rdict['class'] = self.__class__.__name__
 
406
        return self.rformat % rdict
 
407
 
 
408
    @staticmethod
 
409
    def factory(type, **kwargs):
 
410
        global ctype
 
411
        return ctype[type](**kwargs)
 
412
 
 
413
    @staticmethod
 
414
    def sort_key(conflict):
 
415
        if conflict.path is not None:
 
416
            return conflict.path, conflict.typestring
 
417
        elif getattr(conflict, "conflict_path", None) is not None:
 
418
            return conflict.conflict_path, conflict.typestring
 
419
        else:
 
420
            return None, conflict.typestring
 
421
 
 
422
    def _do(self, action, tree):
 
423
        """Apply the specified action to the conflict.
 
424
 
 
425
        :param action: The method name to call.
 
426
 
 
427
        :param tree: The tree passed as a parameter to the method.
 
428
        """
 
429
        meth = getattr(self, 'action_%s' % action, None)
 
430
        if meth is None:
 
431
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
 
432
        meth(tree)
 
433
 
 
434
    def associated_filenames(self):
 
435
        """The names of the files generated to help resolve the conflict."""
 
436
        raise NotImplementedError(self.associated_filenames)
 
437
 
 
438
    def cleanup(self, tree):
 
439
        for fname in self.associated_filenames():
 
440
            try:
 
441
                osutils.delete_any(tree.abspath(fname))
 
442
            except OSError as e:
 
443
                if e.errno != errno.ENOENT:
 
444
                    raise
 
445
 
 
446
    def action_auto(self, tree):
 
447
        raise NotImplementedError(self.action_auto)
 
448
 
 
449
    def action_done(self, tree):
 
450
        """Mark the conflict as solved once it has been handled."""
 
451
        # This method does nothing but simplifies the design of upper levels.
 
452
        pass
 
453
 
 
454
    def action_take_this(self, tree):
 
455
        raise NotImplementedError(self.action_take_this)
 
456
 
 
457
    def action_take_other(self, tree):
 
458
        raise NotImplementedError(self.action_take_other)
 
459
 
 
460
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
 
461
        with tree.get_transform() as tt:
 
462
            self._resolve(tt, *args, **kwargs)
 
463
 
 
464
 
 
465
class PathConflict(Conflict):
 
466
    """A conflict was encountered merging file paths"""
 
467
 
 
468
    typestring = 'path conflict'
 
469
 
 
470
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
471
 
 
472
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
473
 
 
474
    def __init__(self, path, conflict_path=None, file_id=None):
 
475
        Conflict.__init__(self, path, file_id)
 
476
        self.conflict_path = conflict_path
 
477
 
 
478
    def as_stanza(self):
 
479
        s = Conflict.as_stanza(self)
 
480
        if self.conflict_path is not None:
 
481
            s.add('conflict_path', self.conflict_path)
 
482
        return s
 
483
 
 
484
    def associated_filenames(self):
 
485
        # No additional files have been generated here
 
486
        return []
 
487
 
 
488
    def _resolve(self, tt, file_id, path, winner):
 
489
        """Resolve the conflict.
 
490
 
 
491
        :param tt: The TreeTransform where the conflict is resolved.
 
492
        :param file_id: The retained file id.
 
493
        :param path: The retained path.
 
494
        :param winner: 'this' or 'other' indicates which side is the winner.
 
495
        """
 
496
        path_to_create = None
 
497
        if winner == 'this':
 
498
            if self.path == '<deleted>':
 
499
                return  # Nothing to do
 
500
            if self.conflict_path == '<deleted>':
 
501
                path_to_create = self.path
 
502
                revid = tt._tree.get_parent_ids()[0]
 
503
        elif winner == 'other':
 
504
            if self.conflict_path == '<deleted>':
 
505
                return  # Nothing to do
 
506
            if self.path == '<deleted>':
 
507
                path_to_create = self.conflict_path
 
508
                # FIXME: If there are more than two parents we may need to
 
509
                # iterate. Taking the last parent is the safer bet in the mean
 
510
                # time. -- vila 20100309
 
511
                revid = tt._tree.get_parent_ids()[-1]
 
512
        else:
 
513
            # Programmer error
 
514
            raise AssertionError('bad winner: %r' % (winner,))
 
515
        if path_to_create is not None:
 
516
            tid = tt.trans_id_tree_path(path_to_create)
 
517
            tree = self._revision_tree(tt._tree, revid)
 
518
            transform.create_from_tree(
 
519
                tt, tid, tree, tree.id2path(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
    _conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
 
646
 
 
647
    def associated_filenames(self):
 
648
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
649
 
 
650
    def _resolve(self, tt, winner_suffix):
 
651
        """Resolve the conflict by copying one of .THIS or .OTHER into file.
 
652
 
 
653
        :param tt: The TreeTransform where the conflict is resolved.
 
654
        :param winner_suffix: Either 'THIS' or 'OTHER'
 
655
 
 
656
        The resolution is symmetric, when taking THIS, item.THIS is renamed
 
657
        into item and vice-versa. This takes one of the files as a whole
 
658
        ignoring every difference that could have been merged cleanly.
 
659
        """
 
660
        # To avoid useless copies, we switch item and item.winner_suffix, only
 
661
        # item will exist after the conflict has been resolved anyway.
 
662
        item_tid = tt.trans_id_file_id(self.file_id)
 
663
        item_parent_tid = tt.get_tree_parent(item_tid)
 
664
        winner_path = self.path + '.' + winner_suffix
 
665
        winner_tid = tt.trans_id_tree_path(winner_path)
 
666
        winner_parent_tid = tt.get_tree_parent(winner_tid)
 
667
        # Switch the paths to preserve the content
 
668
        tt.adjust_path(osutils.basename(self.path),
 
669
                       winner_parent_tid, winner_tid)
 
670
        tt.adjust_path(osutils.basename(winner_path),
 
671
                       item_parent_tid, item_tid)
 
672
        # Associate the file_id to the right content
 
673
        tt.unversion_file(item_tid)
 
674
        tt.version_file(self.file_id, winner_tid)
 
675
        tt.apply()
 
676
 
 
677
    def action_auto(self, tree):
 
678
        # GZ 2012-07-27: Using NotImplementedError to signal that a conflict
 
679
        #                can't be auto resolved does not seem ideal.
 
680
        try:
 
681
            kind = tree.kind(self.path)
 
682
        except errors.NoSuchFile:
 
683
            return
 
684
        if kind != 'file':
 
685
            raise NotImplementedError("Conflict is not a file")
 
686
        conflict_markers_in_line = self._conflict_re.search
 
687
        # GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
 
688
        with tree.get_file(self.path) as f:
 
689
            for line in f:
 
690
                if conflict_markers_in_line(line):
 
691
                    raise NotImplementedError("Conflict markers present")
 
692
 
 
693
    def action_take_this(self, tree):
 
694
        self._resolve_with_cleanups(tree, 'THIS')
 
695
 
 
696
    def action_take_other(self, tree):
 
697
        self._resolve_with_cleanups(tree, 'OTHER')
 
698
 
 
699
 
 
700
class HandledConflict(Conflict):
 
701
    """A path problem that has been provisionally resolved.
 
702
    This is intended to be a base class.
 
703
    """
 
704
 
 
705
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
706
 
 
707
    def __init__(self, action, path, file_id=None):
 
708
        Conflict.__init__(self, path, file_id)
 
709
        self.action = action
 
710
 
 
711
    def _cmp_list(self):
 
712
        return Conflict._cmp_list(self) + [self.action]
 
713
 
 
714
    def as_stanza(self):
 
715
        s = Conflict.as_stanza(self)
 
716
        s.add('action', self.action)
 
717
        return s
 
718
 
 
719
    def associated_filenames(self):
 
720
        # Nothing has been generated here
 
721
        return []
 
722
 
 
723
 
 
724
class HandledPathConflict(HandledConflict):
 
725
    """A provisionally-resolved path problem involving two paths.
 
726
    This is intended to be a base class.
 
727
    """
 
728
 
 
729
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
730
        " %(file_id)r, %(conflict_file_id)r)"
 
731
 
 
732
    def __init__(self, action, path, conflict_path, file_id=None,
 
733
                 conflict_file_id=None):
 
734
        HandledConflict.__init__(self, action, path, file_id)
 
735
        self.conflict_path = conflict_path
 
736
        # the factory blindly transfers the Stanza values to __init__,
 
737
        # so they can be unicode.
 
738
        if isinstance(conflict_file_id, str):
 
739
            conflict_file_id = cache_utf8.encode(conflict_file_id)
 
740
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id)
 
741
 
 
742
    def _cmp_list(self):
 
743
        return HandledConflict._cmp_list(self) + [self.conflict_path,
 
744
                                                  self.conflict_file_id]
 
745
 
 
746
    def as_stanza(self):
 
747
        s = HandledConflict.as_stanza(self)
 
748
        s.add('conflict_path', self.conflict_path)
 
749
        if self.conflict_file_id is not None:
 
750
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
751
 
 
752
        return s
 
753
 
 
754
 
 
755
class DuplicateID(HandledPathConflict):
 
756
    """Two files want the same file_id."""
 
757
 
 
758
    typestring = 'duplicate id'
 
759
 
 
760
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
761
 
 
762
 
 
763
class DuplicateEntry(HandledPathConflict):
 
764
    """Two directory entries want to have the same name."""
 
765
 
 
766
    typestring = 'duplicate'
 
767
 
 
768
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
769
 
 
770
    def action_take_this(self, tree):
 
771
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
772
        tree.rename_one(self.path, self.conflict_path)
 
773
 
 
774
    def action_take_other(self, tree):
 
775
        tree.remove([self.path], force=True, keep_files=False)
 
776
 
 
777
 
 
778
class ParentLoop(HandledPathConflict):
 
779
    """An attempt to create an infinitely-looping directory structure.
 
780
    This is rare, but can be produced like so:
 
781
 
 
782
    tree A:
 
783
      mv foo bar
 
784
    tree B:
 
785
      mv bar foo
 
786
    merge A and B
 
787
    """
 
788
 
 
789
    typestring = 'parent loop'
 
790
 
 
791
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
792
 
 
793
    def action_take_this(self, tree):
 
794
        # just acccept brz proposal
 
795
        pass
 
796
 
 
797
    def action_take_other(self, tree):
 
798
        with tree.get_transform() as tt:
 
799
            p_tid = tt.trans_id_file_id(self.file_id)
 
800
            parent_tid = tt.get_tree_parent(p_tid)
 
801
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
 
802
            cparent_tid = tt.get_tree_parent(cp_tid)
 
803
            tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
 
804
            tt.adjust_path(osutils.basename(self.conflict_path),
 
805
                           parent_tid, p_tid)
 
806
            tt.apply()
 
807
 
 
808
 
 
809
class UnversionedParent(HandledConflict):
 
810
    """An attempt to version a file whose parent directory is not versioned.
 
811
    Typically, the result of a merge where one tree unversioned the directory
 
812
    and the other added a versioned file to it.
 
813
    """
 
814
 
 
815
    typestring = 'unversioned parent'
 
816
 
 
817
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
818
             ' children.  %(action)s.'
 
819
 
 
820
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
821
    # conflict shouldn't exist (the long story is that the conflict is
 
822
    # generated with another one that can be resolved properly) -- vila 091224
 
823
    def action_take_this(self, tree):
 
824
        pass
 
825
 
 
826
    def action_take_other(self, tree):
 
827
        pass
 
828
 
 
829
 
 
830
class MissingParent(HandledConflict):
 
831
    """An attempt to add files to a directory that is not present.
 
832
    Typically, the result of a merge where THIS deleted the directory and
 
833
    the OTHER added a file to it.
 
834
    See also: DeletingParent (same situation, THIS and OTHER reversed)
 
835
    """
 
836
 
 
837
    typestring = 'missing parent'
 
838
 
 
839
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
840
 
 
841
    def action_take_this(self, tree):
 
842
        tree.remove([self.path], force=True, keep_files=False)
 
843
 
 
844
    def action_take_other(self, tree):
 
845
        # just acccept brz proposal
 
846
        pass
 
847
 
 
848
 
 
849
class DeletingParent(HandledConflict):
 
850
    """An attempt to add files to a directory that is not present.
 
851
    Typically, the result of a merge where one OTHER deleted the directory and
 
852
    the THIS added a file to it.
 
853
    """
 
854
 
 
855
    typestring = 'deleting parent'
 
856
 
 
857
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
858
             "%(action)s."
 
859
 
 
860
    # FIXME: It's a bit strange that the default action is not coherent with
 
861
    # MissingParent from the *user* pov.
 
862
 
 
863
    def action_take_this(self, tree):
 
864
        # just acccept brz proposal
 
865
        pass
 
866
 
 
867
    def action_take_other(self, tree):
 
868
        tree.remove([self.path], force=True, keep_files=False)
 
869
 
 
870
 
 
871
class NonDirectoryParent(HandledConflict):
 
872
    """An attempt to add files to a directory that is not a directory or
 
873
    an attempt to change the kind of a directory with files.
 
874
    """
 
875
 
 
876
    typestring = 'non-directory parent'
 
877
 
 
878
    format = "Conflict: %(path)s is not a directory, but has files in it."\
 
879
             "  %(action)s."
 
880
 
 
881
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
882
 
 
883
    def action_take_this(self, tree):
 
884
        # FIXME: we should preserve that path when the conflict is generated !
 
885
        if self.path.endswith('.new'):
 
886
            conflict_path = self.path[:-(len('.new'))]
 
887
            tree.remove([self.path], force=True, keep_files=False)
 
888
            tree.add(conflict_path)
 
889
        else:
 
890
            raise NotImplementedError(self.action_take_this)
 
891
 
 
892
    def action_take_other(self, tree):
 
893
        # FIXME: we should preserve that path when the conflict is generated !
 
894
        if self.path.endswith('.new'):
 
895
            conflict_path = self.path[:-(len('.new'))]
 
896
            tree.remove([conflict_path], force=True, keep_files=False)
 
897
            tree.rename_one(self.path, conflict_path)
 
898
        else:
 
899
            raise NotImplementedError(self.action_take_other)
 
900
 
 
901
 
 
902
ctype = {}
 
903
 
 
904
 
 
905
def register_types(*conflict_types):
 
906
    """Register a Conflict subclass for serialization purposes"""
 
907
    global ctype
 
908
    for conflict_type in conflict_types:
 
909
        ctype[conflict_type.typestring] = conflict_type
 
910
 
 
911
 
 
912
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
913
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
914
               DeletingParent, NonDirectoryParent)