~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

Handled simultaneous renames of parent and child better

Show diffs side-by-side

added added

removed removed

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