~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Aaron Bentley
  • Date: 2006-02-22 14:39:42 UTC
  • mto: (2027.1.2 revert-subpath-56549)
  • mto: This revision was merged to the branch mainline in revision 1570.
  • Revision ID: abentley@panoramicfeedback.com-20060222143942-ae72299f2de66767
Fixed build_tree with symlinks

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
 
18
18
import os
19
 
import shutil
20
19
import errno
 
20
from shutil import rmtree
 
21
from tempfile import mkdtemp
21
22
 
22
 
import bzrlib.osutils
23
 
import bzrlib.revision
24
 
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
25
 
from bzrlib.merge_core import WeaveMerge
26
 
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
27
 
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
 
23
import bzrlib
28
24
from bzrlib.branch import Branch
 
25
from bzrlib.delta import compare_trees
29
26
from bzrlib.errors import (BzrCommandError,
30
 
                           UnrelatedBranches,
 
27
                           BzrError,
31
28
                           NoCommonAncestor,
32
29
                           NoCommits,
33
 
                           WorkingTreeNotRevision,
 
30
                           NoSuchRevision,
 
31
                           NoSuchFile,
34
32
                           NotBranchError,
35
33
                           NotVersionedError,
36
 
                           BzrError)
37
 
from bzrlib.delta import compare_trees
38
 
from bzrlib.trace import mutter, warning, note
 
34
                           UnrelatedBranches,
 
35
                           WorkingTreeNotRevision,
 
36
                           )
39
37
from bzrlib.fetch import greedy_fetch, fetch
40
 
from bzrlib.revision import is_ancestor, NULL_REVISION
 
38
from bzrlib.merge3 import Merge3
 
39
import bzrlib.osutils
41
40
from bzrlib.osutils import rename, pathjoin
42
 
from bzrlib.revision import common_ancestor, MultipleRevisionSources
43
 
from bzrlib.errors import NoSuchRevision
 
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)
 
45
from bzrlib.trace import mutter, warning, note
44
46
 
45
47
# TODO: Report back as changes are merged in
46
48
 
47
 
# TODO: build_working_dir can be built on something simpler than merge()
48
 
 
49
 
# FIXME: merge() parameters seem oriented towards the command line
50
 
# NOTABUG: merge is a helper for commandline functions.  merge_inner is the
51
 
#          the core functionality.
52
 
 
53
 
# comments from abentley on irc: merge happens in two stages, each
54
 
# of which generates a changeset object
55
 
 
56
 
# stage 1: generate OLD->OTHER,
57
 
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
58
 
 
59
 
class MergeConflictHandler(ExceptionConflictHandler):
60
 
    """Handle conflicts encountered while merging.
61
 
 
62
 
    This subclasses ExceptionConflictHandler, so that any types of
63
 
    conflict that are not explicitly handled cause an exception and
64
 
    terminate the merge.
65
 
    """
66
 
    def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
67
 
        ExceptionConflictHandler.__init__(self)
68
 
        self.conflicts = 0
69
 
        self.ignore_zero = ignore_zero
70
 
        self.this_tree = this_tree
71
 
        self.base_tree = base_tree
72
 
        self.other_tree = other_tree
73
 
 
74
 
    def copy(self, source, dest):
75
 
        """Copy the text and mode of a file
76
 
        :param source: The path of the file to copy
77
 
        :param dest: The distination file to create
78
 
        """
79
 
        s_file = file(source, "rb")
80
 
        d_file = file(dest, "wb")
81
 
        for line in s_file:
82
 
            d_file.write(line)
83
 
        os.chmod(dest, 0777 & os.stat(source).st_mode)
84
 
 
85
 
    def dump(self, lines, dest):
86
 
        """Copy the text and mode of a file
87
 
        :param source: The path of the file to copy
88
 
        :param dest: The distination file to create
89
 
        """
90
 
        d_file = file(dest, "wb")
91
 
        for line in lines:
92
 
            d_file.write(line)
93
 
 
94
 
    def add_suffix(self, name, suffix, last_new_name=None, fix_inventory=True):
95
 
        """Rename a file to append a suffix.  If the new name exists, the
96
 
        suffix is added repeatedly until a non-existant name is found
97
 
 
98
 
        :param name: The path of the file
99
 
        :param suffix: The suffix to append
100
 
        :param last_new_name: (used for recursive calls) the last name tried
101
 
        """
102
 
        if last_new_name is None:
103
 
            last_new_name = name
104
 
        new_name = last_new_name+suffix
105
 
        try:
106
 
            rename(name, new_name)
107
 
            if fix_inventory is True:
108
 
                try:
109
 
                    relpath = self.this_tree.relpath(name)
110
 
                except NotBranchError:
111
 
                    relpath = None
112
 
                if relpath is not None:
113
 
                    file_id = self.this_tree.path2id(relpath)
114
 
                    if file_id is not None:
115
 
                        new_path = self.this_tree.relpath(new_name)
116
 
                        rename(new_name, name)
117
 
                        self.this_tree.rename_one(relpath, new_path)
118
 
                        assert self.this_tree.id2path(file_id) == new_path
119
 
        except OSError, e:
120
 
            if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
121
 
                raise
122
 
            return self.add_suffix(name, suffix, last_new_name=new_name, 
123
 
                                   fix_inventory=fix_inventory)
124
 
        return new_name
125
 
 
126
 
    def conflict(self, text):
127
 
        warning(text)
128
 
        self.conflicts += 1
129
 
        
130
 
 
131
 
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
132
 
        """
133
 
        Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER.  The
134
 
        main file will be a version with diff3 conflicts.
135
 
        :param new_file: Path to the output file with diff3 markers
136
 
        :param this_path: Path to the file text for the THIS tree
137
 
        :param base_path: Path to the file text for the BASE tree
138
 
        :param other_path: Path to the file text for the OTHER tree
139
 
        """
140
 
        self.add_suffix(this_path, ".THIS", fix_inventory=False)
141
 
        self.dump(base_lines, this_path+".BASE")
142
 
        self.dump(other_lines, this_path+".OTHER")
143
 
        rename(new_file, this_path)
144
 
        self.conflict("Diff3 conflict encountered in %s" % this_path)
145
 
 
146
 
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
147
 
        """
148
 
        Handle weave conflicts by producing a .THIS, and .OTHER.  The
149
 
        main file will be a version with diff3-style conflicts.
150
 
        """
151
 
        self.add_suffix(filename, ".THIS", fix_inventory=False)
152
 
        out_file.commit()
153
 
        self.dump(weave.get_iter(other_i), filename+".OTHER")
154
 
        self.conflict("Text conflict encountered in %s" % filename)
155
 
 
156
 
    def new_contents_conflict(self, filename, other_contents):
157
 
        """Conflicting contents for newly added file."""
158
 
        other_contents(filename + ".OTHER", self, False)
159
 
        self.conflict("Conflict in newly added file %s" % filename)
160
 
    
161
 
 
162
 
    def target_exists(self, entry, target, old_path):
163
 
        """Handle the case when the target file or dir exists"""
164
 
        moved_path = self.add_suffix(target, ".moved")
165
 
        self.conflict("Moved existing %s to %s" % (target, moved_path))
166
 
 
167
 
    def rmdir_non_empty(self, filename):
168
 
        """Handle the case where the dir to be removed still has contents"""
169
 
        self.conflict("Directory %s not removed because it is not empty"\
170
 
            % filename)
171
 
        return "skip"
172
 
 
173
 
    def rem_contents_conflict(self, filename, this_contents, base_contents):
174
 
        base_contents(filename+".BASE", self)
175
 
        this_contents(filename+".THIS", self)
176
 
        self.conflict("Other branch deleted locally modified file %s" %
177
 
                      filename)
178
 
        return ReplaceContents(this_contents, None)
179
 
 
180
 
    def abs_this_path(self, file_id):
181
 
        """Return the absolute path for a file_id in the this tree."""
182
 
        return self.this_tree.id2abspath(file_id)
183
 
 
184
 
    def add_missing_parents(self, file_id, tree):
185
 
        """If some of the parents for file_id are missing, add them."""
186
 
        entry = tree.inventory[file_id]
187
 
        if entry.parent_id not in self.this_tree:
188
 
            return self.create_all_missing(entry.parent_id, tree)
189
 
        else:
190
 
            return self.abs_this_path(entry.parent_id)
191
 
 
192
 
    def create_all_missing(self, file_id, tree):
193
 
        """Add contents for a file_id and all its parents to a tree."""
194
 
        entry = tree.inventory[file_id]
195
 
        if entry.parent_id is not None and entry.parent_id not in self.this_tree:
196
 
            abspath = self.create_all_missing(entry.parent_id, tree)
197
 
        else:
198
 
            abspath = self.abs_this_path(entry.parent_id)
199
 
        entry_path = pathjoin(abspath, entry.name)
200
 
        if not os.path.isdir(entry_path):
201
 
            self.create(file_id, entry_path, tree)
202
 
        return entry_path
203
 
 
204
 
    def create(self, file_id, path, tree):
205
 
        """Uses tree data to create a filesystem object for the file_id"""
206
 
        from changeset import get_contents
207
 
        get_contents(tree, file_id)(path, self)
208
 
 
209
 
    def missing_for_merge(self, file_id, other_path):
210
 
        """The file_id doesn't exist in THIS, but does in OTHER and BASE"""
211
 
        self.conflict("Other branch modified locally deleted file %s" %
212
 
                      other_path)
213
 
        parent_dir = self.add_missing_parents(file_id, self.other_tree)
214
 
        stem = pathjoin(parent_dir, os.path.basename(other_path))
215
 
        self.create(file_id, stem+".OTHER", self.other_tree)
216
 
        self.create(file_id, stem+".BASE", self.base_tree)
217
 
 
218
 
    def threeway_contents_conflict(filename, this_contents, base_contents,
219
 
                                   other_contents):
220
 
        self.conflict("Three-way conflict merging %s" % filename)
221
 
 
222
 
    def finalize(self):
223
 
        if self.conflicts == 0:
224
 
            if not self.ignore_zero:
225
 
                note("All changes applied successfully.")
226
 
        else:
227
 
            note("%d conflicts encountered." % self.conflicts)
228
 
            
229
 
def get_tree(treespec, local_branch=None):
 
49
def _get_tree(treespec, local_branch=None):
230
50
    location, revno = treespec
231
51
    branch = Branch.open_containing(location)[0]
232
52
    if revno is None:
237
57
        revision = branch.get_rev_id(revno)
238
58
        if revision is None:
239
59
            revision = NULL_REVISION
240
 
    return branch, get_revid_tree(branch, revision, local_branch)
241
 
 
242
 
def get_revid_tree(branch, revision, local_branch):
 
60
    return branch, _get_revid_tree(branch, revision, local_branch)
 
61
 
 
62
 
 
63
def _get_revid_tree(branch, revision, local_branch):
243
64
    if revision is None:
244
 
        base_tree = branch.working_tree()
 
65
        base_tree = branch.bzrdir.open_workingtree()
245
66
    else:
246
67
        if local_branch is not None:
247
 
            greedy_fetch(local_branch, branch, revision)
248
 
            base_tree = local_branch.revision_tree(revision)
 
68
            if local_branch.base != branch.base:
 
69
                greedy_fetch(local_branch, branch, revision)
 
70
            base_tree = local_branch.repository.revision_tree(revision)
249
71
        else:
250
 
            base_tree = branch.revision_tree(revision)
 
72
            base_tree = branch.repository.revision_tree(revision)
251
73
    return base_tree
252
74
 
253
75
 
254
 
def file_exists(tree, file_id):
255
 
    return tree.has_filename(tree.id2path(file_id))
256
 
    
257
 
 
258
 
def build_working_dir(to_dir):
259
 
    """Build a working directory in an empty directory.
260
 
 
261
 
    to_dir is a directory containing branch metadata but no working files,
262
 
    typically constructed by cloning an existing branch. 
263
 
 
264
 
    This is split out as a special idiomatic case of merge.  It could
265
 
    eventually be done by just building the tree directly calling into 
266
 
    lower-level code (e.g. constructing a changeset).
267
 
    """
268
 
    # RBC 20051019 is this not just 'export' ?
269
 
    # AB Well, export doesn't take care of inventory...
270
 
    this_branch = Branch.open_containing(to_dir)[0]
271
 
    transform_tree(this_branch.working_tree(), this_branch.basis_tree())
272
 
 
273
 
 
274
76
def transform_tree(from_tree, to_tree, interesting_ids=None):
275
77
    merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
276
78
                interesting_ids=interesting_ids)
277
79
 
278
80
 
279
 
def merge(other_revision, base_revision,
280
 
          check_clean=True, ignore_zero=False,
281
 
          this_dir=None, backup_files=False, merge_type=ApplyMerge3,
282
 
          file_list=None, show_base=False, reprocess=False):
283
 
    """Merge changes into a tree.
284
 
 
285
 
    base_revision
286
 
        list(path, revno) Base for three-way merge.  
287
 
        If [None, None] then a base will be automatically determined.
288
 
    other_revision
289
 
        list(path, revno) Other revision for three-way merge.
290
 
    this_dir
291
 
        Directory to merge changes into; '.' by default.
292
 
    check_clean
293
 
        If true, this_dir must have no uncommitted changes before the
294
 
        merge begins.
295
 
    ignore_zero - If true, suppress the "zero conflicts" message when 
296
 
        there are no conflicts; should be set when doing something we expect
297
 
        to complete perfectly.
298
 
    file_list - If supplied, merge only changes to selected files.
299
 
 
300
 
    All available ancestors of other_revision and base_revision are
301
 
    automatically pulled into the branch.
302
 
 
303
 
    The revno may be -1 to indicate the last revision on the branch, which is
304
 
    the typical case.
305
 
 
306
 
    This function is intended for use from the command line; programmatic
307
 
    clients might prefer to call merge_inner(), which has less magic behavior.
308
 
    """
309
 
    if this_dir is None:
310
 
        this_dir = u'.'
311
 
    this_branch = Branch.open_containing(this_dir)[0]
312
 
    if show_base and not merge_type is ApplyMerge3:
313
 
        raise BzrCommandError("Show-base is not supported for this merge"
314
 
                              " type. %s" % merge_type)
315
 
    if reprocess and not merge_type is ApplyMerge3:
316
 
        raise BzrCommandError("Reprocess is not supported for this merge"
317
 
                              " type. %s" % merge_type)
318
 
    if reprocess and show_base:
319
 
        raise BzrCommandError("Cannot reprocess and show base.")
320
 
    merger = Merger(this_branch)
321
 
    merger.check_basis(check_clean)
322
 
    merger.set_other(other_revision)
323
 
    merger.set_base(base_revision)
324
 
    if merger.base_rev_id == merger.other_rev_id:
325
 
        note('Nothing to do.')
326
 
        return 0
327
 
    merger.backup_files = backup_files
328
 
    merger.merge_type = merge_type 
329
 
    merger.set_interesting_files(file_list)
330
 
    merger.show_base = show_base 
331
 
    merger.reprocess = reprocess
332
 
    merger.conflict_handler = MergeConflictHandler(merger.this_tree, 
333
 
                                                   merger.base_tree, 
334
 
                                                   merger.other_tree,
335
 
                                                   ignore_zero=ignore_zero)
336
 
    conflicts = merger.do_merge()
337
 
    merger.set_pending()
338
 
    return conflicts
339
 
 
340
 
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
341
 
                backup_files=False, 
342
 
                merge_type=ApplyMerge3, 
343
 
                interesting_ids=None, 
344
 
                show_base=False, 
345
 
                reprocess=False, 
346
 
                other_rev_id=None,
347
 
                interesting_files=None):
348
 
    """Primary interface for merging. 
349
 
 
350
 
        typical use is probably 
351
 
        'merge_inner(branch, branch.get_revision_tree(other_revision),
352
 
                     branch.get_revision_tree(base_revision))'
353
 
        """
354
 
    merger = Merger(this_branch, other_tree, base_tree)
355
 
    merger.backup_files = backup_files
356
 
    merger.merge_type = merge_type
357
 
    merger.interesting_ids = interesting_ids
358
 
    if interesting_files:
359
 
        assert not interesting_ids, ('Only supply interesting_ids'
360
 
                                     ' or interesting_files')
361
 
        merger._set_interesting_files(interesting_files)
362
 
    merger.show_base = show_base 
363
 
    merger.reprocess = reprocess
364
 
    merger.conflict_handler = MergeConflictHandler(merger.this_tree, base_tree, 
365
 
                                                   other_tree,
366
 
                                                   ignore_zero=ignore_zero)
367
 
    merger.other_rev_id = other_rev_id
368
 
    merger.other_basis = other_rev_id
369
 
    return merger.do_merge()
370
 
 
371
 
 
372
81
class Merger(object):
373
 
    def __init__(self, this_branch, other_tree=None, base_tree=None):
 
82
    def __init__(self, this_branch, other_tree=None, base_tree=None, this_tree=None):
374
83
        object.__init__(self)
 
84
        assert this_tree is not None, "this_tree is required"
375
85
        self.this_branch = this_branch
376
86
        self.this_basis = this_branch.last_revision()
377
87
        self.this_rev_id = None
378
 
        self.this_tree = this_branch.working_tree()
 
88
        self.this_tree = this_tree
379
89
        self.this_revision_tree = None
380
90
        self.this_basis_tree = None
381
91
        self.other_tree = other_tree
385
95
        self.interesting_ids = None
386
96
        self.show_base = False
387
97
        self.reprocess = False
388
 
        self.conflict_handler = MergeConflictHandler(self.this_tree, base_tree, 
389
 
                                                     other_tree)
390
98
 
391
99
    def revision_tree(self, revision_id):
392
 
        return self.this_branch.revision_tree(revision_id)
 
100
        return self.this_branch.repository.revision_tree(revision_id)
393
101
 
394
102
    def ensure_revision_trees(self):
395
103
        if self.this_revision_tree is None:
396
 
            self.this_basis_tree = self.this_branch.revision_tree(
 
104
            self.this_basis_tree = self.this_branch.repository.revision_tree(
397
105
                self.this_basis)
398
106
            if self.this_basis == self.this_rev_id:
399
107
                self.this_revision_tree = self.this_basis_tree
400
108
 
401
 
 
402
109
        if self.other_rev_id is None:
403
110
            other_basis_tree = self.revision_tree(self.other_basis)
404
111
            changes = compare_trees(self.other_tree, other_basis_tree)
407
114
            other_rev_id = other_basis
408
115
            self.other_tree = other_basis_tree
409
116
 
410
 
 
411
117
    def file_revisions(self, file_id):
412
118
        self.ensure_revision_trees()
413
119
        def get_id(tree, file_id):
421
127
 
422
128
        trees = (self.this_basis_tree, self.other_tree)
423
129
        return [get_id(tree, file_id) for tree in trees]
424
 
            
425
 
 
426
 
    def merge_factory(self, file_id, base, other):
427
 
        if self.merge_type.history_based:
428
 
            if self.show_base is True:
429
 
                raise BzrError("Cannot show base for hisory-based merges")
430
 
            if self.reprocess is True:
431
 
                raise BzrError("Cannot reprocess history-based merges")
432
 
                
433
 
            t_revid, o_revid = self.file_revisions(file_id)
434
 
            weave = self.this_basis_tree.get_weave(file_id)
435
 
            contents_change = self.merge_type(weave, t_revid, o_revid)
436
 
        else:
437
 
            if self.show_base is True or self.reprocess is True:
438
 
                contents_change = self.merge_type(file_id, base, other, 
439
 
                                                  show_base=self.show_base, 
440
 
                                                  reprocess=self.reprocess)
441
 
            else:
442
 
                contents_change = self.merge_type(file_id, base, other)
443
 
        if self.backup_files:
444
 
            contents_change = BackupBeforeChange(contents_change)
445
 
        return contents_change
446
130
 
447
131
    def check_basis(self, check_clean):
448
132
        if self.this_basis is None:
453
137
                raise BzrCommandError("Working tree has uncommitted changes.")
454
138
 
455
139
    def compare_basis(self):
456
 
        changes = compare_trees(self.this_branch.working_tree(), 
457
 
                                self.this_branch.basis_tree(), False)
 
140
        changes = compare_trees(self.this_tree, 
 
141
                                self.this_tree.basis_tree(), False)
458
142
        if not changes.has_changed():
459
143
            self.this_rev_id = self.this_basis
460
144
 
472
156
            return
473
157
 
474
158
        interesting_ids = set()
475
 
        for fname in file_list:
476
 
            path = self.this_tree.relpath(fname)
 
159
        for path in file_list:
477
160
            found_id = False
478
161
            for tree in (self.this_tree, self.base_tree, self.other_tree):
479
162
                file_id = tree.inventory.path2id(path)
481
164
                    interesting_ids.add(file_id)
482
165
                    found_id = True
483
166
            if not found_id:
484
 
                raise NotVersionedError(path=fname)
 
167
                raise NotVersionedError(path=path)
485
168
        self.interesting_ids = interesting_ids
486
169
 
487
170
    def set_pending(self):
489
172
            return
490
173
        if self.other_rev_id is None:
491
174
            return
492
 
        if self.other_rev_id in self.this_branch.get_ancestry(self.this_basis):
 
175
        ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
 
176
        if self.other_rev_id in ancestry:
493
177
            return
494
 
        self.this_branch.working_tree().add_pending_merge(self.other_rev_id)
 
178
        self.this_tree.add_pending_merge(self.other_rev_id)
495
179
 
496
180
    def set_other(self, other_revision):
497
 
        other_branch, self.other_tree = get_tree(other_revision, 
498
 
                                                 self.this_branch)
 
181
        other_branch, self.other_tree = _get_tree(other_revision, 
 
182
                                                  self.this_branch)
499
183
        if other_revision[1] == -1:
500
184
            self.other_rev_id = other_branch.last_revision()
501
185
            if self.other_rev_id is None:
509
193
            self.other_basis = other_branch.last_revision()
510
194
            if self.other_basis is None:
511
195
                raise NoCommits(other_branch)
512
 
        fetch(from_branch=other_branch, to_branch=self.this_branch, 
513
 
              last_revision=self.other_basis)
 
196
        if other_branch.base != self.this_branch.base:
 
197
            fetch(from_branch=other_branch, to_branch=self.this_branch, 
 
198
                  last_revision=self.other_basis)
514
199
 
515
200
    def set_base(self, base_revision):
516
201
        mutter("doing merge() with no base_revision specified")
518
203
            try:
519
204
                self.base_rev_id = common_ancestor(self.this_basis, 
520
205
                                                   self.other_basis, 
521
 
                                                   self.this_branch)
 
206
                                                   self.this_branch.repository)
522
207
            except NoCommonAncestor:
523
208
                raise UnrelatedBranches()
524
 
            self.base_tree = get_revid_tree(self.this_branch, self.base_rev_id,
 
209
            self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
525
210
                                            None)
526
211
            self.base_is_ancestor = True
527
212
        else:
528
 
            base_branch, self.base_tree = get_tree(base_revision)
 
213
            base_branch, self.base_tree = _get_tree(base_revision)
529
214
            if base_revision[1] == -1:
530
215
                self.base_rev_id = base_branch.last_revision()
531
216
            elif base_revision[1] is None:
538
223
                                                self.this_branch)
539
224
 
540
225
    def do_merge(self):
541
 
        def get_inventory(tree):
542
 
            return tree.inventory
543
 
        
544
 
        inv_changes = merge_flex(self.this_tree, self.base_tree, 
545
 
                                 self.other_tree,
546
 
                                 generate_changeset, get_inventory,
547
 
                                 self.conflict_handler,
548
 
                                 merge_factory=self.merge_factory, 
549
 
                                 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))
550
246
 
551
 
        adjust_ids = []
552
 
        for id, path in inv_changes.iteritems():
553
 
            if path is not None:
554
 
                if path == u'.':
555
 
                    path = u''
556
 
                else:
557
 
                    assert path.startswith('.' + '/') or path.startswith('.' + '\\'), "path is %s" % path
558
 
                path = path[2:]
559
 
            adjust_ids.append((path, id))
560
 
        if len(adjust_ids) > 0:
561
 
            self.this_branch.working_tree().set_inventory(self.regen_inventory(adjust_ids))
562
 
        conflicts = self.conflict_handler.conflicts
563
 
        self.conflict_handler.finalize()
564
 
        return conflicts
 
247
        return len(merge.cooked_conflicts)
565
248
 
566
249
    def regen_inventory(self, new_entries):
567
 
        old_entries = self.this_branch.working_tree().read_working_inventory()
 
250
        old_entries = self.this_tree.read_working_inventory()
568
251
        new_inventory = {}
569
252
        by_path = {}
570
253
        new_entries_map = {} 
623
306
        new_inventory_list.sort()
624
307
        return new_inventory_list
625
308
 
626
 
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
627
 
                     "diff3": (Diff3Merge,  "Merge using external diff3"),
628
 
                     'weave': (WeaveMerge, "Weave-based merge")
 
309
 
 
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")
629
831
              }
630