/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/bzr/conflicts.py

  • Committer: Jelmer Vernooij
  • Date: 2020-08-10 15:00:17 UTC
  • mfrom: (7490.40.99 work)
  • mto: This revision was merged to the branch mainline in revision 7521.
  • Revision ID: jelmer@jelmer.uk-20200810150017-vs7xnrd1vat4iktg
Merge lp:brz/3.1.

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
from __future__ import absolute_import
 
18
 
 
19
import errno
 
20
import os
 
21
import re
 
22
 
 
23
from ..lazy_import import lazy_import
 
24
lazy_import(globals(), """
 
25
 
 
26
from breezy import (
 
27
    cache_utf8,
 
28
    errors,
 
29
    rio,
 
30
    transform,
 
31
    osutils,
 
32
    )
 
33
""")
 
34
 
 
35
from ..conflicts import (
 
36
    Conflict as BaseConflict,
 
37
    ConflictList as BaseConflictList,
 
38
    )
 
39
from ..sixish import text_type
 
40
 
 
41
 
 
42
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
43
 
 
44
 
 
45
class Conflict(BaseConflict):
 
46
    """Base class for all types of conflict"""
 
47
 
 
48
    # FIXME: cleanup should take care of that ? -- vila 091229
 
49
    has_files = False
 
50
 
 
51
    def __init__(self, path, file_id=None):
 
52
        super(Conflict, self).__init__(path)
 
53
        # the factory blindly transfers the Stanza values to __init__ and
 
54
        # Stanza is purely a Unicode api.
 
55
        if isinstance(file_id, text_type):
 
56
            file_id = cache_utf8.encode(file_id)
 
57
        self.file_id = file_id
 
58
 
 
59
    def as_stanza(self):
 
60
        s = rio.Stanza(type=self.typestring, path=self.path)
 
61
        if self.file_id is not None:
 
62
            # Stanza requires Unicode apis
 
63
            s.add('file_id', self.file_id.decode('utf8'))
 
64
        return s
 
65
 
 
66
    def _cmp_list(self):
 
67
        return [type(self), self.path, self.file_id]
 
68
 
 
69
    def __cmp__(self, other):
 
70
        if getattr(other, "_cmp_list", None) is None:
 
71
            return -1
 
72
        x = self._cmp_list()
 
73
        y = other._cmp_list()
 
74
        return (x > y) - (x < y)
 
75
 
 
76
    def __hash__(self):
 
77
        return hash((type(self), self.path, self.file_id))
 
78
 
 
79
    def __eq__(self, other):
 
80
        return self.__cmp__(other) == 0
 
81
 
 
82
    def __ne__(self, other):
 
83
        return not self.__eq__(other)
 
84
 
 
85
    def __unicode__(self):
 
86
        return self.describe()
 
87
 
 
88
    def __str__(self):
 
89
        return self.describe()
 
90
 
 
91
    def describe(self):
 
92
        return self.format % self.__dict__
 
93
 
 
94
    def __repr__(self):
 
95
        rdict = dict(self.__dict__)
 
96
        rdict['class'] = self.__class__.__name__
 
97
        return self.rformat % rdict
 
98
 
 
99
    @staticmethod
 
100
    def factory(type, **kwargs):
 
101
        global ctype
 
102
        return ctype[type](**kwargs)
 
103
 
 
104
    @staticmethod
 
105
    def sort_key(conflict):
 
106
        if conflict.path is not None:
 
107
            return conflict.path, conflict.typestring
 
108
        elif getattr(conflict, "conflict_path", None) is not None:
 
109
            return conflict.conflict_path, conflict.typestring
 
110
        else:
 
111
            return None, conflict.typestring
 
112
 
 
113
    def do(self, action, tree):
 
114
        """Apply the specified action to the conflict.
 
115
 
 
116
        :param action: The method name to call.
 
117
 
 
118
        :param tree: The tree passed as a parameter to the method.
 
119
        """
 
120
        meth = getattr(self, 'action_%s' % action, None)
 
121
        if meth is None:
 
122
            raise NotImplementedError(self.__class__.__name__ + '.' + action)
 
123
        meth(tree)
 
124
 
 
125
    def action_auto(self, tree):
 
126
        raise NotImplementedError(self.action_auto)
 
127
 
 
128
    def action_done(self, tree):
 
129
        """Mark the conflict as solved once it has been handled."""
 
130
        # This method does nothing but simplifies the design of upper levels.
 
131
        pass
 
132
 
 
133
    def action_take_this(self, tree):
 
134
        raise NotImplementedError(self.action_take_this)
 
135
 
 
136
    def action_take_other(self, tree):
 
137
        raise NotImplementedError(self.action_take_other)
 
138
 
 
139
    def _resolve_with_cleanups(self, tree, *args, **kwargs):
 
140
        with tree.transform() as tt:
 
141
            self._resolve(tt, *args, **kwargs)
 
142
 
 
143
 
 
144
class ConflictList(BaseConflictList):
 
145
 
 
146
    @staticmethod
 
147
    def from_stanzas(stanzas):
 
148
        """Produce a new ConflictList from an iterable of stanzas"""
 
149
        conflicts = ConflictList()
 
150
        for stanza in stanzas:
 
151
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
152
        return conflicts
 
153
 
 
154
    def to_stanzas(self):
 
155
        """Generator of stanzas"""
 
156
        for conflict in self:
 
157
            yield conflict.as_stanza()
 
158
 
 
159
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
160
                         recurse=False):
 
161
        """Select the conflicts associated with paths in a tree.
 
162
 
 
163
        File-ids are also used for this.
 
164
        :return: a pair of ConflictLists: (not_selected, selected)
 
165
        """
 
166
        path_set = set(paths)
 
167
        ids = {}
 
168
        selected_paths = set()
 
169
        new_conflicts = ConflictList()
 
170
        selected_conflicts = ConflictList()
 
171
        for path in paths:
 
172
            file_id = tree.path2id(path)
 
173
            if file_id is not None:
 
174
                ids[file_id] = path
 
175
 
 
176
        for conflict in self:
 
177
            selected = False
 
178
            for key in ('path', 'conflict_path'):
 
179
                cpath = getattr(conflict, key, None)
 
180
                if cpath is None:
 
181
                    continue
 
182
                if cpath in path_set:
 
183
                    selected = True
 
184
                    selected_paths.add(cpath)
 
185
                if recurse:
 
186
                    if osutils.is_inside_any(path_set, cpath):
 
187
                        selected = True
 
188
                        selected_paths.add(cpath)
 
189
 
 
190
            for key in ('file_id', 'conflict_file_id'):
 
191
                cfile_id = getattr(conflict, key, None)
 
192
                if cfile_id is None:
 
193
                    continue
 
194
                try:
 
195
                    cpath = ids[cfile_id]
 
196
                except KeyError:
 
197
                    continue
 
198
                selected = True
 
199
                selected_paths.add(cpath)
 
200
            if selected:
 
201
                selected_conflicts.append(conflict)
 
202
            else:
 
203
                new_conflicts.append(conflict)
 
204
        if ignore_misses is not True:
 
205
            for path in [p for p in paths if p not in selected_paths]:
 
206
                if not os.path.exists(tree.abspath(path)):
 
207
                    print("%s does not exist" % path)
 
208
                else:
 
209
                    print("%s is not conflicted" % path)
 
210
        return new_conflicts, selected_conflicts
 
211
 
 
212
 
 
213
 
 
214
 
 
215
class PathConflict(Conflict):
 
216
    """A conflict was encountered merging file paths"""
 
217
 
 
218
    typestring = 'path conflict'
 
219
 
 
220
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
221
 
 
222
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
223
 
 
224
    def __init__(self, path, conflict_path=None, file_id=None):
 
225
        Conflict.__init__(self, path, file_id)
 
226
        self.conflict_path = conflict_path
 
227
 
 
228
    def as_stanza(self):
 
229
        s = Conflict.as_stanza(self)
 
230
        if self.conflict_path is not None:
 
231
            s.add('conflict_path', self.conflict_path)
 
232
        return s
 
233
 
 
234
    def associated_filenames(self):
 
235
        # No additional files have been generated here
 
236
        return []
 
237
 
 
238
    def _resolve(self, tt, file_id, path, winner):
 
239
        """Resolve the conflict.
 
240
 
 
241
        :param tt: The TreeTransform where the conflict is resolved.
 
242
        :param file_id: The retained file id.
 
243
        :param path: The retained path.
 
244
        :param winner: 'this' or 'other' indicates which side is the winner.
 
245
        """
 
246
        path_to_create = None
 
247
        if winner == 'this':
 
248
            if self.path == '<deleted>':
 
249
                return  # Nothing to do
 
250
            if self.conflict_path == '<deleted>':
 
251
                path_to_create = self.path
 
252
                revid = tt._tree.get_parent_ids()[0]
 
253
        elif winner == 'other':
 
254
            if self.conflict_path == '<deleted>':
 
255
                return  # Nothing to do
 
256
            if self.path == '<deleted>':
 
257
                path_to_create = self.conflict_path
 
258
                # FIXME: If there are more than two parents we may need to
 
259
                # iterate. Taking the last parent is the safer bet in the mean
 
260
                # time. -- vila 20100309
 
261
                revid = tt._tree.get_parent_ids()[-1]
 
262
        else:
 
263
            # Programmer error
 
264
            raise AssertionError('bad winner: %r' % (winner,))
 
265
        if path_to_create is not None:
 
266
            tid = tt.trans_id_tree_path(path_to_create)
 
267
            tree = self._revision_tree(tt._tree, revid)
 
268
            transform.create_from_tree(
 
269
                tt, tid, tree, tree.id2path(file_id))
 
270
            tt.version_file(tid, file_id=file_id)
 
271
        else:
 
272
            tid = tt.trans_id_file_id(file_id)
 
273
        # Adjust the path for the retained file id
 
274
        parent_tid = tt.get_tree_parent(tid)
 
275
        tt.adjust_path(osutils.basename(path), parent_tid, tid)
 
276
        tt.apply()
 
277
 
 
278
    def _revision_tree(self, tree, revid):
 
279
        return tree.branch.repository.revision_tree(revid)
 
280
 
 
281
    def _infer_file_id(self, tree):
 
282
        # Prior to bug #531967, file_id wasn't always set, there may still be
 
283
        # conflict files in the wild so we need to cope with them
 
284
        # Establish which path we should use to find back the file-id
 
285
        possible_paths = []
 
286
        for p in (self.path, self.conflict_path):
 
287
            if p == '<deleted>':
 
288
                # special hard-coded path
 
289
                continue
 
290
            if p is not None:
 
291
                possible_paths.append(p)
 
292
        # Search the file-id in the parents with any path available
 
293
        file_id = None
 
294
        for revid in tree.get_parent_ids():
 
295
            revtree = self._revision_tree(tree, revid)
 
296
            for p in possible_paths:
 
297
                file_id = revtree.path2id(p)
 
298
                if file_id is not None:
 
299
                    return revtree, file_id
 
300
        return None, None
 
301
 
 
302
    def action_take_this(self, tree):
 
303
        if self.file_id is not None:
 
304
            self._resolve_with_cleanups(tree, self.file_id, self.path,
 
305
                                        winner='this')
 
306
        else:
 
307
            # Prior to bug #531967 we need to find back the file_id and restore
 
308
            # the content from there
 
309
            revtree, file_id = self._infer_file_id(tree)
 
310
            tree.revert([revtree.id2path(file_id)],
 
311
                        old_tree=revtree, backups=False)
 
312
 
 
313
    def action_take_other(self, tree):
 
314
        if self.file_id is not None:
 
315
            self._resolve_with_cleanups(tree, self.file_id,
 
316
                                        self.conflict_path,
 
317
                                        winner='other')
 
318
        else:
 
319
            # Prior to bug #531967 we need to find back the file_id and restore
 
320
            # the content from there
 
321
            revtree, file_id = self._infer_file_id(tree)
 
322
            tree.revert([revtree.id2path(file_id)],
 
323
                        old_tree=revtree, backups=False)
 
324
 
 
325
 
 
326
class ContentsConflict(PathConflict):
 
327
    """The files are of different types (or both binary), or not present"""
 
328
 
 
329
    has_files = True
 
330
 
 
331
    typestring = 'contents conflict'
 
332
 
 
333
    format = 'Contents conflict in %(path)s'
 
334
 
 
335
    def associated_filenames(self):
 
336
        return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
 
337
 
 
338
    def _resolve(self, tt, suffix_to_remove):
 
339
        """Resolve the conflict.
 
340
 
 
341
        :param tt: The TreeTransform where the conflict is resolved.
 
342
        :param suffix_to_remove: Either 'THIS' or 'OTHER'
 
343
 
 
344
        The resolution is symmetric: when taking THIS, OTHER is deleted and
 
345
        item.THIS is renamed into item and vice-versa.
 
346
        """
 
347
        try:
 
348
            # Delete 'item.THIS' or 'item.OTHER' depending on
 
349
            # suffix_to_remove
 
350
            tt.delete_contents(
 
351
                tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
 
352
        except errors.NoSuchFile:
 
353
            # There are valid cases where 'item.suffix_to_remove' either
 
354
            # never existed or was already deleted (including the case
 
355
            # where the user deleted it)
 
356
            pass
 
357
        try:
 
358
            this_path = tt._tree.id2path(self.file_id)
 
359
        except errors.NoSuchId:
 
360
            # The file is not present anymore. This may happen if the user
 
361
            # deleted the file either manually or when resolving a conflict on
 
362
            # the parent.  We may raise some exception to indicate that the
 
363
            # conflict doesn't exist anymore and as such doesn't need to be
 
364
            # resolved ? -- vila 20110615
 
365
            this_tid = None
 
366
        else:
 
367
            this_tid = tt.trans_id_tree_path(this_path)
 
368
        if this_tid is not None:
 
369
            # Rename 'item.suffix_to_remove' (note that if
 
370
            # 'item.suffix_to_remove' has been deleted, this is a no-op)
 
371
            parent_tid = tt.get_tree_parent(this_tid)
 
372
            tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
 
373
            tt.apply()
 
374
 
 
375
    def action_take_this(self, tree):
 
376
        self._resolve_with_cleanups(tree, 'OTHER')
 
377
 
 
378
    def action_take_other(self, tree):
 
379
        self._resolve_with_cleanups(tree, 'THIS')
 
380
 
 
381
 
 
382
# TODO: There should be a base revid attribute to better inform the user about
 
383
# how the conflicts were generated.
 
384
class TextConflict(Conflict):
 
385
    """The merge algorithm could not resolve all differences encountered."""
 
386
 
 
387
    has_files = True
 
388
 
 
389
    typestring = 'text conflict'
 
390
 
 
391
    format = 'Text conflict in %(path)s'
 
392
 
 
393
    rformat = '%(class)s(%(path)r, %(file_id)r)'
 
394
 
 
395
    _conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
 
396
 
 
397
    def associated_filenames(self):
 
398
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
399
 
 
400
    def _resolve(self, tt, winner_suffix):
 
401
        """Resolve the conflict by copying one of .THIS or .OTHER into file.
 
402
 
 
403
        :param tt: The TreeTransform where the conflict is resolved.
 
404
        :param winner_suffix: Either 'THIS' or 'OTHER'
 
405
 
 
406
        The resolution is symmetric, when taking THIS, item.THIS is renamed
 
407
        into item and vice-versa. This takes one of the files as a whole
 
408
        ignoring every difference that could have been merged cleanly.
 
409
        """
 
410
        # To avoid useless copies, we switch item and item.winner_suffix, only
 
411
        # item will exist after the conflict has been resolved anyway.
 
412
        item_tid = tt.trans_id_file_id(self.file_id)
 
413
        item_parent_tid = tt.get_tree_parent(item_tid)
 
414
        winner_path = self.path + '.' + winner_suffix
 
415
        winner_tid = tt.trans_id_tree_path(winner_path)
 
416
        winner_parent_tid = tt.get_tree_parent(winner_tid)
 
417
        # Switch the paths to preserve the content
 
418
        tt.adjust_path(osutils.basename(self.path),
 
419
                       winner_parent_tid, winner_tid)
 
420
        tt.adjust_path(osutils.basename(winner_path),
 
421
                       item_parent_tid, item_tid)
 
422
        # Associate the file_id to the right content
 
423
        tt.unversion_file(item_tid)
 
424
        tt.version_file(winner_tid, file_id=self.file_id)
 
425
        tt.apply()
 
426
 
 
427
    def action_auto(self, tree):
 
428
        # GZ 2012-07-27: Using NotImplementedError to signal that a conflict
 
429
        #                can't be auto resolved does not seem ideal.
 
430
        try:
 
431
            kind = tree.kind(self.path)
 
432
        except errors.NoSuchFile:
 
433
            return
 
434
        if kind != 'file':
 
435
            raise NotImplementedError("Conflict is not a file")
 
436
        conflict_markers_in_line = self._conflict_re.search
 
437
        # GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
 
438
        with tree.get_file(self.path) as f:
 
439
            for line in f:
 
440
                if conflict_markers_in_line(line):
 
441
                    raise NotImplementedError("Conflict markers present")
 
442
 
 
443
    def action_take_this(self, tree):
 
444
        self._resolve_with_cleanups(tree, 'THIS')
 
445
 
 
446
    def action_take_other(self, tree):
 
447
        self._resolve_with_cleanups(tree, 'OTHER')
 
448
 
 
449
 
 
450
class HandledConflict(Conflict):
 
451
    """A path problem that has been provisionally resolved.
 
452
    This is intended to be a base class.
 
453
    """
 
454
 
 
455
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
456
 
 
457
    def __init__(self, action, path, file_id=None):
 
458
        Conflict.__init__(self, path, file_id)
 
459
        self.action = action
 
460
 
 
461
    def _cmp_list(self):
 
462
        return Conflict._cmp_list(self) + [self.action]
 
463
 
 
464
    def as_stanza(self):
 
465
        s = Conflict.as_stanza(self)
 
466
        s.add('action', self.action)
 
467
        return s
 
468
 
 
469
    def associated_filenames(self):
 
470
        # Nothing has been generated here
 
471
        return []
 
472
 
 
473
 
 
474
class HandledPathConflict(HandledConflict):
 
475
    """A provisionally-resolved path problem involving two paths.
 
476
    This is intended to be a base class.
 
477
    """
 
478
 
 
479
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
480
        " %(file_id)r, %(conflict_file_id)r)"
 
481
 
 
482
    def __init__(self, action, path, conflict_path, file_id=None,
 
483
                 conflict_file_id=None):
 
484
        HandledConflict.__init__(self, action, path, file_id)
 
485
        self.conflict_path = conflict_path
 
486
        # the factory blindly transfers the Stanza values to __init__,
 
487
        # so they can be unicode.
 
488
        if isinstance(conflict_file_id, text_type):
 
489
            conflict_file_id = cache_utf8.encode(conflict_file_id)
 
490
        self.conflict_file_id = conflict_file_id
 
491
 
 
492
    def _cmp_list(self):
 
493
        return HandledConflict._cmp_list(self) + [self.conflict_path,
 
494
                                                  self.conflict_file_id]
 
495
 
 
496
    def as_stanza(self):
 
497
        s = HandledConflict.as_stanza(self)
 
498
        s.add('conflict_path', self.conflict_path)
 
499
        if self.conflict_file_id is not None:
 
500
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
501
 
 
502
        return s
 
503
 
 
504
 
 
505
class DuplicateID(HandledPathConflict):
 
506
    """Two files want the same file_id."""
 
507
 
 
508
    typestring = 'duplicate id'
 
509
 
 
510
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
511
 
 
512
 
 
513
class DuplicateEntry(HandledPathConflict):
 
514
    """Two directory entries want to have the same name."""
 
515
 
 
516
    typestring = 'duplicate'
 
517
 
 
518
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
519
 
 
520
    def action_take_this(self, tree):
 
521
        tree.remove([self.conflict_path], force=True, keep_files=False)
 
522
        tree.rename_one(self.path, self.conflict_path)
 
523
 
 
524
    def action_take_other(self, tree):
 
525
        tree.remove([self.path], force=True, keep_files=False)
 
526
 
 
527
 
 
528
class ParentLoop(HandledPathConflict):
 
529
    """An attempt to create an infinitely-looping directory structure.
 
530
    This is rare, but can be produced like so:
 
531
 
 
532
    tree A:
 
533
      mv foo bar
 
534
    tree B:
 
535
      mv bar foo
 
536
    merge A and B
 
537
    """
 
538
 
 
539
    typestring = 'parent loop'
 
540
 
 
541
    format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
 
542
 
 
543
    def action_take_this(self, tree):
 
544
        # just acccept brz proposal
 
545
        pass
 
546
 
 
547
    def action_take_other(self, tree):
 
548
        with tree.transform() as tt:
 
549
            p_tid = tt.trans_id_file_id(self.file_id)
 
550
            parent_tid = tt.get_tree_parent(p_tid)
 
551
            cp_tid = tt.trans_id_file_id(self.conflict_file_id)
 
552
            cparent_tid = tt.get_tree_parent(cp_tid)
 
553
            tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
 
554
            tt.adjust_path(osutils.basename(self.conflict_path),
 
555
                           parent_tid, p_tid)
 
556
            tt.apply()
 
557
 
 
558
 
 
559
class UnversionedParent(HandledConflict):
 
560
    """An attempt to version a file whose parent directory is not versioned.
 
561
    Typically, the result of a merge where one tree unversioned the directory
 
562
    and the other added a versioned file to it.
 
563
    """
 
564
 
 
565
    typestring = 'unversioned parent'
 
566
 
 
567
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
568
             ' children.  %(action)s.'
 
569
 
 
570
    # FIXME: We silently do nothing to make tests pass, but most probably the
 
571
    # conflict shouldn't exist (the long story is that the conflict is
 
572
    # generated with another one that can be resolved properly) -- vila 091224
 
573
    def action_take_this(self, tree):
 
574
        pass
 
575
 
 
576
    def action_take_other(self, tree):
 
577
        pass
 
578
 
 
579
 
 
580
class MissingParent(HandledConflict):
 
581
    """An attempt to add files to a directory that is not present.
 
582
    Typically, the result of a merge where THIS deleted the directory and
 
583
    the OTHER added a file to it.
 
584
    See also: DeletingParent (same situation, THIS and OTHER reversed)
 
585
    """
 
586
 
 
587
    typestring = 'missing parent'
 
588
 
 
589
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
590
 
 
591
    def action_take_this(self, tree):
 
592
        tree.remove([self.path], force=True, keep_files=False)
 
593
 
 
594
    def action_take_other(self, tree):
 
595
        # just acccept brz proposal
 
596
        pass
 
597
 
 
598
 
 
599
class DeletingParent(HandledConflict):
 
600
    """An attempt to add files to a directory that is not present.
 
601
    Typically, the result of a merge where one OTHER deleted the directory and
 
602
    the THIS added a file to it.
 
603
    """
 
604
 
 
605
    typestring = 'deleting parent'
 
606
 
 
607
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
608
             "%(action)s."
 
609
 
 
610
    # FIXME: It's a bit strange that the default action is not coherent with
 
611
    # MissingParent from the *user* pov.
 
612
 
 
613
    def action_take_this(self, tree):
 
614
        # just acccept brz proposal
 
615
        pass
 
616
 
 
617
    def action_take_other(self, tree):
 
618
        tree.remove([self.path], force=True, keep_files=False)
 
619
 
 
620
 
 
621
class NonDirectoryParent(HandledConflict):
 
622
    """An attempt to add files to a directory that is not a directory or
 
623
    an attempt to change the kind of a directory with files.
 
624
    """
 
625
 
 
626
    typestring = 'non-directory parent'
 
627
 
 
628
    format = "Conflict: %(path)s is not a directory, but has files in it."\
 
629
             "  %(action)s."
 
630
 
 
631
    # FIXME: .OTHER should be used instead of .new when the conflict is created
 
632
 
 
633
    def action_take_this(self, tree):
 
634
        # FIXME: we should preserve that path when the conflict is generated !
 
635
        if self.path.endswith('.new'):
 
636
            conflict_path = self.path[:-(len('.new'))]
 
637
            tree.remove([self.path], force=True, keep_files=False)
 
638
            tree.add(conflict_path)
 
639
        else:
 
640
            raise NotImplementedError(self.action_take_this)
 
641
 
 
642
    def action_take_other(self, tree):
 
643
        # FIXME: we should preserve that path when the conflict is generated !
 
644
        if self.path.endswith('.new'):
 
645
            conflict_path = self.path[:-(len('.new'))]
 
646
            tree.remove([conflict_path], force=True, keep_files=False)
 
647
            tree.rename_one(self.path, conflict_path)
 
648
        else:
 
649
            raise NotImplementedError(self.action_take_other)
 
650
 
 
651
 
 
652
ctype = {}
 
653
 
 
654
 
 
655
def register_types(*conflict_types):
 
656
    """Register a Conflict subclass for serialization purposes"""
 
657
    global ctype
 
658
    for conflict_type in conflict_types:
 
659
        ctype[conflict_type.typestring] = conflict_type
 
660
 
 
661
 
 
662
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
663
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
664
               DeletingParent, NonDirectoryParent)