15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
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.branch import Branch
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
30
from bzrlib.errors import NoCommits
28
from bzrlib.changeset import Inventory, Diff3Merge
29
from bzrlib.branch import find_branch
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches
31
31
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning, note
33
from bzrlib.fetch import greedy_fetch, fetch
32
from bzrlib.trace import mutter, warning
33
from bzrlib.fetch import greedy_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.
45
36
# comments from abentley on irc: merge happens in two stages, each
46
37
# of which generates a changeset object
55
46
conflict that are not explicitly handled cause an exception and
56
47
terminate the merge.
58
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
59
ExceptionConflictHandler.__init__(self)
49
def __init__(self, dir, ignore_zero=False):
50
ExceptionConflictHandler.__init__(self, dir)
61
52
self.ignore_zero = ignore_zero
62
self.this_tree = this_tree
63
self.base_tree = base_tree
64
self.other_tree = other_tree
66
54
def copy(self, source, dest):
67
55
"""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)
193
130
def finalize(self):
194
131
if not self.ignore_zero:
195
note("%d conflicts encountered.\n" % self.conflicts)
132
print "%d conflicts encountered.\n" % self.conflicts
197
134
def get_tree(treespec, temp_root, label, local_branch=None):
198
135
location, revno = treespec
199
branch = Branch.open_containing(location)
136
branch = find_branch(location)
200
137
if revno is None:
202
139
elif revno == -1:
203
revision = branch.last_revision()
140
revision = branch.last_patch()
205
revision = branch.get_rev_id(revno)
142
revision = branch.lookup_revision(revno)
206
143
return branch, get_revid_tree(branch, revision, temp_root, label,
274
207
if self.root is not None:
275
208
return self.tree.abspath(self.tree.id2path(id))
277
kind = self.tree.inventory[id].kind
278
if kind in ("directory", "root_directory"):
210
if self.tree.inventory[id].kind in ("directory", "root_directory"):
279
211
return self.tempdir
280
212
if not self.cached.has_key(id):
282
path = os.path.join(self.tempdir, "texts", id)
283
outfile = file(path, "wb")
284
outfile.write(self.tree.get_file(id).read())
285
assert(bzrlib.osutils.lexists(path))
286
if self.tree.is_executable(id):
289
assert kind == "symlink"
290
path = os.path.join(self.tempdir, "symlinks", id)
291
target = self.tree.get_symlink_target(id)
292
os.symlink(target, path)
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))
293
217
self.cached[id] = path
294
218
return self.cached[id]
297
def build_working_dir(to_dir):
298
"""Build a working directory in an empty directory.
300
to_dir is a directory containing branch metadata but no working files,
301
typically constructed by cloning an existing branch.
303
This is split out as a special idiomatic case of merge. It could
304
eventually be done by just building the tree directly calling into
305
lower-level code (e.g. constructing a changeset).
307
merge((to_dir, -1), (to_dir, 0), this_dir=to_dir,
308
check_clean=False, ignore_zero=True)
311
222
def merge(other_revision, base_revision,
312
223
check_clean=True, ignore_zero=False,
324
235
If true, this_dir must have no uncommitted changes before the
326
ignore_zero - If true, suppress the "zero conflicts" message when
327
there are no conflicts; should be set when doing something we expect
328
to complete perfectly.
330
All available ancestors of other_revision and base_revision are
237
all available ancestors of other_revision and base_revision are
331
238
automatically pulled into the branch.
240
from bzrlib.revision import common_ancestor, MultipleRevisionSources
241
from bzrlib.errors import NoSuchRevision
333
242
tempdir = tempfile.mkdtemp(prefix="bzr-")
335
244
if this_dir is None:
337
this_branch = Branch.open_containing(this_dir)
338
this_rev_id = this_branch.last_revision()
246
this_branch = find_branch(this_dir)
247
this_rev_id = this_branch.last_patch()
339
248
if this_rev_id is None:
340
249
raise BzrCommandError("This branch has no commits")
346
255
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
348
257
if other_revision[1] == -1:
349
other_rev_id = other_branch.last_revision()
350
if other_rev_id is None:
351
raise NoCommits(other_branch)
258
other_rev_id = other_branch.last_patch()
352
259
other_basis = other_rev_id
353
260
elif other_revision[1] is not None:
354
other_rev_id = other_branch.get_rev_id(other_revision[1])
261
other_rev_id = other_branch.lookup_revision(other_revision[1])
355
262
other_basis = other_rev_id
357
264
other_rev_id = None
358
other_basis = other_branch.last_revision()
359
if other_basis is None:
360
raise NoCommits(other_branch)
265
other_basis = other_branch.last_patch()
361
266
if base_revision == [None, None]:
363
base_rev_id = common_ancestor(this_rev_id, other_basis,
365
except NoCommonAncestor:
267
base_rev_id = common_ancestor(this_rev_id, other_basis,
269
if base_rev_id is None:
366
270
raise UnrelatedBranches()
367
271
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
371
275
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
372
276
if base_revision[1] == -1:
373
base_rev_id = base_branch.last_revision()
277
base_rev_id = base_branch.last_patch()
374
278
elif base_revision[1] is None:
375
279
base_rev_id = None
377
base_rev_id = base_branch.get_rev_id(base_revision[1])
378
fetch(from_branch=base_branch, to_branch=this_branch)
379
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
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
381
288
if file_list is None:
382
289
interesting_ids = None
412
319
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
415
332
def merge_inner(this_branch, other_tree, base_tree, tempdir,
416
333
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
417
334
interesting_ids=None):
428
345
return tree.tree.inventory
430
347
inv_changes = merge_flex(this_tree, base_tree, other_tree,
431
generate_changeset, get_inventory,
432
MergeConflictHandler(this_tree, base_tree,
433
other_tree, ignore_zero=ignore_zero),
348
generate_cset_optimized, get_inventory,
349
MergeConflictHandler(base_tree.root,
350
ignore_zero=ignore_zero),
434
351
merge_factory=merge_factory,
435
352
interesting_ids=interesting_ids)