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 progress import DummyProgress, ProgressPhase
41
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
42
from bzrlib.symbol_versioning import *
32
43
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
other.contents.apply(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
return self.this_tree.id2abspath(file_id)
158
def add_missing_parents(self, file_id, tree):
159
"""If some of the parents for file_id are missing, add them."""
160
entry = tree.inventory[file_id]
161
if entry.parent_id not in self.this_tree:
162
return self.create_all_missing(entry.parent_id, tree)
164
return self.abs_this_path(entry.parent_id)
166
def create_all_missing(self, file_id, tree):
167
"""Add contents for a file_id and all its parents to a tree."""
168
entry = tree.inventory[file_id]
169
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
170
abspath = self.create_all_missing(entry.parent_id, tree)
172
abspath = self.abs_this_path(entry.parent_id)
173
entry_path = os.path.join(abspath, entry.name)
174
if not os.path.isdir(entry_path):
175
self.create(file_id, entry_path, tree)
178
def create(self, file_id, path, tree, reverse=False):
179
"""Uses tree data to create a filesystem object for the file_id"""
180
from changeset import get_contents
181
get_contents(tree, file_id)(path, self, reverse)
183
def missing_for_merge(self, file_id, other_path):
184
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
185
self.conflict("Other branch modified locally deleted file %s" %
187
parent_dir = self.add_missing_parents(file_id, self.other_tree)
188
stem = os.path.join(parent_dir, os.path.basename(other_path))
189
self.create(file_id, stem+".OTHER", self.other_tree)
190
self.create(file_id, stem+".BASE", self.base_tree)
192
def threeway_contents_conflict(filename, this_contents, base_contents,
194
self.conflict("Three-way conflict merging %s" % filename)
197
if not self.ignore_zero:
198
note("%d conflicts encountered.\n" % self.conflicts)
200
def get_tree(treespec, local_branch=None):
44
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
45
conflicts_strings, FinalPaths, create_by_entry,
49
# TODO: Report back as changes are merged in
51
def _get_tree(treespec, local_branch=None):
201
52
location, revno = treespec
202
53
branch = Branch.open_containing(location)[0]
206
57
revision = branch.last_revision()
208
59
revision = branch.get_rev_id(revno)
209
return branch, get_revid_tree(branch, revision, local_branch)
211
def get_revid_tree(branch, revision, local_branch):
61
revision = NULL_REVISION
62
return branch, _get_revid_tree(branch, revision, local_branch)
65
def _get_revid_tree(branch, revision, local_branch):
212
66
if revision is None:
213
base_tree = branch.working_tree()
67
base_tree = branch.bzrdir.open_workingtree()
215
69
if local_branch is not None:
216
greedy_fetch(local_branch, branch, revision)
217
base_tree = local_branch.revision_tree(revision)
70
if local_branch.base != branch.base:
71
local_branch.fetch(branch, revision)
72
base_tree = local_branch.repository.revision_tree(revision)
219
base_tree = branch.revision_tree(revision)
74
base_tree = branch.repository.revision_tree(revision)
223
def file_exists(tree, file_id):
224
return tree.has_filename(tree.id2path(file_id))
227
def build_working_dir(to_dir):
228
"""Build a working directory in an empty directory.
230
to_dir is a directory containing branch metadata but no working files,
231
typically constructed by cloning an existing branch.
233
This is split out as a special idiomatic case of merge. It could
234
eventually be done by just building the tree directly calling into
235
lower-level code (e.g. constructing a changeset).
237
# RBC 20051019 is this not just 'export' ?
238
merge((to_dir, -1), (to_dir, 0), this_dir=to_dir,
239
check_clean=False, ignore_zero=True)
242
def merge(other_revision, base_revision,
243
check_clean=True, ignore_zero=False,
244
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
246
"""Merge changes into a tree.
249
tuple(path, revision) Base for three-way merge.
251
tuple(path, revision) Other revision for three-way merge.
253
Directory to merge changes into; '.' by default.
255
If true, this_dir must have no uncommitted changes before the
257
ignore_zero - If true, suppress the "zero conflicts" message when
258
there are no conflicts; should be set when doing something we expect
259
to complete perfectly.
261
All available ancestors of other_revision and base_revision are
262
automatically pulled into the branch.
266
this_branch = Branch.open_containing(this_dir)[0]
267
this_rev_id = this_branch.last_revision()
268
if this_rev_id is None:
269
raise BzrCommandError("This branch has no commits")
271
changes = compare_trees(this_branch.working_tree(),
272
this_branch.basis_tree(), False)
273
if changes.has_changed():
274
raise BzrCommandError("Working tree has uncommitted changes.")
275
other_branch, other_tree = get_tree(other_revision, this_branch)
276
if other_revision[1] == -1:
277
other_rev_id = other_branch.last_revision()
278
if other_rev_id is None:
279
raise NoCommits(other_branch)
280
other_basis = other_rev_id
281
elif other_revision[1] is not None:
282
other_rev_id = other_branch.get_rev_id(other_revision[1])
283
other_basis = other_rev_id
286
other_basis = other_branch.last_revision()
287
if other_basis is None:
288
raise NoCommits(other_branch)
289
if base_revision == [None, None]:
78
def transform_tree(from_tree, to_tree, interesting_ids=None):
79
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
80
interesting_ids=interesting_ids, this_tree=from_tree)
84
def __init__(self, this_branch, other_tree=None, base_tree=None,
85
this_tree=None, pb=DummyProgress()):
87
assert this_tree is not None, "this_tree is required"
88
self.this_branch = this_branch
89
self.this_basis = this_branch.last_revision()
90
self.this_rev_id = None
91
self.this_tree = this_tree
92
self.this_revision_tree = None
93
self.this_basis_tree = None
94
self.other_tree = other_tree
95
self.base_tree = base_tree
96
self.ignore_zero = False
97
self.backup_files = False
98
self.interesting_ids = None
99
self.show_base = False
100
self.reprocess = False
105
def revision_tree(self, revision_id):
106
return self.this_branch.repository.revision_tree(revision_id)
108
def ensure_revision_trees(self):
109
if self.this_revision_tree is None:
110
self.this_basis_tree = self.this_branch.repository.revision_tree(
112
if self.this_basis == self.this_rev_id:
113
self.this_revision_tree = self.this_basis_tree
115
if self.other_rev_id is None:
116
other_basis_tree = self.revision_tree(self.other_basis)
117
changes = compare_trees(self.other_tree, other_basis_tree)
118
if changes.has_changed():
119
raise WorkingTreeNotRevision(self.this_tree)
120
other_rev_id = other_basis
121
self.other_tree = other_basis_tree
123
def file_revisions(self, file_id):
124
self.ensure_revision_trees()
125
def get_id(tree, file_id):
126
revision_id = tree.inventory[file_id].revision
127
assert revision_id is not None
129
if self.this_rev_id is None:
130
if self.this_basis_tree.get_file_sha1(file_id) != \
131
self.this_tree.get_file_sha1(file_id):
132
raise WorkingTreeNotRevision(self.this_tree)
134
trees = (self.this_basis_tree, self.other_tree)
135
return [get_id(tree, file_id) for tree in trees]
137
def check_basis(self, check_clean):
138
if self.this_basis is None:
139
raise BzrCommandError("This branch has no commits")
142
if self.this_basis != self.this_rev_id:
143
raise BzrCommandError("Working tree has uncommitted changes.")
145
def compare_basis(self):
146
changes = compare_trees(self.this_tree,
147
self.this_tree.basis_tree(), False)
148
if not changes.has_changed():
149
self.this_rev_id = self.this_basis
151
def set_interesting_files(self, file_list):
291
base_rev_id = common_ancestor(this_rev_id, other_basis,
293
except NoCommonAncestor:
294
raise UnrelatedBranches()
295
base_tree = get_revid_tree(this_branch, base_rev_id, None)
296
base_is_ancestor = True
298
base_branch, base_tree = get_tree(base_revision)
299
if base_revision[1] == -1:
300
base_rev_id = base_branch.last_revision()
301
elif base_revision[1] is None:
304
base_rev_id = base_branch.get_rev_id(base_revision[1])
305
fetch(from_branch=base_branch, to_branch=this_branch)
306
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
308
if file_list is None:
309
interesting_ids = None
153
self._set_interesting_files(file_list)
154
except NotVersionedError, e:
155
raise BzrCommandError("%s is not a source file in any"
158
def _set_interesting_files(self, file_list):
159
"""Set the list of interesting ids from a list of files."""
160
if file_list is None:
161
self.interesting_ids = None
311
164
interesting_ids = set()
312
this_tree = this_branch.working_tree()
313
for fname in file_list:
314
path = this_tree.relpath(fname)
165
for path in file_list:
316
for tree in (this_tree, base_tree, other_tree):
167
for tree in (self.this_tree, self.base_tree, self.other_tree):
317
168
file_id = tree.inventory.path2id(path)
318
169
if file_id is not None:
319
170
interesting_ids.add(file_id)
322
raise BzrCommandError("%s is not a source file in any"
324
merge_inner(this_branch, other_tree, base_tree, tempdir=None,
325
ignore_zero=ignore_zero, backup_files=backup_files,
326
merge_type=merge_type, interesting_ids=interesting_ids)
327
if base_is_ancestor and other_rev_id is not None\
328
and other_rev_id not in this_branch.revision_history():
329
this_branch.add_pending_merge(other_rev_id)
332
def set_interesting(inventory_a, inventory_b, interesting_ids):
333
"""Mark files whose ids are in interesting_ids as interesting
335
for inventory in (inventory_a, inventory_b):
336
for path, source_file in inventory.iteritems():
337
source_file.interesting = source_file.id in interesting_ids
340
def merge_inner(this_branch, other_tree, base_tree, tempdir=None,
341
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
342
interesting_ids=None):
173
raise NotVersionedError(path=path)
174
self.interesting_ids = interesting_ids
176
def set_pending(self):
177
if not self.base_is_ancestor:
179
if self.other_rev_id is None:
181
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
182
if self.other_rev_id in ancestry:
184
self.this_tree.add_pending_merge(self.other_rev_id)
186
def set_other(self, other_revision):
187
other_branch, self.other_tree = _get_tree(other_revision,
189
if other_revision[1] == -1:
190
self.other_rev_id = other_branch.last_revision()
191
if self.other_rev_id is None:
192
raise NoCommits(other_branch)
193
self.other_basis = self.other_rev_id
194
elif other_revision[1] is not None:
195
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
196
self.other_basis = self.other_rev_id
198
self.other_rev_id = None
199
self.other_basis = other_branch.last_revision()
200
if self.other_basis is None:
201
raise NoCommits(other_branch)
202
if other_branch.base != self.this_branch.base:
203
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
205
def set_base(self, base_revision):
206
mutter("doing merge() with no base_revision specified")
207
if base_revision == [None, None]:
209
pb = bzrlib.ui.ui_factory.nested_progress_bar()
211
this_repo = self.this_branch.repository
212
self.base_rev_id = common_ancestor(self.this_basis,
217
except NoCommonAncestor:
218
raise UnrelatedBranches()
219
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
221
self.base_is_ancestor = True
223
base_branch, self.base_tree = _get_tree(base_revision)
224
if base_revision[1] == -1:
225
self.base_rev_id = base_branch.last_revision()
226
elif base_revision[1] is None:
227
self.base_rev_id = None
229
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
230
self.this_branch.fetch(base_branch)
231
self.base_is_ancestor = is_ancestor(self.this_basis,
236
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
237
'other_tree': self.other_tree,
238
'interesting_ids': self.interesting_ids,
240
if self.merge_type.requires_base:
241
kwargs['base_tree'] = self.base_tree
242
if self.merge_type.supports_reprocess:
243
kwargs['reprocess'] = self.reprocess
245
raise BzrError("Reprocess is not supported for this merge"
246
" type. %s" % merge_type)
247
if self.merge_type.supports_show_base:
248
kwargs['show_base'] = self.show_base
250
raise BzrError("Showing base is not supported for this"
251
" merge type. %s" % self.merge_type)
252
merge = self.merge_type(pb=self._pb, **kwargs)
253
if len(merge.cooked_conflicts) == 0:
254
if not self.ignore_zero:
255
note("All changes applied successfully.")
257
note("%d conflicts encountered." % len(merge.cooked_conflicts))
259
return len(merge.cooked_conflicts)
261
def regen_inventory(self, new_entries):
262
old_entries = self.this_tree.read_working_inventory()
266
for path, file_id in new_entries:
269
new_entries_map[file_id] = path
271
def id2path(file_id):
272
path = new_entries_map.get(file_id)
275
entry = old_entries[file_id]
276
if entry.parent_id is None:
278
return pathjoin(id2path(entry.parent_id), entry.name)
280
for file_id in old_entries:
281
entry = old_entries[file_id]
282
path = id2path(file_id)
283
new_inventory[file_id] = (path, file_id, entry.parent_id,
285
by_path[path] = file_id
290
for path, file_id in new_entries:
292
del new_inventory[file_id]
295
new_path_list.append((path, file_id))
296
if file_id not in old_entries:
298
# Ensure no file is added before its parent
300
for path, file_id in new_path_list:
304
parent = by_path[os.path.dirname(path)]
305
abspath = pathjoin(self.this_tree.basedir, path)
306
kind = bzrlib.osutils.file_kind(abspath)
307
new_inventory[file_id] = (path, file_id, parent, kind)
308
by_path[path] = file_id
310
# Get a list in insertion order
311
new_inventory_list = new_inventory.values()
312
mutter ("""Inventory regeneration:
313
old length: %i insertions: %i deletions: %i new_length: %i"""\
314
% (len(old_entries), insertions, deletions,
315
len(new_inventory_list)))
316
assert len(new_inventory_list) == len(old_entries) + insertions\
318
new_inventory_list.sort()
319
return new_inventory_list
322
class Merge3Merger(object):
323
"""Three-way merger that uses the merge3 text merger"""
325
supports_reprocess = True
326
supports_show_base = True
327
history_based = False
329
def __init__(self, working_tree, this_tree, base_tree, other_tree,
330
interesting_ids=None, reprocess=False, show_base=False,
331
pb=DummyProgress(), pp=None):
332
"""Initialize the merger object and perform the merge."""
333
object.__init__(self)
334
self.this_tree = working_tree
335
self.base_tree = base_tree
336
self.other_tree = other_tree
337
self._raw_conflicts = []
338
self.cooked_conflicts = []
339
self.reprocess = reprocess
340
self.show_base = show_base
344
self.pp = ProgressPhase("Merge phase", 3, self.pb)
346
if interesting_ids is not None:
347
all_ids = interesting_ids
349
all_ids = set(base_tree)
350
all_ids.update(other_tree)
351
working_tree.lock_write()
352
self.tt = TreeTransform(working_tree, self.pb)
355
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
357
for num, file_id in enumerate(all_ids):
358
child_pb.update('Preparing file merge', num, len(all_ids))
359
self.merge_names(file_id)
360
file_status = self.merge_contents(file_id)
361
self.merge_executable(file_id, file_status)
366
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
368
fs_conflicts = resolve_conflicts(self.tt, child_pb)
371
self.cook_conflicts(fs_conflicts)
372
for line in conflicts_strings(self.cooked_conflicts):
375
results = self.tt.apply()
376
self.write_modified(results)
382
working_tree.unlock()
385
def write_modified(self, results):
387
for path in results.modified_paths:
388
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
391
hash = self.this_tree.get_file_sha1(file_id)
394
modified_hashes[file_id] = hash
395
self.this_tree.set_merge_modified(modified_hashes)
398
def parent(entry, file_id):
399
"""Determine the parent for a file_id (used as a key method)"""
402
return entry.parent_id
405
def name(entry, file_id):
406
"""Determine the name for a file_id (used as a key method)"""
412
def contents_sha1(tree, file_id):
413
"""Determine the sha1 of the file contents (used as a key method)."""
414
if file_id not in tree:
416
return tree.get_file_sha1(file_id)
419
def executable(tree, file_id):
420
"""Determine the executability of a file-id (used as a key method)."""
421
if file_id not in tree:
423
if tree.kind(file_id) != "file":
425
return tree.is_executable(file_id)
428
def kind(tree, file_id):
429
"""Determine the kind of a file-id (used as a key method)."""
430
if file_id not in tree:
432
return tree.kind(file_id)
435
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
436
"""Do a three-way test on a scalar.
437
Return "this", "other" or "conflict", depending whether a value wins.
439
key_base = key(base_tree, file_id)
440
key_other = key(other_tree, file_id)
441
#if base == other, either they all agree, or only THIS has changed.
442
if key_base == key_other:
444
key_this = key(this_tree, file_id)
445
if key_this not in (key_base, key_other):
447
# "Ambiguous clean merge"
448
elif key_this == key_other:
451
assert key_this == key_base
454
def merge_names(self, file_id):
455
"""Perform a merge on file_id names and parents"""
457
if file_id in tree.inventory:
458
return tree.inventory[file_id]
461
this_entry = get_entry(self.this_tree)
462
other_entry = get_entry(self.other_tree)
463
base_entry = get_entry(self.base_tree)
464
name_winner = self.scalar_three_way(this_entry, base_entry,
465
other_entry, file_id, self.name)
466
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
467
other_entry, file_id,
469
if this_entry is None:
470
if name_winner == "this":
471
name_winner = "other"
472
if parent_id_winner == "this":
473
parent_id_winner = "other"
474
if name_winner == "this" and parent_id_winner == "this":
476
if name_winner == "conflict":
477
trans_id = self.tt.trans_id_file_id(file_id)
478
self._raw_conflicts.append(('name conflict', trans_id,
479
self.name(this_entry, file_id),
480
self.name(other_entry, file_id)))
481
if parent_id_winner == "conflict":
482
trans_id = self.tt.trans_id_file_id(file_id)
483
self._raw_conflicts.append(('parent conflict', trans_id,
484
self.parent(this_entry, file_id),
485
self.parent(other_entry, file_id)))
486
if other_entry is None:
487
# it doesn't matter whether the result was 'other' or
488
# 'conflict'-- if there's no 'other', we leave it alone.
490
# if we get here, name_winner and parent_winner are set to safe values.
491
winner_entry = {"this": this_entry, "other": other_entry,
492
"conflict": other_entry}
493
trans_id = self.tt.trans_id_file_id(file_id)
494
parent_id = winner_entry[parent_id_winner].parent_id
495
parent_trans_id = self.tt.trans_id_file_id(parent_id)
496
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
499
def merge_contents(self, file_id):
500
"""Performa a merge on file_id contents."""
501
def contents_pair(tree):
502
if file_id not in tree:
504
kind = tree.kind(file_id)
505
if kind == "root_directory":
508
contents = tree.get_file_sha1(file_id)
509
elif kind == "symlink":
510
contents = tree.get_symlink_target(file_id)
513
return kind, contents
514
# See SPOT run. run, SPOT, run.
515
# So we're not QUITE repeating ourselves; we do tricky things with
517
base_pair = contents_pair(self.base_tree)
518
other_pair = contents_pair(self.other_tree)
519
if base_pair == other_pair:
520
# OTHER introduced no changes
522
this_pair = contents_pair(self.this_tree)
523
if this_pair == other_pair:
524
# THIS and OTHER introduced the same changes
527
trans_id = self.tt.trans_id_file_id(file_id)
528
if this_pair == base_pair:
529
# only OTHER introduced changes
530
if file_id in self.this_tree:
531
# Remove any existing contents
532
self.tt.delete_contents(trans_id)
533
if file_id in self.other_tree:
534
# OTHER changed the file
535
create_by_entry(self.tt,
536
self.other_tree.inventory[file_id],
537
self.other_tree, trans_id)
538
if file_id not in self.this_tree.inventory:
539
self.tt.version_file(file_id, trans_id)
541
elif file_id in self.this_tree.inventory:
542
# OTHER deleted the file
543
self.tt.unversion_file(trans_id)
545
#BOTH THIS and OTHER introduced changes; scalar conflict
546
elif this_pair[0] == "file" and other_pair[0] == "file":
547
# THIS and OTHER are both files, so text merge. Either
548
# BASE is a file, or both converted to files, so at least we
549
# have agreement that output should be a file.
550
if file_id not in self.this_tree.inventory:
551
self.tt.version_file(file_id, trans_id)
552
self.text_merge(file_id, trans_id)
554
self.tt.tree_kind(trans_id)
555
self.tt.delete_contents(trans_id)
560
# Scalar conflict, can't text merge. Dump conflicts
561
trans_id = self.tt.trans_id_file_id(file_id)
562
name = self.tt.final_name(trans_id)
563
parent_id = self.tt.final_parent(trans_id)
564
if file_id in self.this_tree.inventory:
565
self.tt.unversion_file(trans_id)
566
self.tt.delete_contents(trans_id)
567
file_group = self._dump_conflicts(name, parent_id, file_id,
569
self._raw_conflicts.append(('contents conflict', file_group))
571
def get_lines(self, tree, file_id):
572
"""Return the lines in a file, or an empty list."""
574
return tree.get_file(file_id).readlines()
578
def text_merge(self, file_id, trans_id):
579
"""Perform a three-way text merge on a file_id"""
580
# it's possible that we got here with base as a different type.
581
# if so, we just want two-way text conflicts.
582
if file_id in self.base_tree and \
583
self.base_tree.kind(file_id) == "file":
584
base_lines = self.get_lines(self.base_tree, file_id)
587
other_lines = self.get_lines(self.other_tree, file_id)
588
this_lines = self.get_lines(self.this_tree, file_id)
589
m3 = Merge3(base_lines, this_lines, other_lines)
590
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
591
if self.show_base is True:
592
base_marker = '|' * 7
596
def iter_merge3(retval):
597
retval["text_conflicts"] = False
598
for line in m3.merge_lines(name_a = "TREE",
599
name_b = "MERGE-SOURCE",
600
name_base = "BASE-REVISION",
601
start_marker=start_marker,
602
base_marker=base_marker,
603
reprocess=self.reprocess):
604
if line.startswith(start_marker):
605
retval["text_conflicts"] = True
606
yield line.replace(start_marker, '<' * 7)
610
merge3_iterator = iter_merge3(retval)
611
self.tt.create_file(merge3_iterator, trans_id)
612
if retval["text_conflicts"] is True:
613
self._raw_conflicts.append(('text conflict', trans_id))
614
name = self.tt.final_name(trans_id)
615
parent_id = self.tt.final_parent(trans_id)
616
file_group = self._dump_conflicts(name, parent_id, file_id,
617
this_lines, base_lines,
619
file_group.append(trans_id)
621
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
622
base_lines=None, other_lines=None, set_version=False,
624
"""Emit conflict files.
625
If this_lines, base_lines, or other_lines are omitted, they will be
626
determined automatically. If set_version is true, the .OTHER, .THIS
627
or .BASE (in that order) will be created as versioned files.
629
data = [('OTHER', self.other_tree, other_lines),
630
('THIS', self.this_tree, this_lines)]
632
data.append(('BASE', self.base_tree, base_lines))
635
for suffix, tree, lines in data:
637
trans_id = self._conflict_file(name, parent_id, tree, file_id,
639
file_group.append(trans_id)
640
if set_version and not versioned:
641
self.tt.version_file(file_id, trans_id)
645
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
647
"""Emit a single conflict file."""
648
name = name + '.' + suffix
649
trans_id = self.tt.create_path(name, parent_id)
650
entry = tree.inventory[file_id]
651
create_by_entry(self.tt, entry, tree, trans_id, lines)
654
def merge_executable(self, file_id, file_status):
655
"""Perform a merge on the execute bit."""
656
if file_status == "deleted":
658
trans_id = self.tt.trans_id_file_id(file_id)
660
if self.tt.final_kind(trans_id) != "file":
664
winner = self.scalar_three_way(self.this_tree, self.base_tree,
665
self.other_tree, file_id,
667
if winner == "conflict":
668
# There must be a None in here, if we have a conflict, but we
669
# need executability since file status was not deleted.
670
if self.other_tree.is_executable(file_id) is None:
675
if file_status == "modified":
676
executability = self.this_tree.is_executable(file_id)
677
if executability is not None:
678
trans_id = self.tt.trans_id_file_id(file_id)
679
self.tt.set_executability(executability, trans_id)
681
assert winner == "other"
682
if file_id in self.other_tree:
683
executability = self.other_tree.is_executable(file_id)
684
elif file_id in self.this_tree:
685
executability = self.this_tree.is_executable(file_id)
686
elif file_id in self.base_tree:
687
executability = self.base_tree.is_executable(file_id)
688
if executability is not None:
689
trans_id = self.tt.trans_id_file_id(file_id)
690
self.tt.set_executability(executability, trans_id)
692
def cook_conflicts(self, fs_conflicts):
693
"""Convert all conflicts into a form that doesn't depend on trans_id"""
695
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
696
fp = FinalPaths(self.tt)
697
for conflict in self._raw_conflicts:
698
conflict_type = conflict[0]
699
if conflict_type in ('name conflict', 'parent conflict'):
700
trans_id = conflict[1]
701
conflict_args = conflict[2:]
702
if trans_id not in name_conflicts:
703
name_conflicts[trans_id] = {}
704
unique_add(name_conflicts[trans_id], conflict_type,
706
if conflict_type == 'contents conflict':
707
for trans_id in conflict[1]:
708
file_id = self.tt.final_file_id(trans_id)
709
if file_id is not None:
711
path = fp.get_path(trans_id)
712
for suffix in ('.BASE', '.THIS', '.OTHER'):
713
if path.endswith(suffix):
714
path = path[:-len(suffix)]
716
self.cooked_conflicts.append((conflict_type, file_id, path))
717
if conflict_type == 'text conflict':
718
trans_id = conflict[1]
719
path = fp.get_path(trans_id)
720
file_id = self.tt.final_file_id(trans_id)
721
self.cooked_conflicts.append((conflict_type, file_id, path))
723
for trans_id, conflicts in name_conflicts.iteritems():
725
this_parent, other_parent = conflicts['parent conflict']
726
assert this_parent != other_parent
728
this_parent = other_parent = \
729
self.tt.final_file_id(self.tt.final_parent(trans_id))
731
this_name, other_name = conflicts['name conflict']
732
assert this_name != other_name
734
this_name = other_name = self.tt.final_name(trans_id)
735
other_path = fp.get_path(trans_id)
736
if this_parent is not None:
738
fp.get_path(self.tt.trans_id_file_id(this_parent))
739
this_path = pathjoin(this_parent_path, this_name)
741
this_path = "<deleted>"
742
file_id = self.tt.final_file_id(trans_id)
743
self.cooked_conflicts.append(('path conflict', file_id, this_path,
747
class WeaveMerger(Merge3Merger):
748
"""Three-way tree merger, text weave merger."""
749
supports_reprocess = False
750
supports_show_base = False
752
def __init__(self, working_tree, this_tree, base_tree, other_tree,
753
interesting_ids=None, pb=DummyProgress(), pp=None):
754
self.this_revision_tree = self._get_revision_tree(this_tree)
755
self.other_revision_tree = self._get_revision_tree(other_tree)
756
super(WeaveMerger, self).__init__(working_tree, this_tree,
757
base_tree, other_tree,
758
interesting_ids=interesting_ids,
761
def _get_revision_tree(self, tree):
762
"""Return a revision tree releated to this tree.
763
If the tree is a WorkingTree, the basis will be returned.
765
if getattr(tree, 'get_weave', False) is False:
766
# If we have a WorkingTree, try using the basis
767
return tree.branch.basis_tree()
771
def _check_file(self, file_id):
772
"""Check that the revision tree's version of the file matches."""
773
for tree, rt in ((self.this_tree, self.this_revision_tree),
774
(self.other_tree, self.other_revision_tree)):
777
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
778
raise WorkingTreeNotRevision(self.this_tree)
780
def _merged_lines(self, file_id):
781
"""Generate the merged lines.
782
There is no distinction between lines that are meant to contain <<<<<<<
785
weave = self.this_revision_tree.get_weave(file_id)
786
this_revision_id = self.this_revision_tree.inventory[file_id].revision
787
other_revision_id = \
788
self.other_revision_tree.inventory[file_id].revision
789
plan = weave.plan_merge(this_revision_id, other_revision_id)
790
return weave.weave_merge(plan, '<<<<<<< TREE\n',
791
'>>>>>>> MERGE-SOURCE\n')
793
def text_merge(self, file_id, trans_id):
794
"""Perform a (weave) text merge for a given file and file-id.
795
If conflicts are encountered, .THIS and .OTHER files will be emitted,
796
and a conflict will be noted.
798
self._check_file(file_id)
799
lines = self._merged_lines(file_id)
800
conflicts = '<<<<<<< TREE\n' in lines
801
self.tt.create_file(lines, trans_id)
803
self._raw_conflicts.append(('text conflict', trans_id))
804
name = self.tt.final_name(trans_id)
805
parent_id = self.tt.final_parent(trans_id)
806
file_group = self._dump_conflicts(name, parent_id, file_id,
808
file_group.append(trans_id)
811
class Diff3Merger(Merge3Merger):
812
"""Three-way merger using external diff3 for text merging"""
813
def dump_file(self, temp_dir, name, tree, file_id):
814
out_path = pathjoin(temp_dir, name)
815
out_file = file(out_path, "wb")
816
in_file = tree.get_file(file_id)
821
def text_merge(self, file_id, trans_id):
822
"""Perform a diff3 merge using a specified file-id and trans-id.
823
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
824
will be dumped, and a will be conflict noted.
827
temp_dir = mkdtemp(prefix="bzr-")
829
new_file = pathjoin(temp_dir, "new")
830
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
831
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
832
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
833
status = bzrlib.patch.diff3(new_file, this, base, other)
834
if status not in (0, 1):
835
raise BzrError("Unhandled diff3 exit code")
836
self.tt.create_file(file(new_file, "rb"), trans_id)
838
name = self.tt.final_name(trans_id)
839
parent_id = self.tt.final_parent(trans_id)
840
self._dump_conflicts(name, parent_id, file_id)
841
self._raw_conflicts.append(('text conflict', trans_id))
846
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
848
merge_type=Merge3Merger,
849
interesting_ids=None,
853
interesting_files=None,
343
856
"""Primary interface for merging.
345
typical use is probably
346
'merge_inner(branch, branch.get_revision_tree(other_revision),
347
branch.get_revision_tree(base_revision))'
350
_tempdir = tempfile.mkdtemp(prefix="bzr-")
354
_merge_inner(this_branch, other_tree, base_tree, _tempdir,
355
ignore_zero, merge_type, backup_files, interesting_ids)
358
shutil.rmtree(_tempdir)
361
def _merge_inner(this_branch, other_tree, base_tree, user_tempdir,
362
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
363
interesting_ids=None):
364
def merge_factory(file_id, base, other):
365
contents_change = merge_type(file_id, base, other)
367
contents_change = BackupBeforeChange(contents_change)
368
return contents_change
370
this_tree = get_tree((this_branch.base, None))[1]
372
def get_inventory(tree):
373
return tree.inventory
375
inv_changes = merge_flex(this_tree, base_tree, other_tree,
376
generate_changeset, get_inventory,
377
MergeConflictHandler(this_tree, base_tree,
378
other_tree, ignore_zero=ignore_zero),
379
merge_factory=merge_factory,
380
interesting_ids=interesting_ids)
383
for id, path in inv_changes.iteritems():
388
assert path.startswith('.' + os.sep), "path is %s" % path
390
adjust_ids.append((path, id))
391
if len(adjust_ids) > 0:
392
this_branch.set_inventory(regen_inventory(this_branch,
397
def regen_inventory(this_branch, root, new_entries):
398
old_entries = this_branch.read_working_inventory()
402
for path, file_id in new_entries:
405
new_entries_map[file_id] = path
407
def id2path(file_id):
408
path = new_entries_map.get(file_id)
411
entry = old_entries[file_id]
412
if entry.parent_id is None:
414
return os.path.join(id2path(entry.parent_id), entry.name)
416
for file_id in old_entries:
417
entry = old_entries[file_id]
418
path = id2path(file_id)
419
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
420
by_path[path] = file_id
425
for path, file_id in new_entries:
427
del new_inventory[file_id]
430
new_path_list.append((path, file_id))
431
if file_id not in old_entries:
433
# Ensure no file is added before its parent
435
for path, file_id in new_path_list:
439
parent = by_path[os.path.dirname(path)]
440
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
441
new_inventory[file_id] = (path, file_id, parent, kind)
442
by_path[path] = file_id
444
# Get a list in insertion order
445
new_inventory_list = new_inventory.values()
446
mutter ("""Inventory regeneration:
447
old length: %i insertions: %i deletions: %i new_length: %i"""\
448
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
449
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
450
new_inventory_list.sort()
451
return new_inventory_list
453
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
454
"diff3": (Diff3Merge, "Merge using external diff3")
858
typical use is probably
859
'merge_inner(branch, branch.get_revision_tree(other_revision),
860
branch.get_revision_tree(base_revision))'
862
if this_tree is None:
863
warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
864
"bzrlib version 0.8.",
867
this_tree = this_branch.bzrdir.open_workingtree()
868
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
870
merger.backup_files = backup_files
871
merger.merge_type = merge_type
872
merger.interesting_ids = interesting_ids
873
merger.ignore_zero = ignore_zero
874
if interesting_files:
875
assert not interesting_ids, ('Only supply interesting_ids'
876
' or interesting_files')
877
merger._set_interesting_files(interesting_files)
878
merger.show_base = show_base
879
merger.reprocess = reprocess
880
merger.other_rev_id = other_rev_id
881
merger.other_basis = other_rev_id
882
return merger.do_merge()
885
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
886
"diff3": (Diff3Merger, "Merge using external diff3"),
887
'weave': (WeaveMerger, "Weave-based merge")