~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Vincent Ladeuil
  • Date: 2010-10-26 08:08:23 UTC
  • mfrom: (5514.1.1 665100-content-type)
  • mto: This revision was merged to the branch mainline in revision 5516.
  • Revision ID: v.ladeuil+lp@free.fr-20101026080823-3wggo03b7cpn9908
Correctly set the Content-Type header when POSTing http requests

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Aaron Bentley
2
 
 
 
1
# Copyright (C) 2005, 2006, 2007, 2009, 2010 Canonical Ltd
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
# TODO: Move this into builtins
18
 
 
19
 
# TODO: 'bzr resolve' should accept a directory name and work from that 
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
# TODO: 'bzr resolve' should accept a directory name and work from that
20
18
# point down
21
19
 
22
20
import os
 
21
 
 
22
from bzrlib.lazy_import import lazy_import
 
23
lazy_import(globals(), """
23
24
import errno
24
25
 
25
 
import bzrlib
26
 
from bzrlib.commands import register_command
27
 
from bzrlib.errors import BzrCommandError, NotConflicted, UnsupportedOperation
28
 
from bzrlib.option import Option
29
 
from bzrlib.osutils import rename, delete_any
30
 
from bzrlib.rio import Stanza
 
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
    )
31
41
 
32
42
 
33
43
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
34
44
 
35
45
 
36
 
class cmd_conflicts(bzrlib.commands.Command):
37
 
    """List files with conflicts.
 
46
class cmd_conflicts(commands.Command):
 
47
    __doc__ = """List files with conflicts.
38
48
 
39
49
    Merge will do its best to combine the changes in two branches, but there
40
50
    are some kinds of problems only a human can fix.  When it encounters those,
41
51
    it will mark a conflict.  A conflict means that you need to fix something,
42
52
    before you should commit.
43
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
 
44
58
    Use bzr resolve when you have fixed a problem.
45
 
 
46
 
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
47
 
    files.)
48
 
 
49
 
    See also bzr resolve.
50
59
    """
51
 
    def run(self):
52
 
        from bzrlib.workingtree import WorkingTree
53
 
        wt = WorkingTree.open_containing(u'.')[0]
 
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]
54
69
        for conflict in wt.conflicts():
55
 
            print conflict
56
 
 
57
 
 
58
 
class cmd_resolve(bzrlib.commands.Command):
59
 
    """Mark a conflict as resolved.
 
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.
60
102
 
61
103
    Merge will do its best to combine the changes in two branches, but there
62
104
    are some kinds of problems only a human can fix.  When it encounters those,
63
105
    it will mark a conflict.  A conflict means that you need to fix something,
64
106
    before you should commit.
65
107
 
66
 
    Once you have fixed a problem, use "bzr resolve FILE.." to mark
67
 
    individual files as fixed, or "bzr resolve --all" to mark all conflicts as
68
 
    resolved.
69
 
 
70
 
    See also bzr conflicts.
 
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.
71
111
    """
72
112
    aliases = ['resolved']
73
113
    takes_args = ['file*']
74
 
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
75
 
    def run(self, file_list=None, all=False):
76
 
        from bzrlib.workingtree import WorkingTree
 
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'.'):
77
121
        if all:
78
122
            if file_list:
79
 
                raise BzrCommandError("If --all is specified, no FILE may be provided")
80
 
            tree = WorkingTree.open_containing('.')[0]
81
 
            resolve(tree)
82
 
        else:
83
 
            if file_list is None:
84
 
                raise BzrCommandError("command 'resolve' needs one or more FILE, or --all")
85
 
            tree = WorkingTree.open_containing(file_list[0])[0]
86
 
            to_resolve = [tree.relpath(p) for p in file_list]
87
 
            resolve(tree, to_resolve)
88
 
 
89
 
 
90
 
def resolve(tree, paths=None, ignore_misses=False):
91
 
    tree.lock_write()
 
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()
92
179
    try:
93
180
        tree_conflicts = tree.conflicts()
94
181
        if paths is None:
95
182
            new_conflicts = ConflictList()
96
 
            selected_conflicts = tree_conflicts
 
183
            to_process = tree_conflicts
97
184
        else:
98
 
            new_conflicts, selected_conflicts = \
99
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses)
 
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)
100
193
        try:
101
194
            tree.set_conflicts(new_conflicts)
102
 
        except UnsupportedOperation:
 
195
        except errors.UnsupportedOperation:
103
196
            pass
104
 
        selected_conflicts.remove_files(tree)
105
197
    finally:
106
198
        tree.unlock()
107
199
 
108
200
 
109
201
def restore(filename):
110
 
    """\
111
 
    Restore a conflicted file to the state it was in before merging.
112
 
    Only text restoration supported at present.
 
202
    """Restore a conflicted file to the state it was in before merging.
 
203
 
 
204
    Only text restoration is supported at present.
113
205
    """
114
206
    conflicted = False
115
207
    try:
116
 
        rename(filename + ".THIS", filename)
 
208
        osutils.rename(filename + ".THIS", filename)
117
209
        conflicted = True
118
210
    except OSError, e:
119
211
        if e.errno != errno.ENOENT:
131
223
        if e.errno != errno.ENOENT:
132
224
            raise
133
225
    if not conflicted:
134
 
        raise NotConflicted(filename)
 
226
        raise errors.NotConflicted(filename)
135
227
 
136
228
 
137
229
class ConflictList(object):
185
277
        """Generator of stanzas"""
186
278
        for conflict in self:
187
279
            yield conflict.as_stanza()
188
 
            
 
280
 
189
281
    def to_strings(self):
190
282
        """Generate strings for the provided conflicts"""
191
283
        for conflict in self:
196
288
        for conflict in self:
197
289
            if not conflict.has_files:
198
290
                continue
199
 
            for suffix in CONFLICT_SUFFIXES:
200
 
                try:
201
 
                    delete_any(tree.abspath(conflict.path+suffix))
202
 
                except OSError, e:
203
 
                    if e.errno != errno.ENOENT:
204
 
                        raise
 
291
            conflict.cleanup(tree)
205
292
 
206
 
    def select_conflicts(self, tree, paths, ignore_misses=False):
 
293
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
294
                         recurse=False):
207
295
        """Select the conflicts associated with paths in a tree.
208
 
        
 
296
 
209
297
        File-ids are also used for this.
210
298
        :return: a pair of ConflictLists: (not_selected, selected)
211
299
        """
228
316
                if cpath in path_set:
229
317
                    selected = True
230
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
 
231
324
            for key in ('file_id', 'conflict_file_id'):
232
325
                cfile_id = getattr(conflict, key, None)
233
326
                if cfile_id is None:
250
343
                    print "%s is not conflicted" % path
251
344
        return new_conflicts, selected_conflicts
252
345
 
253
 
 
 
346
 
254
347
class Conflict(object):
255
348
    """Base class for all types of conflict"""
256
349
 
 
350
    # FIXME: cleanup should take care of that ? -- vila 091229
257
351
    has_files = False
258
352
 
259
353
    def __init__(self, path, file_id=None):
260
354
        self.path = path
261
 
        self.file_id = file_id
 
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)
262
358
 
263
359
    def as_stanza(self):
264
 
        s = Stanza(type=self.typestring, path=self.path)
 
360
        s = rio.Stanza(type=self.typestring, path=self.path)
265
361
        if self.file_id is not None:
266
 
            s.add('file_id', self.file_id)
 
362
            # Stanza requires Unicode apis
 
363
            s.add('file_id', self.file_id.decode('utf8'))
267
364
        return s
268
365
 
269
366
    def _cmp_list(self):
305
402
        else:
306
403
            return None, conflict.typestring
307
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
 
308
446
 
309
447
class PathConflict(Conflict):
310
448
    """A conflict was encountered merging file paths"""
314
452
    format = 'Path conflict: %(path)s / %(conflict_path)s'
315
453
 
316
454
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
455
 
317
456
    def __init__(self, path, conflict_path=None, file_id=None):
318
457
        Conflict.__init__(self, path, file_id)
319
458
        self.conflict_path = conflict_path
324
463
            s.add('conflict_path', self.conflict_path)
325
464
        return s
326
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
 
327
557
 
328
558
class ContentsConflict(PathConflict):
329
 
    """The files are of different types, or not present"""
 
559
    """The files are of different types (or both binary), or not present"""
330
560
 
331
561
    has_files = True
332
562
 
334
564
 
335
565
    format = 'Contents conflict in %(path)s'
336
566
 
337
 
 
 
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.
338
608
class TextConflict(PathConflict):
339
609
    """The merge algorithm could not resolve all differences encountered."""
340
610
 
344
614
 
345
615
    format = 'Text conflict in %(path)s'
346
616
 
 
617
    def associated_filenames(self):
 
618
        return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
 
619
 
347
620
 
348
621
class HandledConflict(Conflict):
349
622
    """A path problem that has been provisionally resolved.
351
624
    """
352
625
 
353
626
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
354
 
    
 
627
 
355
628
    def __init__(self, action, path, file_id=None):
356
629
        Conflict.__init__(self, path, file_id)
357
630
        self.action = action
364
637
        s.add('action', self.action)
365
638
        return s
366
639
 
 
640
    def associated_filenames(self):
 
641
        # Nothing has been generated here
 
642
        return []
 
643
 
367
644
 
368
645
class HandledPathConflict(HandledConflict):
369
646
    """A provisionally-resolved path problem involving two paths.
376
653
    def __init__(self, action, path, conflict_path, file_id=None,
377
654
                 conflict_file_id=None):
378
655
        HandledConflict.__init__(self, action, path, file_id)
379
 
        self.conflict_path = conflict_path 
380
 
        self.conflict_file_id = conflict_file_id
381
 
        
 
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
 
382
662
    def _cmp_list(self):
383
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
663
        return HandledConflict._cmp_list(self) + [self.conflict_path,
384
664
                                                  self.conflict_file_id]
385
665
 
386
666
    def as_stanza(self):
387
667
        s = HandledConflict.as_stanza(self)
388
668
        s.add('conflict_path', self.conflict_path)
389
669
        if self.conflict_file_id is not None:
390
 
            s.add('conflict_file_id', self.conflict_file_id)
391
 
            
 
670
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
671
 
392
672
        return s
393
673
 
394
674
 
407
687
 
408
688
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
409
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
 
410
697
 
411
698
class ParentLoop(HandledPathConflict):
412
699
    """An attempt to create an infinitely-looping directory structure.
413
700
    This is rare, but can be produced like so:
414
701
 
415
702
    tree A:
416
 
      mv foo/bar
 
703
      mv foo bar
417
704
    tree B:
418
 
      mv bar/foo
 
705
      mv bar foo
419
706
    merge A and B
420
707
    """
421
708
 
422
709
    typestring = 'parent loop'
423
710
 
424
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
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()
425
733
 
426
734
 
427
735
class UnversionedParent(HandledConflict):
428
 
    """An attempt to version an file whose parent directory is not versioned.
 
736
    """An attempt to version a file whose parent directory is not versioned.
429
737
    Typically, the result of a merge where one tree unversioned the directory
430
738
    and the other added a versioned file to it.
431
739
    """
432
740
 
433
741
    typestring = 'unversioned parent'
434
742
 
435
 
    format = 'Conflict adding versioned files to %(path)s.  %(action)s.'
 
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
436
754
 
437
755
 
438
756
class MissingParent(HandledConflict):
439
757
    """An attempt to add files to a directory that is not present.
440
 
    Typically, the result of a merge where one tree deleted the directory and
441
 
    the other added a file to it.
 
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)
442
761
    """
443
762
 
444
763
    typestring = 'missing parent'
445
764
 
446
765
    format = 'Conflict adding files to %(path)s.  %(action)s.'
447
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)
448
826
 
449
827
 
450
828
ctype = {}
456
834
    for conflict_type in conflict_types:
457
835
        ctype[conflict_type.typestring] = conflict_type
458
836
 
459
 
 
460
837
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
461
 
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)
 
838
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
839
               DeletingParent, NonDirectoryParent)