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.merge_core import WeaveMerge
27
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
28
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
29
from bzrlib.branch import Branch
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
31
from bzrlib.errors import NoCommits, WorkingTreeNotRevision
32
from bzrlib.delta import compare_trees
33
from bzrlib.trace import mutter, warning, note
34
from bzrlib.fetch import greedy_fetch, fetch
35
from bzrlib.revision import is_ancestor
36
from bzrlib.osutils import rename
37
from bzrlib.revision import common_ancestor, MultipleRevisionSources
38
from bzrlib.errors import NoSuchRevision
40
# TODO: build_working_dir can be built on something simpler than merge()
42
# FIXME: merge() parameters seem oriented towards the command line
43
# NOTABUG: merge is a helper for commandline functions. merge_inner is the
44
# the core functionality.
46
# comments from abentley on irc: merge happens in two stages, each
47
# of which generates a changeset object
49
# stage 1: generate OLD->OTHER,
50
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
52
class MergeConflictHandler(ExceptionConflictHandler):
53
"""Handle conflicts encountered while merging.
55
This subclasses ExceptionConflictHandler, so that any types of
56
conflict that are not explicitly handled cause an exception and
59
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
60
ExceptionConflictHandler.__init__(self)
62
self.ignore_zero = ignore_zero
63
self.this_tree = this_tree
64
self.base_tree = base_tree
65
self.other_tree = other_tree
67
def copy(self, source, dest):
68
"""Copy the text and mode of a file
69
:param source: The path of the file to copy
70
:param dest: The distination file to create
72
s_file = file(source, "rb")
73
d_file = file(dest, "wb")
76
os.chmod(dest, 0777 & os.stat(source).st_mode)
78
def dump(self, lines, dest):
79
"""Copy the text and mode of a file
80
:param source: The path of the file to copy
81
:param dest: The distination file to create
83
d_file = file(dest, "wb")
87
def add_suffix(self, name, suffix, last_new_name=None):
88
"""Rename a file to append a suffix. If the new name exists, the
89
suffix is added repeatedly until a non-existant name is found
91
:param name: The path of the file
92
:param suffix: The suffix to append
93
:param last_new_name: (used for recursive calls) the last name tried
95
if last_new_name is None:
97
new_name = last_new_name+suffix
99
rename(name, new_name)
102
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
104
return self.add_suffix(name, suffix, last_new_name=new_name)
106
def conflict(self, text):
111
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
113
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
114
main file will be a version with diff3 conflicts.
115
:param new_file: Path to the output file with diff3 markers
116
:param this_path: Path to the file text for the THIS tree
117
:param base_path: Path to the file text for the BASE tree
118
:param other_path: Path to the file text for the OTHER tree
120
self.add_suffix(this_path, ".THIS")
121
self.dump(base_lines, this_path+".BASE")
122
self.dump(other_lines, this_path+".OTHER")
123
rename(new_file, this_path)
124
self.conflict("Diff3 conflict encountered in %s" % this_path)
126
def weave_merge_conflict(self, filename, weave, other_i, out_file):
128
Handle weave conflicts by producing a .THIS, and .OTHER. The
129
main file will be a version with diff3-style conflicts.
131
self.add_suffix(filename, ".THIS")
133
self.dump(weave.get_iter(other_i), filename+".OTHER")
134
self.conflict("Text conflict encountered in %s" % filename)
136
def new_contents_conflict(self, filename, other_contents):
137
"""Conflicting contents for newly added file."""
138
other_contents(filename + ".OTHER", self, False)
139
self.conflict("Conflict in newly added file %s" % filename)
142
def target_exists(self, entry, target, old_path):
143
"""Handle the case when the target file or dir exists"""
144
moved_path = self.add_suffix(target, ".moved")
145
self.conflict("Moved existing %s to %s" % (target, moved_path))
147
def rmdir_non_empty(self, filename):
148
"""Handle the case where the dir to be removed still has contents"""
149
self.conflict("Directory %s not removed because it is not empty"\
153
def rem_contents_conflict(self, filename, this_contents, base_contents):
154
base_contents(filename+".BASE", self, False)
155
this_contents(filename+".THIS", self, False)
156
return ReplaceContents(this_contents, None)
158
def rem_contents_conflict(self, filename, this_contents, base_contents):
159
base_contents(filename+".BASE", self, False)
160
this_contents(filename+".THIS", self, False)
161
self.conflict("Other branch deleted locally modified file %s" %
163
return ReplaceContents(this_contents, None)
165
def abs_this_path(self, file_id):
166
"""Return the absolute path for a file_id in the this tree."""
167
return self.this_tree.id2abspath(file_id)
169
def add_missing_parents(self, file_id, tree):
170
"""If some of the parents for file_id are missing, add them."""
171
entry = tree.inventory[file_id]
172
if entry.parent_id not in self.this_tree:
173
return self.create_all_missing(entry.parent_id, tree)
175
return self.abs_this_path(entry.parent_id)
177
def create_all_missing(self, file_id, tree):
178
"""Add contents for a file_id and all its parents to a tree."""
179
entry = tree.inventory[file_id]
180
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
181
abspath = self.create_all_missing(entry.parent_id, tree)
183
abspath = self.abs_this_path(entry.parent_id)
184
entry_path = os.path.join(abspath, entry.name)
185
if not os.path.isdir(entry_path):
186
self.create(file_id, entry_path, tree)
189
def create(self, file_id, path, tree, reverse=False):
190
"""Uses tree data to create a filesystem object for the file_id"""
191
from changeset import get_contents
192
get_contents(tree, file_id)(path, self, reverse)
194
def missing_for_merge(self, file_id, other_path):
195
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
196
self.conflict("Other branch modified locally deleted file %s" %
198
parent_dir = self.add_missing_parents(file_id, self.other_tree)
199
stem = os.path.join(parent_dir, os.path.basename(other_path))
200
self.create(file_id, stem+".OTHER", self.other_tree)
201
self.create(file_id, stem+".BASE", self.base_tree)
203
def threeway_contents_conflict(filename, this_contents, base_contents,
205
self.conflict("Three-way conflict merging %s" % filename)
208
if not self.ignore_zero:
209
note("%d conflicts encountered.\n" % self.conflicts)
211
def get_tree(treespec, local_branch=None):
212
location, revno = treespec
213
branch = Branch.open_containing(location)[0]
217
revision = branch.last_revision()
219
revision = branch.get_rev_id(revno)
220
return branch, get_revid_tree(branch, revision, local_branch)
222
def get_revid_tree(branch, revision, local_branch):
224
base_tree = branch.working_tree()
226
if local_branch is not None:
227
greedy_fetch(local_branch, branch, revision)
228
base_tree = local_branch.revision_tree(revision)
230
base_tree = branch.revision_tree(revision)
234
def file_exists(tree, file_id):
235
return tree.has_filename(tree.id2path(file_id))
238
def build_working_dir(to_dir):
239
"""Build a working directory in an empty directory.
241
to_dir is a directory containing branch metadata but no working files,
242
typically constructed by cloning an existing branch.
244
This is split out as a special idiomatic case of merge. It could
245
eventually be done by just building the tree directly calling into
246
lower-level code (e.g. constructing a changeset).
248
# RBC 20051019 is this not just 'export' ?
249
# Well, export doesn't take care of inventory...
250
this_branch = Branch.open_containing(to_dir)[0]
251
transform_tree(this_branch.working_tree(), this_branch.basis_tree())
253
def transform_tree(from_tree, to_tree):
254
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True)
256
def merge(other_revision, base_revision,
257
check_clean=True, ignore_zero=False,
258
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
259
file_list=None, show_base=False):
260
"""Merge changes into a tree.
263
tuple(path, revision) Base for three-way merge.
265
tuple(path, revision) Other revision for three-way merge.
267
Directory to merge changes into; '.' by default.
269
If true, this_dir must have no uncommitted changes before the
271
ignore_zero - If true, suppress the "zero conflicts" message when
272
there are no conflicts; should be set when doing something we expect
273
to complete perfectly.
275
All available ancestors of other_revision and base_revision are
276
automatically pulled into the branch.
280
this_branch = Branch.open_containing(this_dir)[0]
281
if show_base and not merge_type is ApplyMerge3:
282
raise BzrCommandError("Show-base is not supported for this merge"
283
" type. %s" % merge_type)
284
merger = Merger(this_branch)
285
merger.check_basis(check_clean)
286
merger.set_other(other_revision)
287
merger.set_base(base_revision)
288
merger.backup_files = backup_files
289
merger.merge_type = merge_type
290
merger.set_interesting_files(file_list)
291
merger.show_base = show_base
292
merger.conflict_handler = MergeConflictHandler(merger.this_tree,
295
ignore_zero=ignore_zero)
296
conflicts = merger.do_merge()
300
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
301
backup_files=False, merge_type=ApplyMerge3,
302
interesting_ids=None, show_base=False):
303
"""Primary interface for merging.
305
typical use is probably
306
'merge_inner(branch, branch.get_revision_tree(other_revision),
307
branch.get_revision_tree(base_revision))'
309
merger = Merger(this_branch, other_tree, base_tree)
310
merger.backup_files = False
311
merger.merge_type = ApplyMerge3
312
merger.interesting_ids = interesting_ids
313
merger.show_base = show_base
314
merger.conflict_handler = MergeConflictHandler(merger.this_tree, base_tree,
316
ignore_zero=ignore_zero)
317
return merger.do_merge()
320
class Merger(object):
321
def __init__(self, this_branch, other_tree=None, base_tree=None):
322
object.__init__(self)
323
self.this_branch = this_branch
324
self.this_basis = this_branch.last_revision()
325
self.this_rev_id = None
326
self.this_tree = this_branch.working_tree()
327
self.this_revision_tree = None
328
self.other_tree = other_tree
329
self.base_tree = base_tree
330
self.ignore_zero = False
331
self.backup_files = False
332
self.interesting_ids = None
333
self.show_base = False
334
self.conflict_handler = MergeConflictHandler(self.this_tree, base_tree,
337
def revision_tree(self, revision_id):
338
return self.this_branch.revision_tree(revision_id)
340
def ensure_revision_trees(self):
341
if self.this_revision_tree is None:
342
if self.this_rev_id is None:
344
if self.this_rev_id is None:
345
raise WorkingTreeNotRevision(self.this_tree)
346
self.this_revision_tree = self.this_branch.revision_tree(
349
if self.other_rev_id is None:
350
other_basis_tree = self.revision_tree(self.other_basis)
351
changes = compare_trees(self.other_tree, other_basis_tree)
352
if changes.has_changed():
353
raise WorkingTreeNotRevision(self.this_tree)
354
other_rev_id = other_basis
355
self.other_tree = other_basis_tree
358
def file_revisions(self, file_id):
359
self.ensure_revision_trees()
360
def get_id(tree, file_id):
361
revision_id = tree.inventory[file_id].revision
362
assert revision_id is not None
364
trees = (self.this_revision_tree, self.other_tree)
365
return [get_id(tree, file_id) for tree in trees]
368
def merge_factory(self, file_id, base, other):
369
if self.merge_type.history_based:
370
t_revid, o_revid = self.file_revisions(file_id)
371
weave = self.this_revision_tree.get_weave(file_id)
372
contents_change = self.merge_type(weave, t_revid, o_revid)
374
if self.show_base is True:
375
contents_change = self.merge_type(file_id, base, other,
378
contents_change = self.merge_type(file_id, base, other)
379
if self.backup_files:
380
contents_change = BackupBeforeChange(contents_change)
381
return contents_change
383
def check_basis(self, check_clean):
384
if self.this_basis is None:
385
raise BzrCommandError("This branch has no commits")
388
if self.this_basis != self.this_rev_id:
389
raise BzrCommandError("Working tree has uncommitted changes.")
391
def compare_basis(self):
392
changes = compare_trees(self.this_branch.working_tree(),
393
self.this_branch.basis_tree(), False)
394
if not changes.has_changed():
395
self.this_rev_id = self.this_basis
397
def set_interesting_files(self, file_list):
398
if file_list is None:
399
self.interesting_ids = None
402
interesting_ids = set()
403
for fname in file_list:
404
path = self.this_tree.relpath(fname)
406
for tree in (self.this_tree, self.base_tree, self.other_tree):
407
file_id = tree.inventory.path2id(path)
408
if file_id is not None:
409
interesting_ids.add(file_id)
412
raise BzrCommandError("%s is not a source file in any"
414
self.interesting_ids = interesting_ids
416
def set_pending(self):
417
if not self.base_is_ancestor:
419
if self.other_rev_id is None:
421
if self.other_rev_id in self.this_branch.get_ancestry(self.this_basis):
423
self.this_branch.add_pending_merge(self.other_rev_id)
425
def set_other(self, other_revision):
426
other_branch, self.other_tree = get_tree(other_revision,
428
if other_revision[1] == -1:
429
self.other_rev_id = other_branch.last_revision()
430
if self.other_rev_id is None:
431
raise NoCommits(other_branch)
432
self.other_basis = self.other_rev_id
433
elif other_revision[1] is not None:
434
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
435
self.other_basis = self.other_rev_id
437
self.other_rev_id = None
438
self.other_basis = other_branch.last_revision()
439
if self.other_basis is None:
440
raise NoCommits(other_branch)
442
def set_base(self, base_revision):
443
if base_revision == [None, None]:
445
self.base_rev_id = common_ancestor(self.this_basis,
448
except NoCommonAncestor:
449
raise UnrelatedBranches()
450
self.base_tree = get_revid_tree(self.this_branch, self.base_rev_id,
452
self.base_is_ancestor = True
454
base_branch, self.base_tree = get_tree(base_revision)
455
if base_revision[1] == -1:
456
self.base_rev_id = base_branch.last_revision()
457
elif base_revision[1] is None:
458
self.base_rev_id = None
460
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
461
fetch(from_branch=base_branch, to_branch=self.this_branch)
462
self.base_is_ancestor = is_ancestor(self.this_basis,
467
def get_inventory(tree):
468
return tree.inventory
470
inv_changes = merge_flex(self.this_tree, self.base_tree,
472
generate_changeset, get_inventory,
473
self.conflict_handler,
474
merge_factory=self.merge_factory,
475
interesting_ids=self.interesting_ids)
478
for id, path in inv_changes.iteritems():
483
assert path.startswith('.' + os.sep), "path is %s" % path
485
adjust_ids.append((path, id))
486
if len(adjust_ids) > 0:
487
self.this_branch.set_inventory(self.regen_inventory(adjust_ids))
488
conflicts = self.conflict_handler.conflicts
489
self.conflict_handler.finalize()
492
def regen_inventory(self, new_entries):
493
old_entries = self.this_branch.read_working_inventory()
497
for path, file_id in new_entries:
500
new_entries_map[file_id] = path
502
def id2path(file_id):
503
path = new_entries_map.get(file_id)
506
entry = old_entries[file_id]
507
if entry.parent_id is None:
509
return os.path.join(id2path(entry.parent_id), entry.name)
511
for file_id in old_entries:
512
entry = old_entries[file_id]
513
path = id2path(file_id)
514
new_inventory[file_id] = (path, file_id, entry.parent_id,
516
by_path[path] = file_id
521
for path, file_id in new_entries:
523
del new_inventory[file_id]
526
new_path_list.append((path, file_id))
527
if file_id not in old_entries:
529
# Ensure no file is added before its parent
531
for path, file_id in new_path_list:
535
parent = by_path[os.path.dirname(path)]
536
abspath = os.path.join(self.this_tree.basedir, path)
537
kind = bzrlib.osutils.file_kind(abspath)
538
new_inventory[file_id] = (path, file_id, parent, kind)
539
by_path[path] = file_id
541
# Get a list in insertion order
542
new_inventory_list = new_inventory.values()
543
mutter ("""Inventory regeneration:
544
old length: %i insertions: %i deletions: %i new_length: %i"""\
545
% (len(old_entries), insertions, deletions,
546
len(new_inventory_list)))
547
assert len(new_inventory_list) == len(old_entries) + insertions\
549
new_inventory_list.sort()
550
return new_inventory_list
552
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
553
"diff3": (Diff3Merge, "Merge using external diff3"),
554
'weave': (WeaveMerge, "Weave-based merge")