20
from shutil import rmtree
21
from tempfile import mkdtemp
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
24
from bzrlib.branch import Branch
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
30
from bzrlib.errors import NoCommits
31
25
from bzrlib.delta import compare_trees
26
from bzrlib.errors import (BzrCommandError,
35
WorkingTreeNotRevision,
37
from bzrlib.merge3 import Merge3
39
from bzrlib.osutils import rename, pathjoin
40
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
32
41
from bzrlib.trace import mutter, warning, note
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
42
# NOTABUG: merge is a helper for commandline functions. merge_inner is the
43
# the core functionality.
45
# comments from abentley on irc: merge happens in two stages, each
46
# of which generates a changeset object
48
# stage 1: generate OLD->OTHER,
49
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
51
class MergeConflictHandler(ExceptionConflictHandler):
52
"""Handle conflicts encountered while merging.
54
This subclasses ExceptionConflictHandler, so that any types of
55
conflict that are not explicitly handled cause an exception and
58
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
59
ExceptionConflictHandler.__init__(self)
61
self.ignore_zero = ignore_zero
62
self.this_tree = this_tree
63
self.base_tree = base_tree
64
self.other_tree = other_tree
66
def copy(self, source, dest):
67
"""Copy the text and mode of a file
68
:param source: The path of the file to copy
69
:param dest: The distination file to create
71
s_file = file(source, "rb")
72
d_file = file(dest, "wb")
75
os.chmod(dest, 0777 & os.stat(source).st_mode)
77
def dump(self, lines, dest):
78
"""Copy the text and mode of a file
79
:param source: The path of the file to copy
80
:param dest: The distination file to create
82
d_file = file(dest, "wb")
86
def add_suffix(self, name, suffix, last_new_name=None):
87
"""Rename a file to append a suffix. If the new name exists, the
88
suffix is added repeatedly until a non-existant name is found
90
:param name: The path of the file
91
:param suffix: The suffix to append
92
:param last_new_name: (used for recursive calls) the last name tried
94
if last_new_name is None:
96
new_name = last_new_name+suffix
98
rename(name, new_name)
101
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
103
return self.add_suffix(name, suffix, last_new_name=new_name)
105
def conflict(self, text):
110
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
112
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
113
main file will be a version with diff3 conflicts.
114
:param new_file: Path to the output file with diff3 markers
115
:param this_path: Path to the file text for the THIS tree
116
:param base_path: Path to the file text for the BASE tree
117
:param other_path: Path to the file text for the OTHER tree
119
self.add_suffix(this_path, ".THIS")
120
self.dump(base_lines, this_path+".BASE")
121
self.dump(other_lines, this_path+".OTHER")
122
rename(new_file, this_path)
123
self.conflict("Diff3 conflict encountered in %s" % this_path)
125
def new_contents_conflict(self, filename, other_contents):
126
"""Conflicting contents for newly added file."""
127
self.copy(other_contents, filename + ".OTHER")
128
self.conflict("Conflict in newly added file %s" % filename)
131
def target_exists(self, entry, target, old_path):
132
"""Handle the case when the target file or dir exists"""
133
moved_path = self.add_suffix(target, ".moved")
134
self.conflict("Moved existing %s to %s" % (target, moved_path))
136
def rmdir_non_empty(self, filename):
137
"""Handle the case where the dir to be removed still has contents"""
138
self.conflict("Directory %s not removed because it is not empty"\
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)
194
if not self.ignore_zero:
195
note("%d conflicts encountered.\n" % self.conflicts)
197
def get_tree(treespec, temp_root, label, local_branch=None):
42
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
43
conflicts_strings, FinalPaths, create_by_entry,
46
# TODO: Report back as changes are merged in
48
def _get_tree(treespec, local_branch=None):
198
49
location, revno = treespec
199
branch = Branch.open_containing(location)
50
branch = Branch.open_containing(location)[0]
203
54
revision = branch.last_revision()
205
56
revision = branch.get_rev_id(revno)
206
return branch, get_revid_tree(branch, revision, temp_root, label,
209
def get_revid_tree(branch, revision, temp_root, label, local_branch):
58
revision = NULL_REVISION
59
return branch, _get_revid_tree(branch, revision, local_branch)
62
def _get_revid_tree(branch, revision, local_branch):
210
63
if revision is None:
211
base_tree = branch.working_tree()
64
base_tree = branch.bzrdir.open_workingtree()
213
66
if local_branch is not None:
214
greedy_fetch(local_branch, branch, revision)
215
base_tree = local_branch.revision_tree(revision)
67
if local_branch.base != branch.base:
68
local_branch.fetch(branch, revision)
69
base_tree = local_branch.repository.revision_tree(revision)
217
base_tree = branch.revision_tree(revision)
218
temp_path = os.path.join(temp_root, label)
220
return MergeTree(base_tree, temp_path)
223
def file_exists(tree, file_id):
224
return tree.has_filename(tree.id2path(file_id))
227
class MergeTree(object):
228
def __init__(self, tree, tempdir):
71
base_tree = branch.repository.revision_tree(revision)
75
def transform_tree(from_tree, to_tree, interesting_ids=None):
76
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
77
interesting_ids=interesting_ids, this_tree=from_tree)
81
def __init__(self, this_branch, other_tree=None, base_tree=None, this_tree=None):
229
82
object.__init__(self)
230
if hasattr(tree, "basedir"):
231
self.root = tree.basedir
235
self.tempdir = tempdir
236
os.mkdir(os.path.join(self.tempdir, "texts"))
237
os.mkdir(os.path.join(self.tempdir, "symlinks"))
241
return self.tree.__iter__()
243
def __contains__(self, file_id):
244
return file_id in self.tree
246
def get_file(self, file_id):
247
return self.tree.get_file(file_id)
249
def get_file_sha1(self, id):
250
return self.tree.get_file_sha1(id)
252
def is_executable(self, id):
253
return self.tree.is_executable(id)
255
def id2path(self, file_id):
256
return self.tree.id2path(file_id)
258
def has_id(self, file_id):
259
return self.tree.has_id(file_id)
261
def has_or_had_id(self, file_id):
262
if file_id == self.tree.inventory.root.file_id:
264
return self.tree.inventory.has_id(file_id)
266
def has_or_had_id(self, file_id):
267
if file_id == self.tree.inventory.root.file_id:
269
return self.tree.inventory.has_id(file_id)
271
def readonly_path(self, id):
272
if id not in self.tree:
274
if self.root is not None:
275
return self.tree.abspath(self.tree.id2path(id))
277
kind = self.tree.inventory[id].kind
278
if kind in ("directory", "root_directory"):
280
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)
293
self.cached[id] = path
294
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
def merge(other_revision, base_revision,
312
check_clean=True, ignore_zero=False,
313
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
315
"""Merge changes into a tree.
318
tuple(path, revision) Base for three-way merge.
320
tuple(path, revision) Other revision for three-way merge.
322
Directory to merge changes into; '.' by default.
324
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
331
automatically pulled into the branch.
333
tempdir = tempfile.mkdtemp(prefix="bzr-")
337
this_branch = Branch.open_containing(this_dir)
338
this_rev_id = this_branch.last_revision()
339
if this_rev_id is None:
83
assert this_tree is not None, "this_tree is required"
84
self.this_branch = this_branch
85
self.this_basis = this_branch.last_revision()
86
self.this_rev_id = None
87
self.this_tree = this_tree
88
self.this_revision_tree = None
89
self.this_basis_tree = None
90
self.other_tree = other_tree
91
self.base_tree = base_tree
92
self.ignore_zero = False
93
self.backup_files = False
94
self.interesting_ids = None
95
self.show_base = False
96
self.reprocess = False
98
def revision_tree(self, revision_id):
99
return self.this_branch.repository.revision_tree(revision_id)
101
def ensure_revision_trees(self):
102
if self.this_revision_tree is None:
103
self.this_basis_tree = self.this_branch.repository.revision_tree(
105
if self.this_basis == self.this_rev_id:
106
self.this_revision_tree = self.this_basis_tree
108
if self.other_rev_id is None:
109
other_basis_tree = self.revision_tree(self.other_basis)
110
changes = compare_trees(self.other_tree, other_basis_tree)
111
if changes.has_changed():
112
raise WorkingTreeNotRevision(self.this_tree)
113
other_rev_id = other_basis
114
self.other_tree = other_basis_tree
116
def file_revisions(self, file_id):
117
self.ensure_revision_trees()
118
def get_id(tree, file_id):
119
revision_id = tree.inventory[file_id].revision
120
assert revision_id is not None
122
if self.this_rev_id is None:
123
if self.this_basis_tree.get_file_sha1(file_id) != \
124
self.this_tree.get_file_sha1(file_id):
125
raise WorkingTreeNotRevision(self.this_tree)
127
trees = (self.this_basis_tree, self.other_tree)
128
return [get_id(tree, file_id) for tree in trees]
130
def check_basis(self, check_clean):
131
if self.this_basis is None:
340
132
raise BzrCommandError("This branch has no commits")
342
changes = compare_trees(this_branch.working_tree(),
343
this_branch.basis_tree(), False)
344
if changes.has_changed():
135
if self.this_basis != self.this_rev_id:
345
136
raise BzrCommandError("Working tree has uncommitted changes.")
346
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
138
def compare_basis(self):
139
changes = compare_trees(self.this_tree,
140
self.this_tree.basis_tree(), False)
141
if not changes.has_changed():
142
self.this_rev_id = self.this_basis
144
def set_interesting_files(self, file_list):
146
self._set_interesting_files(file_list)
147
except NotVersionedError, e:
148
raise BzrCommandError("%s is not a source file in any"
151
def _set_interesting_files(self, file_list):
152
"""Set the list of interesting ids from a list of files."""
153
if file_list is None:
154
self.interesting_ids = None
157
interesting_ids = set()
158
for path in file_list:
160
for tree in (self.this_tree, self.base_tree, self.other_tree):
161
file_id = tree.inventory.path2id(path)
162
if file_id is not None:
163
interesting_ids.add(file_id)
166
raise NotVersionedError(path=path)
167
self.interesting_ids = interesting_ids
169
def set_pending(self):
170
if not self.base_is_ancestor:
172
if self.other_rev_id is None:
174
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
175
if self.other_rev_id in ancestry:
177
self.this_tree.add_pending_merge(self.other_rev_id)
179
def set_other(self, other_revision):
180
other_branch, self.other_tree = _get_tree(other_revision,
348
182
if other_revision[1] == -1:
349
other_rev_id = other_branch.last_revision()
350
if other_rev_id is None:
183
self.other_rev_id = other_branch.last_revision()
184
if self.other_rev_id is None:
351
185
raise NoCommits(other_branch)
352
other_basis = other_rev_id
186
self.other_basis = self.other_rev_id
353
187
elif other_revision[1] is not None:
354
other_rev_id = other_branch.get_rev_id(other_revision[1])
355
other_basis = other_rev_id
188
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
189
self.other_basis = self.other_rev_id
358
other_basis = other_branch.last_revision()
359
if other_basis is None:
191
self.other_rev_id = None
192
self.other_basis = other_branch.last_revision()
193
if self.other_basis is None:
360
194
raise NoCommits(other_branch)
195
if other_branch.base != self.this_branch.base:
196
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
198
def set_base(self, base_revision):
199
mutter("doing merge() with no base_revision specified")
361
200
if base_revision == [None, None]:
363
base_rev_id = common_ancestor(this_rev_id, other_basis,
202
self.base_rev_id = common_ancestor(self.this_basis,
204
self.this_branch.repository)
365
205
except NoCommonAncestor:
366
206
raise UnrelatedBranches()
367
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
369
base_is_ancestor = True
207
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
209
self.base_is_ancestor = True
371
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
211
base_branch, self.base_tree = _get_tree(base_revision)
372
212
if base_revision[1] == -1:
373
base_rev_id = base_branch.last_revision()
213
self.base_rev_id = base_branch.last_revision()
374
214
elif base_revision[1] is 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,
381
if file_list is None:
382
interesting_ids = None
384
interesting_ids = set()
385
this_tree = this_branch.working_tree()
386
for fname in file_list:
387
path = this_branch.relpath(fname)
389
for tree in (this_tree, base_tree.tree, other_tree.tree):
390
file_id = tree.inventory.path2id(path)
215
self.base_rev_id = None
217
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
218
self.this_branch.fetch(base_branch)
219
self.base_is_ancestor = is_ancestor(self.this_basis,
224
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
225
'other_tree': self.other_tree}
226
if self.merge_type.requires_base:
227
kwargs['base_tree'] = self.base_tree
228
if self.merge_type.supports_reprocess:
229
kwargs['reprocess'] = self.reprocess
231
raise BzrError("Reprocess is not supported for this merge"
232
" type. %s" % merge_type)
233
if self.merge_type.supports_show_base:
234
kwargs['show_base'] = self.show_base
236
raise BzrError("Showing base is not supported for this"
237
" merge type. %s" % self.merge_type)
238
merge = self.merge_type(**kwargs)
239
if len(merge.cooked_conflicts) == 0:
240
if not self.ignore_zero:
241
note("All changes applied successfully.")
243
note("%d conflicts encountered." % len(merge.cooked_conflicts))
245
return len(merge.cooked_conflicts)
247
def regen_inventory(self, new_entries):
248
old_entries = self.this_tree.read_working_inventory()
252
for path, file_id in new_entries:
255
new_entries_map[file_id] = path
257
def id2path(file_id):
258
path = new_entries_map.get(file_id)
261
entry = old_entries[file_id]
262
if entry.parent_id is None:
264
return pathjoin(id2path(entry.parent_id), entry.name)
266
for file_id in old_entries:
267
entry = old_entries[file_id]
268
path = id2path(file_id)
269
new_inventory[file_id] = (path, file_id, entry.parent_id,
271
by_path[path] = file_id
276
for path, file_id in new_entries:
278
del new_inventory[file_id]
281
new_path_list.append((path, file_id))
282
if file_id not in old_entries:
284
# Ensure no file is added before its parent
286
for path, file_id in new_path_list:
290
parent = by_path[os.path.dirname(path)]
291
abspath = pathjoin(self.this_tree.basedir, path)
292
kind = bzrlib.osutils.file_kind(abspath)
293
new_inventory[file_id] = (path, file_id, parent, kind)
294
by_path[path] = file_id
296
# Get a list in insertion order
297
new_inventory_list = new_inventory.values()
298
mutter ("""Inventory regeneration:
299
old length: %i insertions: %i deletions: %i new_length: %i"""\
300
% (len(old_entries), insertions, deletions,
301
len(new_inventory_list)))
302
assert len(new_inventory_list) == len(old_entries) + insertions\
304
new_inventory_list.sort()
305
return new_inventory_list
308
class Merge3Merger(object):
309
"""Three-way merger that uses the merge3 text merger"""
311
supports_reprocess = True
312
supports_show_base = True
313
history_based = False
315
def __init__(self, working_tree, this_tree, base_tree, other_tree,
316
reprocess=False, show_base=False):
317
"""Initialize the merger object and perform the merge."""
318
object.__init__(self)
319
self.this_tree = working_tree
320
self.base_tree = base_tree
321
self.other_tree = other_tree
322
self._raw_conflicts = []
323
self.cooked_conflicts = []
324
self.reprocess = reprocess
325
self.show_base = show_base
327
all_ids = set(base_tree)
328
all_ids.update(other_tree)
329
self.tt = TreeTransform(working_tree)
331
for file_id in all_ids:
332
self.merge_names(file_id)
333
file_status = self.merge_contents(file_id)
334
self.merge_executable(file_id, file_status)
336
fs_conflicts = resolve_conflicts(self.tt)
337
self.cook_conflicts(fs_conflicts)
338
for line in conflicts_strings(self.cooked_conflicts):
348
def parent(entry, file_id):
349
"""Determine the parent for a file_id (used as a key method)"""
352
return entry.parent_id
355
def name(entry, file_id):
356
"""Determine the name for a file_id (used as a key method)"""
362
def contents_sha1(tree, file_id):
363
"""Determine the sha1 of the file contents (used as a key method)."""
364
if file_id not in tree:
366
return tree.get_file_sha1(file_id)
369
def executable(tree, file_id):
370
"""Determine the executability of a file-id (used as a key method)."""
371
if file_id not in tree:
373
if tree.kind(file_id) != "file":
375
return tree.is_executable(file_id)
378
def kind(tree, file_id):
379
"""Determine the kind of a file-id (used as a key method)."""
380
if file_id not in tree:
382
return tree.kind(file_id)
385
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
386
"""Do a three-way test on a scalar.
387
Return "this", "other" or "conflict", depending whether a value wins.
389
key_base = key(base_tree, file_id)
390
key_other = key(other_tree, file_id)
391
#if base == other, either they all agree, or only THIS has changed.
392
if key_base == key_other:
394
key_this = key(this_tree, file_id)
395
if key_this not in (key_base, key_other):
397
# "Ambiguous clean merge"
398
elif key_this == key_other:
401
assert key_this == key_base
404
def merge_names(self, file_id):
405
"""Perform a merge on file_id names and parents"""
407
if file_id in tree.inventory:
408
return tree.inventory[file_id]
411
this_entry = get_entry(self.this_tree)
412
other_entry = get_entry(self.other_tree)
413
base_entry = get_entry(self.base_tree)
414
name_winner = self.scalar_three_way(this_entry, base_entry,
415
other_entry, file_id, self.name)
416
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
417
other_entry, file_id,
419
if this_entry is None:
420
if name_winner == "this":
421
name_winner = "other"
422
if parent_id_winner == "this":
423
parent_id_winner = "other"
424
if name_winner == "this" and parent_id_winner == "this":
426
if name_winner == "conflict":
427
trans_id = self.tt.trans_id_file_id(file_id)
428
self._raw_conflicts.append(('name conflict', trans_id,
429
self.name(this_entry, file_id),
430
self.name(other_entry, file_id)))
431
if parent_id_winner == "conflict":
432
trans_id = self.tt.trans_id_file_id(file_id)
433
self._raw_conflicts.append(('parent conflict', trans_id,
434
self.parent(this_entry, file_id),
435
self.parent(other_entry, file_id)))
436
if other_entry is None:
437
# it doesn't matter whether the result was 'other' or
438
# 'conflict'-- if there's no 'other', we leave it alone.
440
# if we get here, name_winner and parent_winner are set to safe values.
441
winner_entry = {"this": this_entry, "other": other_entry,
442
"conflict": other_entry}
443
trans_id = self.tt.trans_id_file_id(file_id)
444
parent_id = winner_entry[parent_id_winner].parent_id
445
parent_trans_id = self.tt.trans_id_file_id(parent_id)
446
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
449
def merge_contents(self, file_id):
450
"""Performa a merge on file_id contents."""
451
def contents_pair(tree):
452
if file_id not in tree:
454
kind = tree.kind(file_id)
455
if kind == "root_directory":
458
contents = tree.get_file_sha1(file_id)
459
elif kind == "symlink":
460
contents = tree.get_symlink_target(file_id)
463
return kind, contents
464
# See SPOT run. run, SPOT, run.
465
# So we're not QUITE repeating ourselves; we do tricky things with
467
base_pair = contents_pair(self.base_tree)
468
other_pair = contents_pair(self.other_tree)
469
if base_pair == other_pair:
470
# OTHER introduced no changes
472
this_pair = contents_pair(self.this_tree)
473
if this_pair == other_pair:
474
# THIS and OTHER introduced the same changes
477
trans_id = self.tt.trans_id_file_id(file_id)
478
if this_pair == base_pair:
479
# only OTHER introduced changes
480
if file_id in self.this_tree:
481
# Remove any existing contents
482
self.tt.delete_contents(trans_id)
483
if file_id in self.other_tree:
484
# OTHER changed the file
485
create_by_entry(self.tt,
486
self.other_tree.inventory[file_id],
487
self.other_tree, trans_id)
488
if file_id not in self.this_tree.inventory:
489
self.tt.version_file(file_id, trans_id)
491
elif file_id in self.this_tree.inventory:
492
# OTHER deleted the file
493
self.tt.unversion_file(trans_id)
495
#BOTH THIS and OTHER introduced changes; scalar conflict
496
elif this_pair[0] == "file" and other_pair[0] == "file":
497
# THIS and OTHER are both files, so text merge. Either
498
# BASE is a file, or both converted to files, so at least we
499
# have agreement that output should be a file.
500
if file_id not in self.this_tree.inventory:
501
self.tt.version_file(file_id, trans_id)
502
self.text_merge(file_id, trans_id)
504
self.tt.tree_kind(trans_id)
505
self.tt.delete_contents(trans_id)
510
# Scalar conflict, can't text merge. Dump conflicts
511
trans_id = self.tt.trans_id_file_id(file_id)
512
name = self.tt.final_name(trans_id)
513
parent_id = self.tt.final_parent(trans_id)
514
if file_id in self.this_tree.inventory:
515
self.tt.unversion_file(trans_id)
516
self.tt.delete_contents(trans_id)
517
file_group = self._dump_conflicts(name, parent_id, file_id,
519
self._raw_conflicts.append(('contents conflict', file_group))
521
def get_lines(self, tree, file_id):
522
"""Return the lines in a file, or an empty list."""
524
return tree.get_file(file_id).readlines()
528
def text_merge(self, file_id, trans_id):
529
"""Perform a three-way text merge on a file_id"""
530
# it's possible that we got here with base as a different type.
531
# if so, we just want two-way text conflicts.
532
if file_id in self.base_tree and \
533
self.base_tree.kind(file_id) == "file":
534
base_lines = self.get_lines(self.base_tree, file_id)
537
other_lines = self.get_lines(self.other_tree, file_id)
538
this_lines = self.get_lines(self.this_tree, file_id)
539
m3 = Merge3(base_lines, this_lines, other_lines)
540
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
541
if self.show_base is True:
542
base_marker = '|' * 7
546
def iter_merge3(retval):
547
retval["text_conflicts"] = False
548
for line in m3.merge_lines(name_a = "TREE",
549
name_b = "MERGE-SOURCE",
550
name_base = "BASE-REVISION",
551
start_marker=start_marker,
552
base_marker=base_marker,
553
reprocess=self.reprocess):
554
if line.startswith(start_marker):
555
retval["text_conflicts"] = True
556
yield line.replace(start_marker, '<' * 7)
560
merge3_iterator = iter_merge3(retval)
561
self.tt.create_file(merge3_iterator, trans_id)
562
if retval["text_conflicts"] is True:
563
self._raw_conflicts.append(('text conflict', trans_id))
564
name = self.tt.final_name(trans_id)
565
parent_id = self.tt.final_parent(trans_id)
566
file_group = self._dump_conflicts(name, parent_id, file_id,
567
this_lines, base_lines,
569
file_group.append(trans_id)
571
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
572
base_lines=None, other_lines=None, set_version=False,
574
"""Emit conflict files.
575
If this_lines, base_lines, or other_lines are omitted, they will be
576
determined automatically. If set_version is true, the .OTHER, .THIS
577
or .BASE (in that order) will be created as versioned files.
579
data = [('OTHER', self.other_tree, other_lines),
580
('THIS', self.this_tree, this_lines)]
582
data.append(('BASE', self.base_tree, base_lines))
585
for suffix, tree, lines in data:
587
trans_id = self._conflict_file(name, parent_id, tree, file_id,
589
file_group.append(trans_id)
590
if set_version and not versioned:
591
self.tt.version_file(file_id, trans_id)
595
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
597
"""Emit a single conflict file."""
598
name = name + '.' + suffix
599
trans_id = self.tt.create_path(name, parent_id)
600
entry = tree.inventory[file_id]
601
create_by_entry(self.tt, entry, tree, trans_id, lines)
604
def merge_executable(self, file_id, file_status):
605
"""Perform a merge on the execute bit."""
606
if file_status == "deleted":
608
trans_id = self.tt.trans_id_file_id(file_id)
610
if self.tt.final_kind(trans_id) != "file":
614
winner = self.scalar_three_way(self.this_tree, self.base_tree,
615
self.other_tree, file_id,
617
if winner == "conflict":
618
# There must be a None in here, if we have a conflict, but we
619
# need executability since file status was not deleted.
620
if self.other_tree.is_executable(file_id) is None:
625
if file_status == "modified":
626
executability = self.this_tree.is_executable(file_id)
627
if executability is not None:
628
trans_id = self.tt.trans_id_file_id(file_id)
629
self.tt.set_executability(executability, trans_id)
631
assert winner == "other"
632
if file_id in self.other_tree:
633
executability = self.other_tree.is_executable(file_id)
634
elif file_id in self.this_tree:
635
executability = self.this_tree.is_executable(file_id)
636
elif file_id in self.base_tree:
637
executability = self.base_tree.is_executable(file_id)
638
if executability is not None:
639
trans_id = self.tt.trans_id_file_id(file_id)
640
self.tt.set_executability(executability, trans_id)
642
def cook_conflicts(self, fs_conflicts):
643
"""Convert all conflicts into a form that doesn't depend on trans_id"""
645
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
646
fp = FinalPaths(self.tt)
647
for conflict in self._raw_conflicts:
648
conflict_type = conflict[0]
649
if conflict_type in ('name conflict', 'parent conflict'):
650
trans_id = conflict[1]
651
conflict_args = conflict[2:]
652
if trans_id not in name_conflicts:
653
name_conflicts[trans_id] = {}
654
unique_add(name_conflicts[trans_id], conflict_type,
656
if conflict_type == 'contents conflict':
657
for trans_id in conflict[1]:
658
file_id = self.tt.final_file_id(trans_id)
391
659
if file_id is not None:
392
interesting_ids.add(file_id)
395
raise BzrCommandError("%s is not a source file in any"
397
merge_inner(this_branch, other_tree, base_tree, tempdir,
398
ignore_zero=ignore_zero, backup_files=backup_files,
399
merge_type=merge_type, interesting_ids=interesting_ids)
400
if base_is_ancestor and other_rev_id is not None\
401
and other_rev_id not in this_branch.revision_history():
402
this_branch.add_pending_merge(other_rev_id)
404
shutil.rmtree(tempdir)
407
def set_interesting(inventory_a, inventory_b, interesting_ids):
408
"""Mark files whose ids are in interesting_ids as interesting
410
for inventory in (inventory_a, inventory_b):
411
for path, source_file in inventory.iteritems():
412
source_file.interesting = source_file.id in interesting_ids
415
def merge_inner(this_branch, other_tree, base_tree, tempdir,
416
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
417
interesting_ids=None):
419
def merge_factory(file_id, base, other):
420
contents_change = merge_type(file_id, base, other)
422
contents_change = BackupBeforeChange(contents_change)
423
return contents_change
425
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
427
def get_inventory(tree):
428
return tree.tree.inventory
430
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),
434
merge_factory=merge_factory,
435
interesting_ids=interesting_ids)
438
for id, path in inv_changes.iteritems():
661
path = fp.get_path(trans_id)
662
for suffix in ('.BASE', '.THIS', '.OTHER'):
663
if path.endswith(suffix):
664
path = path[:-len(suffix)]
666
self.cooked_conflicts.append((conflict_type, file_id, path))
667
if conflict_type == 'text conflict':
668
trans_id = conflict[1]
669
path = fp.get_path(trans_id)
670
file_id = self.tt.final_file_id(trans_id)
671
self.cooked_conflicts.append((conflict_type, file_id, path))
673
for trans_id, conflicts in name_conflicts.iteritems():
675
this_parent, other_parent = conflicts['parent conflict']
676
assert this_parent != other_parent
678
this_parent = other_parent = \
679
self.tt.final_file_id(self.tt.final_parent(trans_id))
681
this_name, other_name = conflicts['name conflict']
682
assert this_name != other_name
684
this_name = other_name = self.tt.final_name(trans_id)
685
other_path = fp.get_path(trans_id)
686
if this_parent is not None:
688
fp.get_path(self.tt.trans_id_file_id(this_parent))
689
this_path = pathjoin(this_parent_path, this_name)
443
assert path.startswith('.' + os.sep), "path is %s" % path
445
adjust_ids.append((path, id))
446
if len(adjust_ids) > 0:
447
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
451
def regen_inventory(this_branch, root, new_entries):
452
old_entries = this_branch.read_working_inventory()
456
for path, file_id in new_entries:
459
new_entries_map[file_id] = path
461
def id2path(file_id):
462
path = new_entries_map.get(file_id)
465
entry = old_entries[file_id]
466
if entry.parent_id is None:
468
return os.path.join(id2path(entry.parent_id), entry.name)
470
for file_id in old_entries:
471
entry = old_entries[file_id]
472
path = id2path(file_id)
473
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
474
by_path[path] = file_id
479
for path, file_id in new_entries:
481
del new_inventory[file_id]
484
new_path_list.append((path, file_id))
485
if file_id not in old_entries:
487
# Ensure no file is added before its parent
489
for path, file_id in new_path_list:
493
parent = by_path[os.path.dirname(path)]
494
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
495
new_inventory[file_id] = (path, file_id, parent, kind)
496
by_path[path] = file_id
498
# Get a list in insertion order
499
new_inventory_list = new_inventory.values()
500
mutter ("""Inventory regeneration:
501
old length: %i insertions: %i deletions: %i new_length: %i"""\
502
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
503
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
504
new_inventory_list.sort()
505
return new_inventory_list
507
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
508
"diff3": (Diff3Merge, "Merge using external diff3")
691
this_path = "<deleted>"
692
file_id = self.tt.final_file_id(trans_id)
693
self.cooked_conflicts.append(('path conflict', file_id, this_path,
697
class WeaveMerger(Merge3Merger):
698
"""Three-way tree merger, text weave merger."""
699
supports_reprocess = False
700
supports_show_base = False
702
def __init__(self, working_tree, this_tree, base_tree, other_tree):
703
self.this_revision_tree = self._get_revision_tree(this_tree)
704
self.other_revision_tree = self._get_revision_tree(other_tree)
705
super(WeaveMerger, self).__init__(working_tree, this_tree,
706
base_tree, other_tree)
708
def _get_revision_tree(self, tree):
709
"""Return a revision tree releated to this tree.
710
If the tree is a WorkingTree, the basis will be returned.
712
if getattr(tree, 'get_weave', False) is False:
713
# If we have a WorkingTree, try using the basis
714
return tree.branch.basis_tree()
718
def _check_file(self, file_id):
719
"""Check that the revision tree's version of the file matches."""
720
for tree, rt in ((self.this_tree, self.this_revision_tree),
721
(self.other_tree, self.other_revision_tree)):
724
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
725
raise WorkingTreeNotRevision(self.this_tree)
727
def _merged_lines(self, file_id):
728
"""Generate the merged lines.
729
There is no distinction between lines that are meant to contain <<<<<<<
732
weave = self.this_revision_tree.get_weave(file_id)
733
this_revision_id = self.this_revision_tree.inventory[file_id].revision
734
other_revision_id = \
735
self.other_revision_tree.inventory[file_id].revision
736
this_i = weave.lookup(this_revision_id)
737
other_i = weave.lookup(other_revision_id)
738
plan = weave.plan_merge(this_i, other_i)
739
return weave.weave_merge(plan)
741
def text_merge(self, file_id, trans_id):
742
"""Perform a (weave) text merge for a given file and file-id.
743
If conflicts are encountered, .THIS and .OTHER files will be emitted,
744
and a conflict will be noted.
746
self._check_file(file_id)
747
lines = self._merged_lines(file_id)
748
conflicts = '<<<<<<<\n' in lines
749
self.tt.create_file(lines, trans_id)
751
self._raw_conflicts.append(('text conflict', trans_id))
752
name = self.tt.final_name(trans_id)
753
parent_id = self.tt.final_parent(trans_id)
754
file_group = self._dump_conflicts(name, parent_id, file_id,
756
file_group.append(trans_id)
759
class Diff3Merger(Merge3Merger):
760
"""Three-way merger using external diff3 for text merging"""
761
def dump_file(self, temp_dir, name, tree, file_id):
762
out_path = pathjoin(temp_dir, name)
763
out_file = file(out_path, "wb")
764
in_file = tree.get_file(file_id)
769
def text_merge(self, file_id, trans_id):
770
"""Perform a diff3 merge using a specified file-id and trans-id.
771
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
772
will be dumped, and a will be conflict noted.
775
temp_dir = mkdtemp(prefix="bzr-")
777
new_file = pathjoin(temp_dir, "new")
778
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
779
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
780
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
781
status = bzrlib.patch.diff3(new_file, this, base, other)
782
if status not in (0, 1):
783
raise BzrError("Unhandled diff3 exit code")
784
self.tt.create_file(file(new_file, "rb"), trans_id)
786
name = self.tt.final_name(trans_id)
787
parent_id = self.tt.final_parent(trans_id)
788
self._dump_conflicts(name, parent_id, file_id)
789
self._raw_conflicts.append(('text conflict', trans_id))
794
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
796
merge_type=Merge3Merger,
797
interesting_ids=None,
801
interesting_files=None,
803
"""Primary interface for merging.
805
typical use is probably
806
'merge_inner(branch, branch.get_revision_tree(other_revision),
807
branch.get_revision_tree(base_revision))'
809
if this_tree is None:
810
this_tree = this_branch.working_tree()
811
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree)
812
merger.backup_files = backup_files
813
merger.merge_type = merge_type
814
merger.interesting_ids = interesting_ids
815
if interesting_files:
816
assert not interesting_ids, ('Only supply interesting_ids'
817
' or interesting_files')
818
merger._set_interesting_files(interesting_files)
819
merger.show_base = show_base
820
merger.reprocess = reprocess
821
merger.other_rev_id = other_rev_id
822
merger.other_basis = other_rev_id
823
return merger.do_merge()
826
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
827
"diff3": (Diff3Merger, "Merge using external diff3"),
828
'weave': (WeaveMerger, "Weave-based merge")