~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

Tree Transform-based merger

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
import os
19
19
import errno
 
20
from shutil import rmtree
 
21
from tempfile import mkdtemp
20
22
 
21
23
import bzrlib
22
 
from bzrlib._changeset import generate_changeset, ExceptionConflictHandler
23
 
from bzrlib._changeset import Inventory, Diff3Merge, ReplaceContents
24
 
from bzrlib._merge_core import WeaveMerge
25
 
from bzrlib._merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
26
24
from bzrlib.branch import Branch
27
25
from bzrlib.delta import compare_trees
28
26
from bzrlib.errors import (BzrCommandError,
30
28
                           NoCommonAncestor,
31
29
                           NoCommits,
32
30
                           NoSuchRevision,
 
31
                           NoSuchFile,
33
32
                           NotBranchError,
34
33
                           NotVersionedError,
35
34
                           UnrelatedBranches,
36
35
                           WorkingTreeNotRevision,
37
36
                           )
38
37
from bzrlib.fetch import greedy_fetch, fetch
 
38
from bzrlib.merge3 import Merge3
39
39
import bzrlib.osutils
40
40
from bzrlib.osutils import rename, pathjoin
41
41
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
 
42
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
 
43
                              conflicts_strings, FinalPaths, create_by_entry,
 
44
                              unique_add)
42
45
from bzrlib.trace import mutter, warning, note
43
46
 
44
47
# TODO: Report back as changes are merged in
45
48
 
46
 
# comments from abentley on irc: merge happens in two stages, each
47
 
# of which generates a changeset object
48
 
 
49
 
# stage 1: generate OLD->OTHER,
50
 
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
51
 
 
52
 
class _MergeConflictHandler(ExceptionConflictHandler):
53
 
    """Handle conflicts encountered while merging.
54
 
 
55
 
    This subclasses ExceptionConflictHandler, so that any types of
56
 
    conflict that are not explicitly handled cause an exception and
57
 
    terminate the merge.
58
 
    """
59
 
    def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
60
 
        ExceptionConflictHandler.__init__(self)
61
 
        self.conflicts = 0
62
 
        self.ignore_zero = ignore_zero
63
 
        self.this_tree = this_tree
64
 
        self.base_tree = base_tree
65
 
        self.other_tree = other_tree
66
 
 
67
 
    def copy(self, source, dest):
68
 
        """Copy the text and mode of a file
69
 
        :param source: The path of the file to copy
70
 
        :param dest: The distination file to create
71
 
        """
72
 
        s_file = file(source, "rb")
73
 
        d_file = file(dest, "wb")
74
 
        for line in s_file:
75
 
            d_file.write(line)
76
 
        os.chmod(dest, 0777 & os.stat(source).st_mode)
77
 
 
78
 
    def dump(self, lines, dest):
79
 
        """Copy the text and mode of a file
80
 
        :param source: The path of the file to copy
81
 
        :param dest: The distination file to create
82
 
        """
83
 
        d_file = file(dest, "wb")
84
 
        for line in lines:
85
 
            d_file.write(line)
86
 
 
87
 
    def add_suffix(self, name, suffix, last_new_name=None, fix_inventory=True):
88
 
        """Rename a file to append a suffix.  If the new name exists, the
89
 
        suffix is added repeatedly until a non-existant name is found
90
 
 
91
 
        :param name: The path of the file
92
 
        :param suffix: The suffix to append
93
 
        :param last_new_name: (used for recursive calls) the last name tried
94
 
        """
95
 
        if last_new_name is None:
96
 
            last_new_name = name
97
 
        new_name = last_new_name+suffix
98
 
        try:
99
 
            rename(name, new_name)
100
 
            if fix_inventory is True:
101
 
                try:
102
 
                    relpath = self.this_tree.relpath(name)
103
 
                except NotBranchError:
104
 
                    relpath = None
105
 
                if relpath is not None:
106
 
                    file_id = self.this_tree.path2id(relpath)
107
 
                    if file_id is not None:
108
 
                        new_path = self.this_tree.relpath(new_name)
109
 
                        rename(new_name, name)
110
 
                        self.this_tree.rename_one(relpath, new_path)
111
 
                        assert self.this_tree.id2path(file_id) == new_path
112
 
        except OSError, e:
113
 
            if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
114
 
                raise
115
 
            return self.add_suffix(name, suffix, last_new_name=new_name, 
116
 
                                   fix_inventory=fix_inventory)
117
 
        return new_name
118
 
 
119
 
    def conflict(self, text):
120
 
        warning(text)
121
 
        self.conflicts += 1
122
 
        
123
 
 
124
 
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
125
 
        """
126
 
        Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER.  The
127
 
        main file will be a version with diff3 conflicts.
128
 
        :param new_file: Path to the output file with diff3 markers
129
 
        :param this_path: Path to the file text for the THIS tree
130
 
        :param base_path: Path to the file text for the BASE tree
131
 
        :param other_path: Path to the file text for the OTHER tree
132
 
        """
133
 
        self.add_suffix(this_path, ".THIS", fix_inventory=False)
134
 
        self.dump(base_lines, this_path+".BASE")
135
 
        self.dump(other_lines, this_path+".OTHER")
136
 
        rename(new_file, this_path)
137
 
        self.conflict("Diff3 conflict encountered in %s" % this_path)
138
 
 
139
 
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
140
 
        """
141
 
        Handle weave conflicts by producing a .THIS, and .OTHER.  The
142
 
        main file will be a version with diff3-style conflicts.
143
 
        """
144
 
        self.add_suffix(filename, ".THIS", fix_inventory=False)
145
 
        out_file.commit()
146
 
        self.dump(weave.get_iter(other_i), filename+".OTHER")
147
 
        self.conflict("Text conflict encountered in %s" % filename)
148
 
 
149
 
    def new_contents_conflict(self, filename, other_contents):
150
 
        """Conflicting contents for newly added file."""
151
 
        other_contents(filename + ".OTHER", self, False)
152
 
        self.conflict("Conflict in newly added file %s" % filename)
153
 
    
154
 
 
155
 
    def target_exists(self, entry, target, old_path):
156
 
        """Handle the case when the target file or dir exists"""
157
 
        moved_path = self.add_suffix(target, ".moved")
158
 
        self.conflict("Moved existing %s to %s" % (target, moved_path))
159
 
 
160
 
    def rmdir_non_empty(self, filename):
161
 
        """Handle the case where the dir to be removed still has contents"""
162
 
        self.conflict("Directory %s not removed because it is not empty"\
163
 
            % filename)
164
 
        return "skip"
165
 
 
166
 
    def rem_contents_conflict(self, filename, this_contents, base_contents):
167
 
        base_contents(filename+".BASE", self)
168
 
        this_contents(filename+".THIS", self)
169
 
        self.conflict("Other branch deleted locally modified file %s" %
170
 
                      filename)
171
 
        return ReplaceContents(this_contents, None)
172
 
 
173
 
    def abs_this_path(self, file_id):
174
 
        """Return the absolute path for a file_id in the this tree."""
175
 
        return self.this_tree.id2abspath(file_id)
176
 
 
177
 
    def add_missing_parents(self, file_id, tree):
178
 
        """If some of the parents for file_id are missing, add them."""
179
 
        entry = tree.inventory[file_id]
180
 
        if entry.parent_id not in self.this_tree:
181
 
            return self.create_all_missing(entry.parent_id, tree)
182
 
        else:
183
 
            return self.abs_this_path(entry.parent_id)
184
 
 
185
 
    def create_all_missing(self, file_id, tree):
186
 
        """Add contents for a file_id and all its parents to a tree."""
187
 
        entry = tree.inventory[file_id]
188
 
        if entry.parent_id is not None and entry.parent_id not in self.this_tree:
189
 
            abspath = self.create_all_missing(entry.parent_id, tree)
190
 
        else:
191
 
            abspath = self.abs_this_path(entry.parent_id)
192
 
        entry_path = pathjoin(abspath, entry.name)
193
 
        if not os.path.isdir(entry_path):
194
 
            self.create(file_id, entry_path, tree)
195
 
        return entry_path
196
 
 
197
 
    def create(self, file_id, path, tree):
198
 
        """Uses tree data to create a filesystem object for the file_id"""
199
 
        from bzrlib._changeset import get_contents
200
 
        get_contents(tree, file_id)(path, self)
201
 
 
202
 
    def missing_for_merge(self, file_id, other_path):
203
 
        """The file_id doesn't exist in THIS, but does in OTHER and BASE"""
204
 
        self.conflict("Other branch modified locally deleted file %s" %
205
 
                      other_path)
206
 
        parent_dir = self.add_missing_parents(file_id, self.other_tree)
207
 
        stem = pathjoin(parent_dir, os.path.basename(other_path))
208
 
        self.create(file_id, stem+".OTHER", self.other_tree)
209
 
        self.create(file_id, stem+".BASE", self.base_tree)
210
 
 
211
 
    def threeway_contents_conflict(filename, this_contents, base_contents,
212
 
                                   other_contents):
213
 
        self.conflict("Three-way conflict merging %s" % filename)
214
 
 
215
 
    def finalize(self):
216
 
        if self.conflicts == 0:
217
 
            if not self.ignore_zero:
218
 
                note("All changes applied successfully.")
219
 
        else:
220
 
            note("%d conflicts encountered." % self.conflicts)
221
 
 
222
49
def _get_tree(treespec, local_branch=None):
223
50
    location, revno = treespec
224
51
    branch = Branch.open_containing(location)[0]
251
78
                interesting_ids=interesting_ids)
252
79
 
253
80
 
254
 
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
255
 
                backup_files=False, 
256
 
                merge_type=ApplyMerge3, 
257
 
                interesting_ids=None, 
258
 
                show_base=False, 
259
 
                reprocess=False, 
260
 
                other_rev_id=None,
261
 
                interesting_files=None,
262
 
                this_tree=None):
263
 
    """Primary interface for merging. 
264
 
 
265
 
        typical use is probably 
266
 
        'merge_inner(branch, branch.get_revision_tree(other_revision),
267
 
                     branch.get_revision_tree(base_revision))'
268
 
        """
269
 
    if this_tree is None:
270
 
        this_tree = this_branch.bzrdir.open_workingtree()
271
 
    merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree)
272
 
    merger.backup_files = backup_files
273
 
    merger.merge_type = merge_type
274
 
    merger.interesting_ids = interesting_ids
275
 
    if interesting_files:
276
 
        assert not interesting_ids, ('Only supply interesting_ids'
277
 
                                     ' or interesting_files')
278
 
        merger._set_interesting_files(interesting_files)
279
 
    merger.show_base = show_base 
280
 
    merger.reprocess = reprocess
281
 
    merger.conflict_handler = _MergeConflictHandler(merger.this_tree, 
282
 
                                                    base_tree, other_tree,
283
 
                                                    ignore_zero=ignore_zero)
284
 
    merger.other_rev_id = other_rev_id
285
 
    merger.other_basis = other_rev_id
286
 
    return merger.do_merge()
287
 
 
288
 
 
289
81
class Merger(object):
290
82
    def __init__(self, this_branch, other_tree=None, base_tree=None, this_tree=None):
291
83
        object.__init__(self)
303
95
        self.interesting_ids = None
304
96
        self.show_base = False
305
97
        self.reprocess = False
306
 
        self.conflict_handler = _MergeConflictHandler(self.this_tree, 
307
 
                                                      base_tree, other_tree)
308
98
 
309
99
    def revision_tree(self, revision_id):
310
100
        return self.this_branch.repository.revision_tree(revision_id)
338
128
        trees = (self.this_basis_tree, self.other_tree)
339
129
        return [get_id(tree, file_id) for tree in trees]
340
130
 
341
 
    def merge_factory(self, file_id, base, other):
342
 
        if self.merge_type.history_based:
343
 
            if self.show_base is True:
344
 
                raise BzrError("Cannot show base for hisory-based merges")
345
 
            if self.reprocess is True:
346
 
                raise BzrError("Cannot reprocess history-based merges")
347
 
                
348
 
            t_revid, o_revid = self.file_revisions(file_id)
349
 
            weave = self.this_basis_tree.get_weave(file_id)
350
 
            contents_change = self.merge_type(weave, t_revid, o_revid)
351
 
        else:
352
 
            if self.show_base is True or self.reprocess is True:
353
 
                contents_change = self.merge_type(file_id, base, other, 
354
 
                                                  show_base=self.show_base, 
355
 
                                                  reprocess=self.reprocess)
356
 
            else:
357
 
                contents_change = self.merge_type(file_id, base, other)
358
 
        if self.backup_files:
359
 
            contents_change = BackupBeforeChange(contents_change)
360
 
        return contents_change
361
 
 
362
131
    def check_basis(self, check_clean):
363
132
        if self.this_basis is None:
364
133
            raise BzrCommandError("This branch has no commits")
454
223
                                                self.this_branch)
455
224
 
456
225
    def do_merge(self):
457
 
        def get_inventory(tree):
458
 
            return tree.inventory
459
 
        
460
 
        inv_changes = merge_flex(self.this_tree, self.base_tree, 
461
 
                                 self.other_tree,
462
 
                                 generate_changeset, get_inventory,
463
 
                                 self.conflict_handler,
464
 
                                 merge_factory=self.merge_factory, 
465
 
                                 interesting_ids=self.interesting_ids)
 
226
        kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree, 
 
227
                  'other_tree': self.other_tree}
 
228
        if self.merge_type.requires_base:
 
229
            kwargs['base_tree'] = self.base_tree
 
230
        if self.merge_type.supports_reprocess:
 
231
            kwargs['reprocess'] = self.reprocess
 
232
        elif self.reprocess:
 
233
            raise BzrError("Reprocess is not supported for this merge"
 
234
                                  " type. %s" % merge_type)
 
235
        if self.merge_type.supports_show_base:
 
236
            kwargs['show_base'] = self.show_base
 
237
        elif self.show_base:
 
238
            raise BzrError("Showing base is not supported for this"
 
239
                                  " merge type. %s" % self.merge_type)
 
240
        merge = self.merge_type(**kwargs)
 
241
        if len(merge.cooked_conflicts) == 0:
 
242
            if not self.ignore_zero:
 
243
                note("All changes applied successfully.")
 
244
        else:
 
245
            note("%d conflicts encountered." % len(merge.cooked_conflicts))
466
246
 
467
 
        adjust_ids = []
468
 
        for id, path in inv_changes.iteritems():
469
 
            if path is not None:
470
 
                if path == u'.':
471
 
                    path = u''
472
 
                else:
473
 
                    assert path.startswith('.' + '/') or path.startswith('.' + '\\'), "path is %s" % path
474
 
                path = path[2:]
475
 
            adjust_ids.append((path, id))
476
 
        if len(adjust_ids) > 0:
477
 
            self.this_tree.set_inventory(self.regen_inventory(adjust_ids))
478
 
        conflicts = self.conflict_handler.conflicts
479
 
        self.conflict_handler.finalize()
480
 
        return conflicts
 
247
        return len(merge.cooked_conflicts)
481
248
 
482
249
    def regen_inventory(self, new_entries):
483
250
        old_entries = self.this_tree.read_working_inventory()
540
307
        return new_inventory_list
541
308
 
542
309
 
543
 
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
544
 
                     "diff3": (Diff3Merge,  "Merge using external diff3"),
545
 
                     'weave': (WeaveMerge, "Weave-based merge")
 
310
class Merge3Merger(object):
 
311
    """Three-way merger that uses the merge3 text merger"""
 
312
    requires_base = True
 
313
    supports_reprocess = True
 
314
    supports_show_base = True
 
315
    history_based = False
 
316
 
 
317
    def __init__(self, working_tree, this_tree, base_tree, other_tree, 
 
318
                 reprocess=False, show_base=False):
 
319
        """Initialize the merger object and perform the merge."""
 
320
        object.__init__(self)
 
321
        self.this_tree = working_tree
 
322
        self.base_tree = base_tree
 
323
        self.other_tree = other_tree
 
324
        self._raw_conflicts = []
 
325
        self.cooked_conflicts = []
 
326
        self.reprocess = reprocess
 
327
        self.show_base = show_base
 
328
 
 
329
        all_ids = set(base_tree)
 
330
        all_ids.update(other_tree)
 
331
        self.tt = TreeTransform(working_tree)
 
332
        try:
 
333
            for file_id in all_ids:
 
334
                self.merge_names(file_id)
 
335
                file_status = self.merge_contents(file_id)
 
336
                self.merge_executable(file_id, file_status)
 
337
                
 
338
            fs_conflicts = resolve_conflicts(self.tt)
 
339
            self.cook_conflicts(fs_conflicts)
 
340
            for line in conflicts_strings(self.cooked_conflicts):
 
341
                warning(line)
 
342
            self.tt.apply()
 
343
        finally:
 
344
            try:
 
345
                self.tt.finalize()
 
346
            except:
 
347
                pass
 
348
       
 
349
    @staticmethod
 
350
    def parent(entry, file_id):
 
351
        """Determine the parent for a file_id (used as a key method)"""
 
352
        if entry is None:
 
353
            return None
 
354
        return entry.parent_id
 
355
 
 
356
    @staticmethod
 
357
    def name(entry, file_id):
 
358
        """Determine the name for a file_id (used as a key method)"""
 
359
        if entry is None:
 
360
            return None
 
361
        return entry.name
 
362
    
 
363
    @staticmethod
 
364
    def contents_sha1(tree, file_id):
 
365
        """Determine the sha1 of the file contents (used as a key method)."""
 
366
        if file_id not in tree:
 
367
            return None
 
368
        return tree.get_file_sha1(file_id)
 
369
 
 
370
    @staticmethod
 
371
    def executable(tree, file_id):
 
372
        """Determine the executability of a file-id (used as a key method)."""
 
373
        if file_id not in tree:
 
374
            return None
 
375
        if tree.kind(file_id) != "file":
 
376
            return False
 
377
        return tree.is_executable(file_id)
 
378
 
 
379
    @staticmethod
 
380
    def kind(tree, file_id):
 
381
        """Determine the kind of a file-id (used as a key method)."""
 
382
        if file_id not in tree:
 
383
            return None
 
384
        return tree.kind(file_id)
 
385
 
 
386
    @staticmethod
 
387
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
 
388
        """Do a three-way test on a scalar.
 
389
        Return "this", "other" or "conflict", depending whether a value wins.
 
390
        """
 
391
        key_base = key(base_tree, file_id)
 
392
        key_other = key(other_tree, file_id)
 
393
        #if base == other, either they all agree, or only THIS has changed.
 
394
        if key_base == key_other:
 
395
            return "this"
 
396
        key_this = key(this_tree, file_id)
 
397
        if key_this not in (key_base, key_other):
 
398
            return "conflict"
 
399
        # "Ambiguous clean merge"
 
400
        elif key_this == key_other:
 
401
            return "this"
 
402
        else:
 
403
            assert key_this == key_base
 
404
            return "other"
 
405
 
 
406
    def merge_names(self, file_id):
 
407
        """Perform a merge on file_id names and parents"""
 
408
        def get_entry(tree):
 
409
            if file_id in tree.inventory:
 
410
                return tree.inventory[file_id]
 
411
            else:
 
412
                return None
 
413
        this_entry = get_entry(self.this_tree)
 
414
        other_entry = get_entry(self.other_tree)
 
415
        base_entry = get_entry(self.base_tree)
 
416
        name_winner = self.scalar_three_way(this_entry, base_entry, 
 
417
                                            other_entry, file_id, self.name)
 
418
        parent_id_winner = self.scalar_three_way(this_entry, base_entry, 
 
419
                                                 other_entry, file_id, 
 
420
                                                 self.parent)
 
421
        if this_entry is None:
 
422
            if name_winner == "this":
 
423
                name_winner = "other"
 
424
            if parent_id_winner == "this":
 
425
                parent_id_winner = "other"
 
426
        if name_winner == "this" and parent_id_winner == "this":
 
427
            return
 
428
        if name_winner == "conflict":
 
429
            trans_id = self.tt.trans_id_file_id(file_id)
 
430
            self._raw_conflicts.append(('name conflict', trans_id, 
 
431
                                        self.name(this_entry, file_id), 
 
432
                                        self.name(other_entry, file_id)))
 
433
        if parent_id_winner == "conflict":
 
434
            trans_id = self.tt.trans_id_file_id(file_id)
 
435
            self._raw_conflicts.append(('parent conflict', trans_id, 
 
436
                                        self.parent(this_entry, file_id), 
 
437
                                        self.parent(other_entry, file_id)))
 
438
        if other_entry is None:
 
439
            # it doesn't matter whether the result was 'other' or 
 
440
            # 'conflict'-- if there's no 'other', we leave it alone.
 
441
            return
 
442
        # if we get here, name_winner and parent_winner are set to safe values.
 
443
        winner_entry = {"this": this_entry, "other": other_entry, 
 
444
                        "conflict": other_entry}
 
445
        trans_id = self.tt.trans_id_file_id(file_id)
 
446
        parent_id = winner_entry[parent_id_winner].parent_id
 
447
        parent_trans_id = self.tt.trans_id_file_id(parent_id)
 
448
        self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
 
449
                            trans_id)
 
450
 
 
451
    def merge_contents(self, file_id):
 
452
        """Performa a merge on file_id contents."""
 
453
        def contents_pair(tree):
 
454
            if file_id not in tree:
 
455
                return (None, None)
 
456
            kind = tree.kind(file_id)
 
457
            if kind == "root_directory":
 
458
                kind = "directory"
 
459
            if kind == "file":
 
460
                contents = tree.get_file_sha1(file_id)
 
461
            elif kind == "symlink":
 
462
                contents = tree.get_symlink_target(file_id)
 
463
            else:
 
464
                contents = None
 
465
            return kind, contents
 
466
        # See SPOT run.  run, SPOT, run.
 
467
        # So we're not QUITE repeating ourselves; we do tricky things with
 
468
        # file kind...
 
469
        base_pair = contents_pair(self.base_tree)
 
470
        other_pair = contents_pair(self.other_tree)
 
471
        if base_pair == other_pair:
 
472
            # OTHER introduced no changes
 
473
            return "unmodified"
 
474
        this_pair = contents_pair(self.this_tree)
 
475
        if this_pair == other_pair:
 
476
            # THIS and OTHER introduced the same changes
 
477
            return "unmodified"
 
478
        else:
 
479
            trans_id = self.tt.trans_id_file_id(file_id)
 
480
            if this_pair == base_pair:
 
481
                # only OTHER introduced changes
 
482
                if file_id in self.this_tree:
 
483
                    # Remove any existing contents
 
484
                    self.tt.delete_contents(trans_id)
 
485
                if file_id in self.other_tree:
 
486
                    # OTHER changed the file
 
487
                    create_by_entry(self.tt, 
 
488
                                    self.other_tree.inventory[file_id], 
 
489
                                    self.other_tree, trans_id)
 
490
                    if file_id not in self.this_tree.inventory:
 
491
                        self.tt.version_file(file_id, trans_id)
 
492
                    return "modified"
 
493
                elif file_id in self.this_tree.inventory:
 
494
                    # OTHER deleted the file
 
495
                    self.tt.unversion_file(trans_id)
 
496
                    return "deleted"
 
497
            #BOTH THIS and OTHER introduced changes; scalar conflict
 
498
            elif this_pair[0] == "file" and other_pair[0] == "file":
 
499
                # THIS and OTHER are both files, so text merge.  Either
 
500
                # BASE is a file, or both converted to files, so at least we
 
501
                # have agreement that output should be a file.
 
502
                if file_id not in self.this_tree.inventory:
 
503
                    self.tt.version_file(file_id, trans_id)
 
504
                self.text_merge(file_id, trans_id)
 
505
                try:
 
506
                    self.tt.tree_kind(trans_id)
 
507
                    self.tt.delete_contents(trans_id)
 
508
                except NoSuchFile:
 
509
                    pass
 
510
                return "modified"
 
511
            else:
 
512
                # Scalar conflict, can't text merge.  Dump conflicts
 
513
                trans_id = self.tt.trans_id_file_id(file_id)
 
514
                name = self.tt.final_name(trans_id)
 
515
                parent_id = self.tt.final_parent(trans_id)
 
516
                if file_id in self.this_tree.inventory:
 
517
                    self.tt.unversion_file(trans_id)
 
518
                    self.tt.delete_contents(trans_id)
 
519
                file_group = self._dump_conflicts(name, parent_id, file_id, 
 
520
                                                  set_version=True)
 
521
                self._raw_conflicts.append(('contents conflict', file_group))
 
522
 
 
523
    def get_lines(self, tree, file_id):
 
524
        """Return the lines in a file, or an empty list."""
 
525
        if file_id in tree:
 
526
            return tree.get_file(file_id).readlines()
 
527
        else:
 
528
            return []
 
529
 
 
530
    def text_merge(self, file_id, trans_id):
 
531
        """Perform a three-way text merge on a file_id"""
 
532
        # it's possible that we got here with base as a different type.
 
533
        # if so, we just want two-way text conflicts.
 
534
        if file_id in self.base_tree and \
 
535
            self.base_tree.kind(file_id) == "file":
 
536
            base_lines = self.get_lines(self.base_tree, file_id)
 
537
        else:
 
538
            base_lines = []
 
539
        other_lines = self.get_lines(self.other_tree, file_id)
 
540
        this_lines = self.get_lines(self.this_tree, file_id)
 
541
        m3 = Merge3(base_lines, this_lines, other_lines)
 
542
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
 
543
        if self.show_base is True:
 
544
            base_marker = '|' * 7
 
545
        else:
 
546
            base_marker = None
 
547
 
 
548
        def iter_merge3(retval):
 
549
            retval["text_conflicts"] = False
 
550
            for line in m3.merge_lines(name_a = "TREE", 
 
551
                                       name_b = "MERGE-SOURCE", 
 
552
                                       name_base = "BASE-REVISION",
 
553
                                       start_marker=start_marker, 
 
554
                                       base_marker=base_marker,
 
555
                                       reprocess=self.reprocess):
 
556
                if line.startswith(start_marker):
 
557
                    retval["text_conflicts"] = True
 
558
                    yield line.replace(start_marker, '<' * 7)
 
559
                else:
 
560
                    yield line
 
561
        retval = {}
 
562
        merge3_iterator = iter_merge3(retval)
 
563
        self.tt.create_file(merge3_iterator, trans_id)
 
564
        if retval["text_conflicts"] is True:
 
565
            self._raw_conflicts.append(('text conflict', trans_id))
 
566
            name = self.tt.final_name(trans_id)
 
567
            parent_id = self.tt.final_parent(trans_id)
 
568
            file_group = self._dump_conflicts(name, parent_id, file_id, 
 
569
                                              this_lines, base_lines,
 
570
                                              other_lines)
 
571
            file_group.append(trans_id)
 
572
 
 
573
    def _dump_conflicts(self, name, parent_id, file_id, this_lines=None, 
 
574
                        base_lines=None, other_lines=None, set_version=False,
 
575
                        no_base=False):
 
576
        """Emit conflict files.
 
577
        If this_lines, base_lines, or other_lines are omitted, they will be
 
578
        determined automatically.  If set_version is true, the .OTHER, .THIS
 
579
        or .BASE (in that order) will be created as versioned files.
 
580
        """
 
581
        data = [('OTHER', self.other_tree, other_lines), 
 
582
                ('THIS', self.this_tree, this_lines)]
 
583
        if not no_base:
 
584
            data.append(('BASE', self.base_tree, base_lines))
 
585
        versioned = False
 
586
        file_group = []
 
587
        for suffix, tree, lines in data:
 
588
            if file_id in tree:
 
589
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
 
590
                                               suffix, lines)
 
591
                file_group.append(trans_id)
 
592
                if set_version and not versioned:
 
593
                    self.tt.version_file(file_id, trans_id)
 
594
                    versioned = True
 
595
        return file_group
 
596
           
 
597
    def _conflict_file(self, name, parent_id, tree, file_id, suffix, 
 
598
                       lines=None):
 
599
        """Emit a single conflict file."""
 
600
        name = name + '.' + suffix
 
601
        trans_id = self.tt.create_path(name, parent_id)
 
602
        entry = tree.inventory[file_id]
 
603
        create_by_entry(self.tt, entry, tree, trans_id, lines)
 
604
        return trans_id
 
605
 
 
606
    def merge_executable(self, file_id, file_status):
 
607
        """Perform a merge on the execute bit."""
 
608
        if file_status == "deleted":
 
609
            return
 
610
        trans_id = self.tt.trans_id_file_id(file_id)
 
611
        try:
 
612
            if self.tt.final_kind(trans_id) != "file":
 
613
                return
 
614
        except NoSuchFile:
 
615
            return
 
616
        winner = self.scalar_three_way(self.this_tree, self.base_tree, 
 
617
                                       self.other_tree, file_id, 
 
618
                                       self.executable)
 
619
        if winner == "conflict":
 
620
        # There must be a None in here, if we have a conflict, but we
 
621
        # need executability since file status was not deleted.
 
622
            if self.other_tree.is_executable(file_id) is None:
 
623
                winner = "this"
 
624
            else:
 
625
                winner = "other"
 
626
        if winner == "this":
 
627
            if file_status == "modified":
 
628
                executability = self.this_tree.is_executable(file_id)
 
629
                if executability is not None:
 
630
                    trans_id = self.tt.trans_id_file_id(file_id)
 
631
                    self.tt.set_executability(executability, trans_id)
 
632
        else:
 
633
            assert winner == "other"
 
634
            if file_id in self.other_tree:
 
635
                executability = self.other_tree.is_executable(file_id)
 
636
            elif file_id in self.this_tree:
 
637
                executability = self.this_tree.is_executable(file_id)
 
638
            elif file_id in self.base_tree:
 
639
                executability = self.base_tree.is_executable(file_id)
 
640
            if executability is not None:
 
641
                trans_id = self.tt.trans_id_file_id(file_id)
 
642
                self.tt.set_executability(executability, trans_id)
 
643
 
 
644
    def cook_conflicts(self, fs_conflicts):
 
645
        """Convert all conflicts into a form that doesn't depend on trans_id"""
 
646
        name_conflicts = {}
 
647
        self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
 
648
        fp = FinalPaths(self.tt)
 
649
        for conflict in self._raw_conflicts:
 
650
            conflict_type = conflict[0]
 
651
            if conflict_type in ('name conflict', 'parent conflict'):
 
652
                trans_id = conflict[1]
 
653
                conflict_args = conflict[2:]
 
654
                if trans_id not in name_conflicts:
 
655
                    name_conflicts[trans_id] = {}
 
656
                unique_add(name_conflicts[trans_id], conflict_type, 
 
657
                           conflict_args)
 
658
            if conflict_type == 'contents conflict':
 
659
                for trans_id in conflict[1]:
 
660
                    file_id = self.tt.final_file_id(trans_id)
 
661
                    if file_id is not None:
 
662
                        break
 
663
                path = fp.get_path(trans_id)
 
664
                for suffix in ('.BASE', '.THIS', '.OTHER'):
 
665
                    if path.endswith(suffix):
 
666
                        path = path[:-len(suffix)]
 
667
                        break
 
668
                self.cooked_conflicts.append((conflict_type, file_id, path))
 
669
            if conflict_type == 'text conflict':
 
670
                trans_id = conflict[1]
 
671
                path = fp.get_path(trans_id)
 
672
                file_id = self.tt.final_file_id(trans_id)
 
673
                self.cooked_conflicts.append((conflict_type, file_id, path))
 
674
 
 
675
        for trans_id, conflicts in name_conflicts.iteritems():
 
676
            try:
 
677
                this_parent, other_parent = conflicts['parent conflict']
 
678
                assert this_parent != other_parent
 
679
            except KeyError:
 
680
                this_parent = other_parent = \
 
681
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
 
682
            try:
 
683
                this_name, other_name = conflicts['name conflict']
 
684
                assert this_name != other_name
 
685
            except KeyError:
 
686
                this_name = other_name = self.tt.final_name(trans_id)
 
687
            other_path = fp.get_path(trans_id)
 
688
            if this_parent is not None:
 
689
                this_parent_path = \
 
690
                    fp.get_path(self.tt.trans_id_file_id(this_parent))
 
691
                this_path = pathjoin(this_parent_path, this_name)
 
692
            else:
 
693
                this_path = "<deleted>"
 
694
            file_id = self.tt.final_file_id(trans_id)
 
695
            self.cooked_conflicts.append(('path conflict', file_id, this_path, 
 
696
                                         other_path))
 
697
 
 
698
 
 
699
class WeaveMerger(Merge3Merger):
 
700
    """Three-way tree merger, text weave merger."""
 
701
    supports_reprocess = False
 
702
    supports_show_base = False
 
703
 
 
704
    def __init__(self, working_tree, this_tree, base_tree, other_tree):
 
705
        self.this_revision_tree = self._get_revision_tree(this_tree)
 
706
        self.other_revision_tree = self._get_revision_tree(other_tree)
 
707
        super(WeaveMerger, self).__init__(working_tree, this_tree, 
 
708
                                          base_tree, other_tree)
 
709
 
 
710
    def _get_revision_tree(self, tree):
 
711
        """Return a revision tree releated to this tree.
 
712
        If the tree is a WorkingTree, the basis will be returned.
 
713
        """
 
714
        if getattr(tree, 'get_weave', False) is False:
 
715
            # If we have a WorkingTree, try using the basis
 
716
            return tree.branch.basis_tree()
 
717
        else:
 
718
            return tree
 
719
 
 
720
    def _check_file(self, file_id):
 
721
        """Check that the revision tree's version of the file matches."""
 
722
        for tree, rt in ((self.this_tree, self.this_revision_tree), 
 
723
                         (self.other_tree, self.other_revision_tree)):
 
724
            if rt is tree:
 
725
                continue
 
726
            if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
 
727
                raise WorkingTreeNotRevision(self.this_tree)
 
728
 
 
729
    def _merged_lines(self, file_id):
 
730
        """Generate the merged lines.
 
731
        There is no distinction between lines that are meant to contain <<<<<<<
 
732
        and conflicts.
 
733
        """
 
734
        weave = self.this_revision_tree.get_weave(file_id)
 
735
        this_revision_id = self.this_revision_tree.inventory[file_id].revision
 
736
        other_revision_id = \
 
737
            self.other_revision_tree.inventory[file_id].revision
 
738
        this_i = weave.lookup(this_revision_id)
 
739
        other_i = weave.lookup(other_revision_id)
 
740
        plan =  weave.plan_merge(this_i, other_i)
 
741
        return weave.weave_merge(plan)
 
742
 
 
743
    def text_merge(self, file_id, trans_id):
 
744
        """Perform a (weave) text merge for a given file and file-id.
 
745
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
 
746
        and a conflict will be noted.
 
747
        """
 
748
        self._check_file(file_id)
 
749
        lines = self._merged_lines(file_id)
 
750
        conflicts = '<<<<<<<\n' in lines
 
751
        self.tt.create_file(lines, trans_id)
 
752
        if conflicts:
 
753
            self._raw_conflicts.append(('text conflict', trans_id))
 
754
            name = self.tt.final_name(trans_id)
 
755
            parent_id = self.tt.final_parent(trans_id)
 
756
            file_group = self._dump_conflicts(name, parent_id, file_id, 
 
757
                                              no_base=True)
 
758
            file_group.append(trans_id)
 
759
 
 
760
 
 
761
class Diff3Merger(Merge3Merger):
 
762
    """Three-way merger using external diff3 for text merging"""
 
763
    def dump_file(self, temp_dir, name, tree, file_id):
 
764
        out_path = pathjoin(temp_dir, name)
 
765
        out_file = file(out_path, "wb")
 
766
        in_file = tree.get_file(file_id)
 
767
        for line in in_file:
 
768
            out_file.write(line)
 
769
        return out_path
 
770
 
 
771
    def text_merge(self, file_id, trans_id):
 
772
        """Perform a diff3 merge using a specified file-id and trans-id.
 
773
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
 
774
        will be dumped, and a will be conflict noted.
 
775
        """
 
776
        import bzrlib.patch
 
777
        temp_dir = mkdtemp(prefix="bzr-")
 
778
        try:
 
779
            new_file = pathjoin(temp_dir, "new")
 
780
            this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
 
781
            base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
 
782
            other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
 
783
            status = bzrlib.patch.diff3(new_file, this, base, other)
 
784
            if status not in (0, 1):
 
785
                raise BzrError("Unhandled diff3 exit code")
 
786
            self.tt.create_file(file(new_file, "rb"), trans_id)
 
787
            if status == 1:
 
788
                name = self.tt.final_name(trans_id)
 
789
                parent_id = self.tt.final_parent(trans_id)
 
790
                self._dump_conflicts(name, parent_id, file_id)
 
791
            self._raw_conflicts.append(('text conflict', trans_id))
 
792
        finally:
 
793
            rmtree(temp_dir)
 
794
 
 
795
 
 
796
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
 
797
                backup_files=False, 
 
798
                merge_type=Merge3Merger, 
 
799
                interesting_ids=None, 
 
800
                show_base=False, 
 
801
                reprocess=False, 
 
802
                other_rev_id=None,
 
803
                interesting_files=None,
 
804
                this_tree=None):
 
805
    """Primary interface for merging. 
 
806
 
 
807
        typical use is probably 
 
808
        'merge_inner(branch, branch.get_revision_tree(other_revision),
 
809
                     branch.get_revision_tree(base_revision))'
 
810
        """
 
811
    if this_tree is None:
 
812
        this_tree = this_branch.working_tree()
 
813
    merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree)
 
814
    merger.backup_files = backup_files
 
815
    merger.merge_type = merge_type
 
816
    merger.interesting_ids = interesting_ids
 
817
    if interesting_files:
 
818
        assert not interesting_ids, ('Only supply interesting_ids'
 
819
                                     ' or interesting_files')
 
820
        merger._set_interesting_files(interesting_files)
 
821
    merger.show_base = show_base 
 
822
    merger.reprocess = reprocess
 
823
    merger.other_rev_id = other_rev_id
 
824
    merger.other_basis = other_rev_id
 
825
    return merger.do_merge()
 
826
 
 
827
 
 
828
merge_types = {     "merge3": (Merge3Merger, "Native diff3-style merge"), 
 
829
                     "diff3": (Diff3Merger,  "Merge using external diff3"),
 
830
                     'weave': (WeaveMerger, "Weave-based merge")
546
831
              }