1
from merge_core import merge_flex
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory
4
from bzrlib import Branch
6
from trace import mutter
1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24
import bzrlib.revision
25
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
26
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
31
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning
33
from bzrlib.fetch import greedy_fetch, fetch
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
43
# comments from abentley on irc: merge happens in two stages, each
44
# of which generates a changeset object
46
# stage 1: generate OLD->OTHER,
47
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
12
49
class MergeConflictHandler(ExceptionConflictHandler):
13
"""Handle conflicts encountered while merging"""
50
"""Handle conflicts encountered while merging.
52
This subclasses ExceptionConflictHandler, so that any types of
53
conflict that are not explicitly handled cause an exception and
56
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
57
ExceptionConflictHandler.__init__(self)
59
self.ignore_zero = ignore_zero
60
self.this_tree = this_tree
61
self.base_tree = base_tree
62
self.other_tree = other_tree
14
64
def copy(self, source, dest):
15
65
"""Copy the text and mode of a file
16
66
:param source: The path of the file to copy
50
115
:param other_path: Path to the file text for the OTHER tree
52
117
self.add_suffix(this_path, ".THIS")
53
self.copy(base_path, this_path+".BASE")
54
self.copy(other_path, this_path+".OTHER")
55
os.rename(new_file, this_path)
118
self.dump(base_lines, this_path+".BASE")
119
self.dump(other_lines, this_path+".OTHER")
120
rename(new_file, this_path)
121
self.conflict("Diff3 conflict encountered in %s" % this_path)
123
def new_contents_conflict(self, filename, other_contents):
124
"""Conflicting contents for newly added file."""
125
self.copy(other_contents, filename + ".OTHER")
126
self.conflict("Conflict in newly added file %s" % filename)
57
129
def target_exists(self, entry, target, old_path):
58
130
"""Handle the case when the target file or dir exists"""
59
self.add_suffix(target, ".moved")
131
moved_path = self.add_suffix(target, ".moved")
132
self.conflict("Moved existing %s to %s" % (target, moved_path))
134
def rmdir_non_empty(self, filename):
135
"""Handle the case where the dir to be removed still has contents"""
136
self.conflict("Directory %s not removed because it is not empty"\
140
def rem_contents_conflict(self, filename, this_contents, base_contents):
141
base_contents(filename+".BASE", self, False)
142
this_contents(filename+".THIS", self, False)
143
self.conflict("Other branch deleted locally modified file %s" %
145
return ReplaceContents(this_contents, None)
147
def abs_this_path(self, file_id):
148
"""Return the absolute path for a file_id in the this tree."""
149
relpath = self.this_tree.id2path(file_id)
150
return self.this_tree.tree.abspath(relpath)
152
def add_missing_parents(self, file_id, tree):
153
"""If some of the parents for file_id are missing, add them."""
154
entry = tree.tree.inventory[file_id]
155
if entry.parent_id not in self.this_tree:
156
return self.create_all_missing(entry.parent_id, tree)
158
return self.abs_this_path(entry.parent_id)
160
def create_all_missing(self, file_id, tree):
161
"""Add contents for a file_id and all its parents to a tree."""
162
entry = tree.tree.inventory[file_id]
163
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
164
abspath = self.create_all_missing(entry.parent_id, tree)
166
abspath = self.abs_this_path(entry.parent_id)
167
entry_path = os.path.join(abspath, entry.name)
168
if not os.path.isdir(entry_path):
169
self.create(file_id, entry_path, tree)
172
def create(self, file_id, path, tree, reverse=False):
173
"""Uses tree data to create a filesystem object for the file_id"""
174
from merge_core import get_id_contents
175
get_id_contents(file_id, tree)(path, self, reverse)
177
def missing_for_merge(self, file_id, other_path):
178
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
179
self.conflict("Other branch modified locally deleted file %s" %
181
parent_dir = self.add_missing_parents(file_id, self.other_tree)
182
stem = os.path.join(parent_dir, os.path.basename(other_path))
183
self.create(file_id, stem+".OTHER", self.other_tree)
184
self.create(file_id, stem+".BASE", self.base_tree)
187
if not self.ignore_zero:
188
print "%d conflicts encountered.\n" % self.conflicts
62
def __init__(self, path, id, present=None, isdir=None):
65
self.present = present
67
self.interesting = True
70
return "SourceFile(%s, %s)" % (self.path, self.id)
72
def get_tree(treespec, temp_root, label):
190
def get_tree(treespec, temp_root, label, local_branch=None):
191
location, revno = treespec
192
branch = Branch.open_containing(location)
196
revision = branch.last_revision()
198
revision = branch.get_rev_id(revno)
199
return branch, get_revid_tree(branch, revision, temp_root, label,
202
def get_revid_tree(branch, revision, temp_root, label, local_branch):
76
204
base_tree = branch.working_tree()
78
base_tree = branch.basis_tree()
80
base_tree = branch.revision_tree(branch.lookup_revision(revno))
206
if local_branch is not None:
207
greedy_fetch(local_branch, branch, revision)
208
base_tree = local_branch.revision_tree(revision)
210
base_tree = branch.revision_tree(revision)
81
211
temp_path = os.path.join(temp_root, label)
82
212
os.mkdir(temp_path)
83
213
return MergeTree(base_tree, temp_path)
86
def abspath(tree, file_id):
87
path = tree.inventory.id2path(file_id)
92
216
def file_exists(tree, file_id):
93
217
return tree.has_filename(tree.id2path(file_id))
95
def inventory_map(tree):
97
for file_id in tree.inventory:
98
if not file_exists(tree, file_id):
100
path = abspath(tree, file_id)
101
inventory[path] = SourceFile(path, file_id)
105
220
class MergeTree(object):
106
221
def __init__(self, tree, tempdir):
109
224
self.root = tree.basedir
112
self.inventory = inventory_map(tree)
114
228
self.tempdir = tempdir
115
229
os.mkdir(os.path.join(self.tempdir, "texts"))
230
os.mkdir(os.path.join(self.tempdir, "symlinks"))
234
return self.tree.__iter__()
236
def __contains__(self, file_id):
237
return file_id in self.tree
239
def get_file(self, file_id):
240
return self.tree.get_file(file_id)
242
def get_file_sha1(self, id):
243
return self.tree.get_file_sha1(id)
245
def is_executable(self, id):
246
return self.tree.is_executable(id)
248
def id2path(self, file_id):
249
return self.tree.id2path(file_id)
251
def has_id(self, file_id):
252
return self.tree.has_id(file_id)
254
def has_or_had_id(self, file_id):
255
if file_id == self.tree.inventory.root.file_id:
257
return self.tree.inventory.has_id(file_id)
259
def has_or_had_id(self, file_id):
260
if file_id == self.tree.inventory.root.file_id:
262
return self.tree.inventory.has_id(file_id)
118
264
def readonly_path(self, id):
265
if id not in self.tree:
119
267
if self.root is not None:
120
268
return self.tree.abspath(self.tree.id2path(id))
122
if self.tree.inventory[id].kind in ("directory", "root_directory"):
270
kind = self.tree.inventory[id].kind
271
if kind in ("directory", "root_directory"):
123
272
return self.tempdir
124
273
if not self.cached.has_key(id):
125
path = os.path.join(self.tempdir, "texts", id)
126
outfile = file(path, "wb")
127
outfile.write(self.tree.get_file(id).read())
128
assert(os.path.exists(path))
275
path = os.path.join(self.tempdir, "texts", id)
276
outfile = file(path, "wb")
277
outfile.write(self.tree.get_file(id).read())
278
assert(bzrlib.osutils.lexists(path))
279
if self.tree.is_executable(id):
282
assert kind == "symlink"
283
path = os.path.join(self.tempdir, "symlinks", id)
284
target = self.tree.get_symlink_target(id)
285
os.symlink(target, path)
129
286
self.cached[id] = path
130
287
return self.cached[id]
132
def merge(other_revision, base_revision):
290
def build_working_dir(to_dir):
291
"""Build a working directory in an empty directory.
293
to_dir is a directory containing branch metadata but no working files,
294
typically constructed by cloning an existing branch.
296
This is split out as a special idiomatic case of merge. It could
297
eventually be done by just building the tree directly calling into
298
lower-level code (e.g. constructing a changeset).
300
merge((to_dir, -1), (to_dir, 0), this_dir=to_dir,
301
check_clean=False, ignore_zero=True)
304
def merge(other_revision, base_revision,
305
check_clean=True, ignore_zero=False,
306
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
308
"""Merge changes into a tree.
311
tuple(path, revision) Base for three-way merge.
313
tuple(path, revision) Other revision for three-way merge.
315
Directory to merge changes into; '.' by default.
317
If true, this_dir must have no uncommitted changes before the
319
ignore_zero - If true, suppress the "zero conflicts" message when
320
there are no conflicts; should be set when doing something we expect
321
to complete perfectly.
323
All available ancestors of other_revision and base_revision are
324
automatically pulled into the branch.
133
326
tempdir = tempfile.mkdtemp(prefix="bzr-")
135
this_branch = Branch('.')
136
other_tree = get_tree(other_revision, tempdir, "other")
137
base_tree = get_tree(base_revision, tempdir, "base")
138
merge_inner(this_branch, other_tree, base_tree, tempdir)
330
this_branch = Branch.open_containing(this_dir)
331
this_rev_id = this_branch.last_revision()
332
if this_rev_id is None:
333
raise BzrCommandError("This branch has no commits")
335
changes = compare_trees(this_branch.working_tree(),
336
this_branch.basis_tree(), False)
337
if changes.has_changed():
338
raise BzrCommandError("Working tree has uncommitted changes.")
339
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
341
if other_revision[1] == -1:
342
other_rev_id = other_branch.last_revision()
343
if other_rev_id is None:
344
raise NoCommits(other_branch)
345
other_basis = other_rev_id
346
elif other_revision[1] is not None:
347
other_rev_id = other_branch.get_rev_id(other_revision[1])
348
other_basis = other_rev_id
351
other_basis = other_branch.last_revision()
352
if other_basis is None:
353
raise NoCommits(other_branch)
354
if base_revision == [None, None]:
356
base_rev_id = common_ancestor(this_rev_id, other_basis,
358
except NoCommonAncestor:
359
raise UnrelatedBranches()
360
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
362
base_is_ancestor = True
364
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
365
if base_revision[1] == -1:
366
base_rev_id = base_branch.last_revision()
367
elif base_revision[1] is None:
370
base_rev_id = base_branch.get_rev_id(base_revision[1])
371
fetch(from_branch=base_branch, to_branch=this_branch)
372
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
374
if file_list is None:
375
interesting_ids = None
377
interesting_ids = set()
378
this_tree = this_branch.working_tree()
379
for fname in file_list:
380
path = this_branch.relpath(fname)
382
for tree in (this_tree, base_tree.tree, other_tree.tree):
383
file_id = tree.inventory.path2id(path)
384
if file_id is not None:
385
interesting_ids.add(file_id)
388
raise BzrCommandError("%s is not a source file in any"
390
merge_inner(this_branch, other_tree, base_tree, tempdir,
391
ignore_zero=ignore_zero, backup_files=backup_files,
392
merge_type=merge_type, interesting_ids=interesting_ids)
393
if base_is_ancestor and other_rev_id is not None\
394
and other_rev_id not in this_branch.revision_history():
395
this_branch.add_pending_merge(other_rev_id)
140
397
shutil.rmtree(tempdir)
143
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
144
"""Generate a changeset, using the text_id to mark really-changed files.
145
This permits blazing comparisons when text_ids are present. It also
146
disables metadata comparison for files with identical texts.
400
def set_interesting(inventory_a, inventory_b, interesting_ids):
401
"""Mark files whose ids are in interesting_ids as interesting
403
for inventory in (inventory_a, inventory_b):
404
for path, source_file in inventory.iteritems():
405
source_file.interesting = source_file.id in interesting_ids
408
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
409
"""Generate a changeset. If interesting_ids is supplied, only changes
410
to those files will be shown. Metadata changes are stripped.
148
for file_id in tree_a.tree.inventory:
149
if file_id not in tree_b.tree.inventory:
151
entry_a = tree_a.tree.inventory[file_id]
152
entry_b = tree_b.tree.inventory[file_id]
153
if (entry_a.kind, entry_b.kind) != ("file", "file"):
155
if None in (entry_a.text_id, entry_b.text_id):
157
if entry_a.text_id != entry_b.text_id:
159
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
160
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
161
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
412
cset = generate_changeset(tree_a, tree_b, interesting_ids)
162
413
for entry in cset.entries.itervalues():
163
414
entry.metadata_change = None
167
def merge_inner(this_branch, other_tree, base_tree, tempdir):
168
this_tree = get_tree(('.', None), tempdir, "this")
418
def merge_inner(this_branch, other_tree, base_tree, tempdir,
419
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
420
interesting_ids=None):
422
def merge_factory(file_id, base, other):
423
contents_change = merge_type(file_id, base, other)
425
contents_change = BackupBeforeChange(contents_change)
426
return contents_change
428
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
170
430
def get_inventory(tree):
171
return tree.inventory
431
return tree.tree.inventory
173
433
inv_changes = merge_flex(this_tree, base_tree, other_tree,
174
434
generate_cset_optimized, get_inventory,
175
MergeConflictHandler(base_tree.root))
435
MergeConflictHandler(this_tree, base_tree,
436
other_tree, ignore_zero=ignore_zero),
437
merge_factory=merge_factory,
438
interesting_ids=interesting_ids)
178
441
for id, path in inv_changes.iteritems():