~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

 * bzr add now lists how many files were ignored per glob.  add --verbose
   lists the specific files.  (Aaron Bentley)

Show diffs side-by-side

added added

removed removed

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