22
from fetch import greedy_fetch
24
23
import bzrlib.osutils
25
24
import bzrlib.revision
26
25
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
27
26
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
28
from bzrlib.changeset import Inventory, Diff3Merge
27
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
29
28
from bzrlib.branch import Branch
30
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
31
30
from bzrlib.errors import NoCommits
32
31
from bzrlib.delta import compare_trees
33
from bzrlib.trace import mutter, warning
34
from bzrlib.fetch import greedy_fetch
32
from bzrlib.trace import mutter, warning, note
33
from bzrlib.fetch import greedy_fetch, fetch
35
34
from bzrlib.revision import is_ancestor
35
from bzrlib.osutils import rename
36
from bzrlib.revision import common_ancestor, MultipleRevisionSources
37
from bzrlib.errors import NoSuchRevision
39
# TODO: build_working_dir can be built on something simpler than merge()
41
# FIXME: merge() parameters seem oriented towards the command line
42
# NOTABUG: merge is a helper for commandline functions. merge_inner is the
43
# the core functionality.
37
45
# comments from abentley on irc: merge happens in two stages, each
38
46
# of which generates a changeset object
142
def rem_contents_conflict(self, filename, this_contents, base_contents):
143
base_contents(filename+".BASE", self, False)
144
this_contents(filename+".THIS", self, False)
145
return ReplaceContents(this_contents, None)
147
def rem_contents_conflict(self, filename, this_contents, base_contents):
148
base_contents(filename+".BASE", self, False)
149
this_contents(filename+".THIS", self, False)
150
self.conflict("Other branch deleted locally modified file %s" %
152
return ReplaceContents(this_contents, None)
154
def abs_this_path(self, file_id):
155
"""Return the absolute path for a file_id in the this tree."""
156
return self.this_tree.id2abspath(file_id)
158
def add_missing_parents(self, file_id, tree):
159
"""If some of the parents for file_id are missing, add them."""
160
entry = tree.inventory[file_id]
161
if entry.parent_id not in self.this_tree:
162
return self.create_all_missing(entry.parent_id, tree)
164
return self.abs_this_path(entry.parent_id)
166
def create_all_missing(self, file_id, tree):
167
"""Add contents for a file_id and all its parents to a tree."""
168
entry = tree.inventory[file_id]
169
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
170
abspath = self.create_all_missing(entry.parent_id, tree)
172
abspath = self.abs_this_path(entry.parent_id)
173
entry_path = os.path.join(abspath, entry.name)
174
if not os.path.isdir(entry_path):
175
self.create(file_id, entry_path, tree)
178
def create(self, file_id, path, tree, reverse=False):
179
"""Uses tree data to create a filesystem object for the file_id"""
180
from changeset import get_contents
181
get_contents(tree, file_id)(path, self, reverse)
183
def missing_for_merge(self, file_id, other_path):
184
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
185
self.conflict("Other branch modified locally deleted file %s" %
187
parent_dir = self.add_missing_parents(file_id, self.other_tree)
188
stem = os.path.join(parent_dir, os.path.basename(other_path))
189
self.create(file_id, stem+".OTHER", self.other_tree)
190
self.create(file_id, stem+".BASE", self.base_tree)
192
def threeway_contents_conflict(filename, this_contents, base_contents,
194
self.conflict("Three-way conflict merging %s" % filename)
131
196
def finalize(self):
132
197
if not self.ignore_zero:
133
print "%d conflicts encountered.\n" % self.conflicts
198
note("%d conflicts encountered.\n" % self.conflicts)
135
def get_tree(treespec, temp_root, label, local_branch=None):
200
def get_tree(treespec, local_branch=None):
136
201
location, revno = treespec
137
branch = Branch.open(location)
202
branch = Branch.open_containing(location)[0]
138
203
if revno is None:
140
205
elif revno == -1:
141
revision = branch.last_patch()
206
revision = branch.last_revision()
143
208
revision = branch.get_rev_id(revno)
144
return branch, get_revid_tree(branch, revision, temp_root, label,
209
return branch, get_revid_tree(branch, revision, local_branch)
147
def get_revid_tree(branch, revision, temp_root, label, local_branch):
211
def get_revid_tree(branch, revision, local_branch):
148
212
if revision is None:
149
213
base_tree = branch.working_tree()
153
217
base_tree = local_branch.revision_tree(revision)
155
219
base_tree = branch.revision_tree(revision)
156
temp_path = os.path.join(temp_root, label)
158
return MergeTree(base_tree, temp_path)
161
223
def file_exists(tree, file_id):
162
224
return tree.has_filename(tree.id2path(file_id))
165
class MergeTree(object):
166
def __init__(self, tree, tempdir):
167
object.__init__(self)
168
if hasattr(tree, "basedir"):
169
self.root = tree.basedir
173
self.tempdir = tempdir
174
os.mkdir(os.path.join(self.tempdir, "texts"))
178
return self.tree.__iter__()
180
def __contains__(self, file_id):
181
return file_id in self.tree
183
def get_file(self, file_id):
184
return self.tree.get_file(file_id)
186
def get_file_sha1(self, id):
187
return self.tree.get_file_sha1(id)
189
def id2path(self, file_id):
190
return self.tree.id2path(file_id)
192
def has_id(self, file_id):
193
return self.tree.has_id(file_id)
195
def has_or_had_id(self, file_id):
196
if file_id == self.tree.inventory.root.file_id:
198
return self.tree.inventory.has_id(file_id)
200
def has_or_had_id(self, file_id):
201
if file_id == self.tree.inventory.root.file_id:
203
return self.tree.inventory.has_id(file_id)
205
def readonly_path(self, id):
206
if id not in self.tree:
208
if self.root is not None:
209
return self.tree.abspath(self.tree.id2path(id))
211
if self.tree.inventory[id].kind in ("directory", "root_directory"):
213
if not self.cached.has_key(id):
214
path = os.path.join(self.tempdir, "texts", id)
215
outfile = file(path, "wb")
216
outfile.write(self.tree.get_file(id).read())
217
assert(os.path.exists(path))
218
self.cached[id] = path
219
return self.cached[id]
227
def build_working_dir(to_dir):
228
"""Build a working directory in an empty directory.
230
to_dir is a directory containing branch metadata but no working files,
231
typically constructed by cloning an existing branch.
233
This is split out as a special idiomatic case of merge. It could
234
eventually be done by just building the tree directly calling into
235
lower-level code (e.g. constructing a changeset).
237
# RBC 20051019 is this not just 'export' ?
238
merge((to_dir, -1), (to_dir, 0), this_dir=to_dir,
239
check_clean=False, ignore_zero=True)
223
242
def merge(other_revision, base_revision,
224
243
check_clean=True, ignore_zero=False,
225
244
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
245
file_list=None, show_base=False):
227
246
"""Merge changes into a tree.
236
255
If true, this_dir must have no uncommitted changes before the
238
all available ancestors of other_revision and base_revision are
257
ignore_zero - If true, suppress the "zero conflicts" message when
258
there are no conflicts; should be set when doing something we expect
259
to complete perfectly.
261
All available ancestors of other_revision and base_revision are
239
262
automatically pulled into the branch.
241
from bzrlib.revision import common_ancestor, MultipleRevisionSources
242
from bzrlib.errors import NoSuchRevision
243
tempdir = tempfile.mkdtemp(prefix="bzr-")
247
this_branch = Branch.open_containing(this_dir)
248
this_rev_id = this_branch.last_patch()
249
if this_rev_id is None:
250
raise BzrCommandError("This branch has no commits")
252
changes = compare_trees(this_branch.working_tree(),
253
this_branch.basis_tree(), False)
254
if changes.has_changed():
255
raise BzrCommandError("Working tree has uncommitted changes.")
256
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
258
if other_revision[1] == -1:
259
other_rev_id = other_branch.last_patch()
260
if other_rev_id is None:
261
raise NoCommits(other_branch)
262
other_basis = other_rev_id
263
elif other_revision[1] is not None:
264
other_rev_id = other_branch.get_rev_id(other_revision[1])
265
other_basis = other_rev_id
268
other_basis = other_branch.last_patch()
269
if other_basis is None:
270
raise NoCommits(other_branch)
271
if base_revision == [None, None]:
273
base_rev_id = common_ancestor(this_rev_id, other_basis,
275
except NoCommonAncestor:
276
raise UnrelatedBranches()
277
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
279
base_is_ancestor = True
281
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
282
if base_revision[1] == -1:
283
base_rev_id = base_branch.last_patch()
284
elif base_revision[1] is None:
287
base_rev_id = base_branch.get_rev_id(base_revision[1])
288
multi_source = MultipleRevisionSources(this_branch, base_branch)
289
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
291
if file_list is None:
292
interesting_ids = None
294
interesting_ids = set()
295
this_tree = this_branch.working_tree()
296
for fname in file_list:
297
path = this_branch.relpath(fname)
299
for tree in (this_tree, base_tree.tree, other_tree.tree):
300
file_id = tree.inventory.path2id(path)
301
if file_id is not None:
302
interesting_ids.add(file_id)
305
raise BzrCommandError("%s is not a source file in any"
307
merge_inner(this_branch, other_tree, base_tree, tempdir,
308
ignore_zero=ignore_zero, backup_files=backup_files,
309
merge_type=merge_type, interesting_ids=interesting_ids)
310
if base_is_ancestor and other_rev_id is not None\
311
and other_rev_id not in this_branch.revision_history():
312
this_branch.add_pending_merge(other_rev_id)
314
shutil.rmtree(tempdir)
266
this_branch = Branch.open_containing(this_dir)[0]
267
this_rev_id = this_branch.last_revision()
268
if this_rev_id is None:
269
raise BzrCommandError("This branch has no commits")
271
changes = compare_trees(this_branch.working_tree(),
272
this_branch.basis_tree(), False)
273
if changes.has_changed():
274
raise BzrCommandError("Working tree has uncommitted changes.")
275
other_branch, other_tree = get_tree(other_revision, this_branch)
276
if other_revision[1] == -1:
277
other_rev_id = other_branch.last_revision()
278
if other_rev_id is None:
279
raise NoCommits(other_branch)
280
other_basis = other_rev_id
281
elif other_revision[1] is not None:
282
other_rev_id = other_branch.get_rev_id(other_revision[1])
283
other_basis = other_rev_id
286
other_basis = other_branch.last_revision()
287
if other_basis is None:
288
raise NoCommits(other_branch)
289
if base_revision == [None, None]:
291
base_rev_id = common_ancestor(this_rev_id, other_basis,
293
except NoCommonAncestor:
294
raise UnrelatedBranches()
295
base_tree = get_revid_tree(this_branch, base_rev_id, None)
296
base_is_ancestor = True
298
base_branch, base_tree = get_tree(base_revision)
299
if base_revision[1] == -1:
300
base_rev_id = base_branch.last_revision()
301
elif base_revision[1] is None:
304
base_rev_id = base_branch.get_rev_id(base_revision[1])
305
fetch(from_branch=base_branch, to_branch=this_branch)
306
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
308
if file_list is None:
309
interesting_ids = None
311
interesting_ids = set()
312
this_tree = this_branch.working_tree()
313
for fname in file_list:
314
path = this_tree.relpath(fname)
316
for tree in (this_tree, base_tree, other_tree):
317
file_id = tree.inventory.path2id(path)
318
if file_id is not None:
319
interesting_ids.add(file_id)
322
raise BzrCommandError("%s is not a source file in any"
324
merge_inner(this_branch, other_tree, base_tree, tempdir=None,
325
ignore_zero=ignore_zero, backup_files=backup_files,
326
merge_type=merge_type, interesting_ids=interesting_ids,
328
if base_is_ancestor and other_rev_id is not None\
329
and other_rev_id not in this_branch.revision_history():
330
this_branch.add_pending_merge(other_rev_id)
317
333
def set_interesting(inventory_a, inventory_b, interesting_ids):
322
338
source_file.interesting = source_file.id in interesting_ids
325
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
326
"""Generate a changeset. If interesting_ids is supplied, only changes
327
to those files will be shown. Metadata changes are stripped.
329
cset = generate_changeset(tree_a, tree_b, interesting_ids)
330
for entry in cset.entries.itervalues():
331
entry.metadata_change = None
335
def merge_inner(this_branch, other_tree, base_tree, tempdir,
336
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
337
interesting_ids=None):
341
def merge_inner(this_branch, other_tree, base_tree, tempdir=None,
342
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
343
interesting_ids=None, show_base=False):
344
"""Primary interface for merging.
346
typical use is probably
347
'merge_inner(branch, branch.get_revision_tree(other_revision),
348
branch.get_revision_tree(base_revision))'
351
_tempdir = tempfile.mkdtemp(prefix="bzr-")
355
_merge_inner(this_branch, other_tree, base_tree, _tempdir,
356
ignore_zero, merge_type, backup_files, interesting_ids,
360
shutil.rmtree(_tempdir)
363
def _merge_inner(this_branch, other_tree, base_tree, user_tempdir,
364
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
365
interesting_ids=None, show_base=False):
339
366
def merge_factory(file_id, base, other):
340
contents_change = merge_type(file_id, base, other)
367
if show_base is True:
368
contents_change = merge_type(file_id, base, other, show_base=True)
370
contents_change = merge_type(file_id, base, other)
342
372
contents_change = BackupBeforeChange(contents_change)
343
373
return contents_change
345
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
375
this_tree = get_tree((this_branch.base, None))[1]
347
377
def get_inventory(tree):
348
return tree.tree.inventory
378
return tree.inventory
350
380
inv_changes = merge_flex(this_tree, base_tree, other_tree,
351
generate_cset_optimized, get_inventory,
352
MergeConflictHandler(ignore_zero=ignore_zero),
381
generate_changeset, get_inventory,
382
MergeConflictHandler(this_tree, base_tree,
383
other_tree, ignore_zero=ignore_zero),
353
384
merge_factory=merge_factory,
354
385
interesting_ids=interesting_ids)