22
from fetch import greedy_fetch
23
24
import bzrlib.osutils
24
25
import bzrlib.revision
25
26
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
26
27
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
27
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
28
from bzrlib.changeset import Inventory, Diff3Merge
28
29
from bzrlib.branch import Branch
29
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
30
31
from bzrlib.errors import NoCommits
31
32
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning, note
33
from bzrlib.fetch import greedy_fetch, fetch
33
from bzrlib.trace import mutter, warning
34
from bzrlib.fetch import greedy_fetch
34
35
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.
45
37
# comments from abentley on irc: merge happens in two stages, each
46
38
# 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)
196
131
def finalize(self):
197
132
if not self.ignore_zero:
198
note("%d conflicts encountered.\n" % self.conflicts)
133
print "%d conflicts encountered.\n" % self.conflicts
200
def get_tree(treespec, local_branch=None):
135
def get_tree(treespec, temp_root, label, local_branch=None):
201
136
location, revno = treespec
202
branch = Branch.open_containing(location)[0]
137
branch = Branch.open(location)
203
138
if revno is None:
205
140
elif revno == -1:
206
revision = branch.last_revision()
141
revision = branch.last_patch()
208
143
revision = branch.get_rev_id(revno)
209
return branch, get_revid_tree(branch, revision, local_branch)
144
return branch, get_revid_tree(branch, revision, temp_root, label,
211
def get_revid_tree(branch, revision, local_branch):
147
def get_revid_tree(branch, revision, temp_root, label, local_branch):
212
148
if revision is None:
213
149
base_tree = branch.working_tree()
217
153
base_tree = local_branch.revision_tree(revision)
219
155
base_tree = branch.revision_tree(revision)
156
temp_path = os.path.join(temp_root, label)
158
return MergeTree(base_tree, temp_path)
223
161
def file_exists(tree, file_id):
224
162
return tree.has_filename(tree.id2path(file_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)
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]
242
223
def merge(other_revision, base_revision,
243
224
check_clean=True, ignore_zero=False,
244
225
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
245
file_list=None, show_base=False):
246
227
"""Merge changes into a tree.
255
236
If true, this_dir must have no uncommitted changes before the
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
238
all available ancestors of other_revision and base_revision are
262
239
automatically pulled into the branch.
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)
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)
333
317
def set_interesting(inventory_a, inventory_b, interesting_ids):
338
322
source_file.interesting = source_file.id in interesting_ids
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):
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):
366
339
def merge_factory(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)
340
contents_change = merge_type(file_id, base, other)
372
342
contents_change = BackupBeforeChange(contents_change)
373
343
return contents_change
375
this_tree = get_tree((this_branch.base, None))[1]
345
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
377
347
def get_inventory(tree):
378
return tree.inventory
348
return tree.tree.inventory
380
350
inv_changes = merge_flex(this_tree, base_tree, other_tree,
381
generate_changeset, get_inventory,
382
MergeConflictHandler(this_tree, base_tree,
383
other_tree, ignore_zero=ignore_zero),
351
generate_cset_optimized, get_inventory,
352
MergeConflictHandler(ignore_zero=ignore_zero),
384
353
merge_factory=merge_factory,
385
354
interesting_ids=interesting_ids)