15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
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
29
from bzrlib.branch import find_branch
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches
27
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
28
from bzrlib.branch import Branch
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
30
from bzrlib.errors import NoCommits
31
31
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning
33
from bzrlib.fetch import greedy_fetch
32
from bzrlib.trace import mutter, warning, note
33
from bzrlib.fetch import greedy_fetch, fetch
34
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.
36
45
# comments from abentley on irc: merge happens in two stages, each
37
46
# of which generates a changeset object
46
55
conflict that are not explicitly handled cause an exception and
47
56
terminate the merge.
49
def __init__(self, dir, ignore_zero=False):
50
ExceptionConflictHandler.__init__(self, dir)
58
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
59
ExceptionConflictHandler.__init__(self)
52
61
self.ignore_zero = ignore_zero
62
self.this_tree = this_tree
63
self.base_tree = base_tree
64
self.other_tree = other_tree
54
66
def copy(self, source, dest):
55
67
"""Copy the text and mode of a file
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
relpath = self.this_tree.id2path(file_id)
157
return self.this_tree.tree.abspath(relpath)
159
def add_missing_parents(self, file_id, tree):
160
"""If some of the parents for file_id are missing, add them."""
161
entry = tree.tree.inventory[file_id]
162
if entry.parent_id not in self.this_tree:
163
return self.create_all_missing(entry.parent_id, tree)
165
return self.abs_this_path(entry.parent_id)
167
def create_all_missing(self, file_id, tree):
168
"""Add contents for a file_id and all its parents to a tree."""
169
entry = tree.tree.inventory[file_id]
170
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
171
abspath = self.create_all_missing(entry.parent_id, tree)
173
abspath = self.abs_this_path(entry.parent_id)
174
entry_path = os.path.join(abspath, entry.name)
175
if not os.path.isdir(entry_path):
176
self.create(file_id, entry_path, tree)
179
def create(self, file_id, path, tree, reverse=False):
180
"""Uses tree data to create a filesystem object for the file_id"""
181
from merge_core import get_id_contents
182
get_id_contents(file_id, tree)(path, self, reverse)
184
def missing_for_merge(self, file_id, other_path):
185
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
186
self.conflict("Other branch modified locally deleted file %s" %
188
parent_dir = self.add_missing_parents(file_id, self.other_tree)
189
stem = os.path.join(parent_dir, os.path.basename(other_path))
190
self.create(file_id, stem+".OTHER", self.other_tree)
191
self.create(file_id, stem+".BASE", self.base_tree)
130
193
def finalize(self):
131
194
if not self.ignore_zero:
132
print "%d conflicts encountered.\n" % self.conflicts
195
note("%d conflicts encountered.\n" % self.conflicts)
134
197
def get_tree(treespec, temp_root, label, local_branch=None):
135
198
location, revno = treespec
136
branch = find_branch(location)
199
branch = Branch.open_containing(location)
137
200
if revno is None:
139
202
elif revno == -1:
140
revision = branch.last_patch()
203
revision = branch.last_revision()
142
revision = branch.lookup_revision(revno)
205
revision = branch.get_rev_id(revno)
143
206
return branch, get_revid_tree(branch, revision, temp_root, label,
154
217
base_tree = branch.revision_tree(revision)
155
218
temp_path = os.path.join(temp_root, label)
156
219
os.mkdir(temp_path)
157
return MergeTree(base_tree, temp_path)
220
return MergeAdapterTree(base_tree, temp_path)
160
223
def file_exists(tree, file_id):
161
224
return tree.has_filename(tree.id2path(file_id))
164
class MergeTree(object):
227
class MergeAdapterTree(object):
228
"""MergeAdapterTree adapts a normal tree for merge_inner to use.
230
The interface the merge_inner needs is nearly but not quite
231
the same as that of bzrlib.tree with the exception of readonly_path.
165
234
def __init__(self, tree, tempdir):
166
235
object.__init__(self)
167
236
if hasattr(tree, "basedir"):
207
280
if self.root is not None:
208
281
return self.tree.abspath(self.tree.id2path(id))
210
if self.tree.inventory[id].kind in ("directory", "root_directory"):
283
kind = self.tree.inventory[id].kind
284
if kind in ("directory", "root_directory"):
211
285
return self.tempdir
212
286
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))
288
path = os.path.join(self.tempdir, "texts", id)
289
outfile = file(path, "wb")
290
outfile.write(self.tree.get_file(id).read())
291
assert(bzrlib.osutils.lexists(path))
292
if self.tree.is_executable(id):
295
assert kind == "symlink"
296
path = os.path.join(self.tempdir, "symlinks", id)
297
target = self.tree.get_symlink_target(id)
298
os.symlink(target, path)
217
299
self.cached[id] = path
218
300
return self.cached[id]
303
def build_working_dir(to_dir):
304
"""Build a working directory in an empty directory.
306
to_dir is a directory containing branch metadata but no working files,
307
typically constructed by cloning an existing branch.
309
This is split out as a special idiomatic case of merge. It could
310
eventually be done by just building the tree directly calling into
311
lower-level code (e.g. constructing a changeset).
313
merge((to_dir, -1), (to_dir, 0), this_dir=to_dir,
314
check_clean=False, ignore_zero=True)
222
317
def merge(other_revision, base_revision,
223
318
check_clean=True, ignore_zero=False,
235
330
If true, this_dir must have no uncommitted changes before the
237
all available ancestors of other_revision and base_revision are
332
ignore_zero - If true, suppress the "zero conflicts" message when
333
there are no conflicts; should be set when doing something we expect
334
to complete perfectly.
336
All available ancestors of other_revision and base_revision are
238
337
automatically pulled into the branch.
240
from bzrlib.revision import common_ancestor, MultipleRevisionSources
241
from bzrlib.errors import NoSuchRevision
242
339
tempdir = tempfile.mkdtemp(prefix="bzr-")
244
341
if this_dir is None:
246
this_branch = find_branch(this_dir)
247
this_rev_id = this_branch.last_patch()
343
this_branch = Branch.open_containing(this_dir)
344
this_rev_id = this_branch.last_revision()
248
345
if this_rev_id is None:
249
346
raise BzrCommandError("This branch has no commits")
255
352
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
257
354
if other_revision[1] == -1:
258
other_rev_id = other_branch.last_patch()
355
other_rev_id = other_branch.last_revision()
356
if other_rev_id is None:
357
raise NoCommits(other_branch)
259
358
other_basis = other_rev_id
260
359
elif other_revision[1] is not None:
261
other_rev_id = other_branch.lookup_revision(other_revision[1])
360
other_rev_id = other_branch.get_rev_id(other_revision[1])
262
361
other_basis = other_rev_id
264
363
other_rev_id = None
265
other_basis = other_branch.last_patch()
364
other_basis = other_branch.last_revision()
365
if other_basis is None:
366
raise NoCommits(other_branch)
266
367
if base_revision == [None, None]:
267
base_rev_id = common_ancestor(this_rev_id, other_basis,
269
if base_rev_id is None:
369
base_rev_id = common_ancestor(this_rev_id, other_basis,
371
except NoCommonAncestor:
270
372
raise UnrelatedBranches()
271
373
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
275
377
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
276
378
if base_revision[1] == -1:
277
base_rev_id = base_branch.last_patch()
379
base_rev_id = base_branch.last_revision()
278
380
elif base_revision[1] is None:
279
381
base_rev_id = None
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,
287
base_is_ancestor = False
383
base_rev_id = base_branch.get_rev_id(base_revision[1])
384
fetch(from_branch=base_branch, to_branch=this_branch)
385
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
288
387
if file_list is None:
289
388
interesting_ids = None
291
390
interesting_ids = set()
292
391
this_tree = this_branch.working_tree()
293
392
for fname in file_list:
294
path = this_branch.relpath(fname)
393
path = this_tree.relpath(fname)
296
395
for tree in (this_tree, base_tree.tree, other_tree.tree):
297
396
file_id = tree.inventory.path2id(path)
319
418
source_file.interesting = source_file.id in interesting_ids
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.
326
cset = generate_changeset(tree_a, tree_b, interesting_ids)
327
for entry in cset.entries.itervalues():
328
entry.metadata_change = None
332
421
def merge_inner(this_branch, other_tree, base_tree, tempdir,
333
422
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
334
423
interesting_ids=None):
345
434
return tree.tree.inventory
347
436
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),
437
generate_changeset, get_inventory,
438
MergeConflictHandler(this_tree, base_tree,
439
other_tree, ignore_zero=ignore_zero),
351
440
merge_factory=merge_factory,
352
441
interesting_ids=interesting_ids)