1
from merge_core import merge_flex
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory
4
from bzrlib import find_branch
6
from bzrlib.errors import BzrCommandError
7
from bzrlib.diff import compare_trees
8
from trace import mutter, warning
1
# Copyright (C) 2005, 2006 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
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
21
class MergeConflictHandler(ExceptionConflictHandler):
22
"""Handle conflicts encountered while merging"""
23
def __init__(self, dir, ignore_zero=False):
24
ExceptionConflictHandler.__init__(self, dir)
26
self.ignore_zero = ignore_zero
28
def copy(self, source, dest):
29
"""Copy the text and mode of a file
30
:param source: The path of the file to copy
31
:param dest: The distination file to create
33
s_file = file(source, "rb")
34
d_file = file(dest, "wb")
37
os.chmod(dest, 0777 & os.stat(source).st_mode)
39
def add_suffix(self, name, suffix, last_new_name=None):
40
"""Rename a file to append a suffix. If the new name exists, the
41
suffix is added repeatedly until a non-existant name is found
43
:param name: The path of the file
44
:param suffix: The suffix to append
45
:param last_new_name: (used for recursive calls) the last name tried
47
if last_new_name is None:
49
new_name = last_new_name+suffix
51
os.rename(name, new_name)
54
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
56
return self.add_suffix(name, suffix, last_new_name=new_name)
58
def conflict(self, text):
63
def merge_conflict(self, new_file, this_path, base_path, other_path):
65
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
66
main file will be a version with diff3 conflicts.
67
:param new_file: Path to the output file with diff3 markers
68
:param this_path: Path to the file text for the THIS tree
69
:param base_path: Path to the file text for the BASE tree
70
:param other_path: Path to the file text for the OTHER tree
72
self.add_suffix(this_path, ".THIS")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
75
os.rename(new_file, this_path)
76
self.conflict("Diff3 conflict encountered in %s" % this_path)
78
def target_exists(self, entry, target, old_path):
79
"""Handle the case when the target file or dir exists"""
80
moved_path = self.add_suffix(target, ".moved")
81
self.conflict("Moved existing %s to %s" % (target, moved_path))
83
def rmdir_non_empty(self, filename):
84
"""Handle the case where the dir to be removed still has contents"""
85
self.conflict("Directory %s not removed because it is not empty"\
90
if not self.ignore_zero:
91
print "%d conflicts encountered.\n" % self.conflicts
93
class SourceFile(object):
94
def __init__(self, path, id, present=None, isdir=None):
97
self.present = present
99
self.interesting = True
102
return "SourceFile(%s, %s)" % (self.path, self.id)
104
def get_tree(treespec, temp_root, label):
25
from bzrlib.branch import Branch
26
from bzrlib.conflicts import ConflictList, Conflict
27
from bzrlib.errors import (BzrCommandError,
37
WorkingTreeNotRevision,
40
from bzrlib.merge3 import Merge3
41
from bzrlib.osutils import rename, pathjoin
42
from progress import DummyProgress, ProgressPhase
43
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
44
from bzrlib.textfile import check_text_lines
45
from bzrlib.trace import mutter, warning, note
46
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
47
FinalPaths, create_by_entry, unique_add,
49
from bzrlib.versionedfile import WeaveMerge
52
# TODO: Report back as changes are merged in
54
def _get_tree(treespec, local_branch=None):
55
from bzrlib import workingtree
105
56
location, revno = treespec
106
branch = find_branch(location)
108
base_tree = branch.working_tree()
110
base_tree = branch.basis_tree()
112
base_tree = branch.revision_tree(branch.lookup_revision(revno))
113
temp_path = os.path.join(temp_root, label)
115
return branch, MergeTree(base_tree, temp_path)
118
def abspath(tree, file_id):
119
path = tree.inventory.id2path(file_id)
124
def file_exists(tree, file_id):
125
return tree.has_filename(tree.id2path(file_id))
127
def inventory_map(tree):
129
for file_id in tree.inventory:
130
path = abspath(tree, file_id)
131
inventory[path] = SourceFile(path, file_id)
135
class MergeTree(object):
136
def __init__(self, tree, tempdir):
58
tree = workingtree.WorkingTree.open_containing(location)[0]
59
return tree.branch, tree
60
branch = Branch.open_containing(location)[0]
62
revision = branch.last_revision()
64
revision = branch.get_rev_id(revno)
66
revision = NULL_REVISION
67
return branch, _get_revid_tree(branch, revision, local_branch)
70
def _get_revid_tree(branch, revision, local_branch):
72
base_tree = branch.bzrdir.open_workingtree()
74
if local_branch is not None:
75
if local_branch.base != branch.base:
76
local_branch.fetch(branch, revision)
77
base_tree = local_branch.repository.revision_tree(revision)
79
base_tree = branch.repository.revision_tree(revision)
83
def transform_tree(from_tree, to_tree, interesting_ids=None):
84
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
85
interesting_ids=interesting_ids, this_tree=from_tree)
89
def __init__(self, this_branch, other_tree=None, base_tree=None,
90
this_tree=None, pb=DummyProgress()):
137
91
object.__init__(self)
138
if hasattr(tree, "basedir"):
139
self.root = tree.basedir
142
self.inventory = inventory_map(tree)
144
self.tempdir = tempdir
145
os.mkdir(os.path.join(self.tempdir, "texts"))
148
def readonly_path(self, id):
149
if id not in self.tree:
151
if self.root is not None:
152
return self.tree.abspath(self.tree.id2path(id))
154
if self.tree.inventory[id].kind in ("directory", "root_directory"):
156
if not self.cached.has_key(id):
157
path = os.path.join(self.tempdir, "texts", id)
158
outfile = file(path, "wb")
159
outfile.write(self.tree.get_file(id).read())
160
assert(os.path.exists(path))
161
self.cached[id] = path
162
return self.cached[id]
166
def merge(other_revision, base_revision,
167
check_clean=True, ignore_zero=False,
169
"""Merge changes into a tree.
172
Base for three-way merge.
174
Other revision for three-way merge.
176
Directory to merge changes into; '.' by default.
178
If true, this_dir must have no uncommitted changes before the
181
tempdir = tempfile.mkdtemp(prefix="bzr-")
185
this_branch = find_branch(this_dir)
92
assert this_tree is not None, "this_tree is required"
93
self.this_branch = this_branch
94
self.this_basis = this_branch.last_revision()
95
self.this_rev_id = None
96
self.this_tree = this_tree
97
self.this_revision_tree = None
98
self.this_basis_tree = None
99
self.other_tree = other_tree
100
self.base_tree = base_tree
101
self.ignore_zero = False
102
self.backup_files = False
103
self.interesting_ids = None
104
self.show_base = False
105
self.reprocess = False
110
def revision_tree(self, revision_id):
111
return self.this_branch.repository.revision_tree(revision_id)
113
def ensure_revision_trees(self):
114
if self.this_revision_tree is None:
115
self.this_basis_tree = self.this_branch.repository.revision_tree(
117
if self.this_basis == self.this_rev_id:
118
self.this_revision_tree = self.this_basis_tree
120
if self.other_rev_id is None:
121
other_basis_tree = self.revision_tree(self.other_basis)
122
changes = other_basis_tree.changes_from(self.other_tree)
123
if changes.has_changed():
124
raise WorkingTreeNotRevision(self.this_tree)
125
other_rev_id = self.other_basis
126
self.other_tree = other_basis_tree
128
def file_revisions(self, file_id):
129
self.ensure_revision_trees()
130
def get_id(tree, file_id):
131
revision_id = tree.inventory[file_id].revision
132
assert revision_id is not None
134
if self.this_rev_id is None:
135
if self.this_basis_tree.get_file_sha1(file_id) != \
136
self.this_tree.get_file_sha1(file_id):
137
raise WorkingTreeNotRevision(self.this_tree)
139
trees = (self.this_basis_tree, self.other_tree)
140
return [get_id(tree, file_id) for tree in trees]
142
def check_basis(self, check_clean, require_commits=True):
143
if self.this_basis is None and require_commits is True:
144
raise BzrCommandError("This branch has no commits")
187
changes = compare_trees(this_branch.working_tree(),
188
this_branch.basis_tree(), False)
189
if changes.has_changed():
147
if self.this_basis != self.this_rev_id:
190
148
raise BzrCommandError("Working tree has uncommitted changes.")
191
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
150
def compare_basis(self):
151
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
152
if not changes.has_changed():
153
self.this_rev_id = self.this_basis
155
def set_interesting_files(self, file_list):
157
self._set_interesting_files(file_list)
158
except NotVersionedError, e:
159
raise BzrCommandError("%s is not a source file in any"
162
def _set_interesting_files(self, file_list):
163
"""Set the list of interesting ids from a list of files."""
164
if file_list is None:
165
self.interesting_ids = None
168
interesting_ids = set()
169
for path in file_list:
171
for tree in (self.this_tree, self.base_tree, self.other_tree):
172
file_id = tree.inventory.path2id(path)
173
if file_id is not None:
174
interesting_ids.add(file_id)
177
raise NotVersionedError(path=path)
178
self.interesting_ids = interesting_ids
180
def set_pending(self):
181
if not self.base_is_ancestor:
183
if self.other_rev_id is None:
185
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
186
if self.other_rev_id in ancestry:
188
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
190
def set_other(self, other_revision):
191
"""Set the revision and tree to merge from.
193
This sets the other_tree, other_rev_id, other_basis attributes.
195
:param other_revision: The [path, revision] list to merge from.
197
other_branch, self.other_tree = _get_tree(other_revision,
199
if other_revision[1] == -1:
200
self.other_rev_id = other_branch.last_revision()
201
if self.other_rev_id is None:
202
raise NoCommits(other_branch)
203
self.other_basis = self.other_rev_id
204
elif other_revision[1] is not None:
205
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
206
self.other_basis = self.other_rev_id
208
self.other_rev_id = None
209
self.other_basis = other_branch.last_revision()
210
if self.other_basis is None:
211
raise NoCommits(other_branch)
212
if other_branch.base != self.this_branch.base:
213
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
216
self.set_base([None, None])
218
def set_base(self, base_revision):
219
"""Set the base revision to use for the merge.
221
:param base_revision: A 2-list containing a path and revision number.
223
mutter("doing merge() with no base_revision specified")
192
224
if base_revision == [None, None]:
193
if other_revision[1] == -1:
196
o_revno = other_revision[1]
197
base_revno = this_branch.common_ancestor(other_branch,
198
other_revno=o_revno)[0]
199
if base_revno is None:
226
pb = ui.ui_factory.nested_progress_bar()
228
this_repo = self.this_branch.repository
229
self.base_rev_id = common_ancestor(self.this_basis,
234
except NoCommonAncestor:
200
235
raise UnrelatedBranches()
201
base_revision = ['.', base_revno]
202
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
203
merge_inner(this_branch, other_tree, base_tree, tempdir,
204
ignore_zero=ignore_zero)
206
shutil.rmtree(tempdir)
209
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
210
"""Generate a changeset, using the text_id to mark really-changed files.
211
This permits blazing comparisons when text_ids are present. It also
212
disables metadata comparison for files with identical texts.
214
for file_id in tree_a.tree.inventory:
215
if file_id not in tree_b.tree.inventory:
217
entry_a = tree_a.tree.inventory[file_id]
218
entry_b = tree_b.tree.inventory[file_id]
219
if (entry_a.kind, entry_b.kind) != ("file", "file"):
221
if None in (entry_a.text_id, entry_b.text_id):
223
if entry_a.text_id != entry_b.text_id:
225
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
226
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
227
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
228
for entry in cset.entries.itervalues():
229
entry.metadata_change = None
233
def merge_inner(this_branch, other_tree, base_tree, tempdir,
235
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
237
def get_inventory(tree):
238
return tree.inventory
240
inv_changes = merge_flex(this_tree, base_tree, other_tree,
241
generate_cset_optimized, get_inventory,
242
MergeConflictHandler(base_tree.root,
243
ignore_zero=ignore_zero))
246
for id, path in inv_changes.iteritems():
251
assert path.startswith('./')
253
adjust_ids.append((path, id))
254
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
257
def regen_inventory(this_branch, root, new_entries):
258
old_entries = this_branch.read_working_inventory()
261
for file_id in old_entries:
262
entry = old_entries[file_id]
263
path = old_entries.id2path(file_id)
264
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
265
by_path[path] = file_id
236
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
238
self.base_is_ancestor = True
240
base_branch, self.base_tree = _get_tree(base_revision)
241
if base_revision[1] == -1:
242
self.base_rev_id = base_branch.last_revision()
243
elif base_revision[1] is None:
244
self.base_rev_id = None
246
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
247
if self.this_branch.base != base_branch.base:
248
self.this_branch.fetch(base_branch)
249
self.base_is_ancestor = is_ancestor(self.this_basis,
254
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
255
'other_tree': self.other_tree,
256
'interesting_ids': self.interesting_ids,
258
if self.merge_type.requires_base:
259
kwargs['base_tree'] = self.base_tree
260
if self.merge_type.supports_reprocess:
261
kwargs['reprocess'] = self.reprocess
263
raise BzrError("Conflict reduction is not supported for merge"
264
" type %s." % self.merge_type)
265
if self.merge_type.supports_show_base:
266
kwargs['show_base'] = self.show_base
268
raise BzrError("Showing base is not supported for this"
269
" merge type. %s" % self.merge_type)
270
merge = self.merge_type(pb=self._pb, **kwargs)
271
if len(merge.cooked_conflicts) == 0:
272
if not self.ignore_zero:
273
note("All changes applied successfully.")
275
note("%d conflicts encountered." % len(merge.cooked_conflicts))
277
return len(merge.cooked_conflicts)
279
def regen_inventory(self, new_entries):
280
old_entries = self.this_tree.read_working_inventory()
284
for path, file_id in new_entries:
287
new_entries_map[file_id] = path
289
def id2path(file_id):
290
path = new_entries_map.get(file_id)
293
entry = old_entries[file_id]
294
if entry.parent_id is None:
296
return pathjoin(id2path(entry.parent_id), entry.name)
298
for file_id in old_entries:
299
entry = old_entries[file_id]
300
path = id2path(file_id)
301
if file_id in self.base_tree.inventory:
302
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
304
executable = getattr(entry, 'executable', False)
305
new_inventory[file_id] = (path, file_id, entry.parent_id,
306
entry.kind, executable)
308
by_path[path] = file_id
313
for path, file_id in new_entries:
315
del new_inventory[file_id]
318
new_path_list.append((path, file_id))
319
if file_id not in old_entries:
321
# Ensure no file is added before its parent
323
for path, file_id in new_path_list:
327
parent = by_path[os.path.dirname(path)]
328
abspath = pathjoin(self.this_tree.basedir, path)
329
kind = osutils.file_kind(abspath)
330
if file_id in self.base_tree.inventory:
331
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
334
new_inventory[file_id] = (path, file_id, parent, kind, executable)
335
by_path[path] = file_id
337
# Get a list in insertion order
338
new_inventory_list = new_inventory.values()
339
mutter ("""Inventory regeneration:
340
old length: %i insertions: %i deletions: %i new_length: %i"""\
341
% (len(old_entries), insertions, deletions,
342
len(new_inventory_list)))
343
assert len(new_inventory_list) == len(old_entries) + insertions\
345
new_inventory_list.sort()
346
return new_inventory_list
349
class Merge3Merger(object):
350
"""Three-way merger that uses the merge3 text merger"""
352
supports_reprocess = True
353
supports_show_base = True
354
history_based = False
356
def __init__(self, working_tree, this_tree, base_tree, other_tree,
357
interesting_ids=None, reprocess=False, show_base=False,
358
pb=DummyProgress(), pp=None):
359
"""Initialize the merger object and perform the merge."""
360
object.__init__(self)
361
self.this_tree = working_tree
362
self.base_tree = base_tree
363
self.other_tree = other_tree
364
self._raw_conflicts = []
365
self.cooked_conflicts = []
366
self.reprocess = reprocess
367
self.show_base = show_base
371
self.pp = ProgressPhase("Merge phase", 3, self.pb)
373
if interesting_ids is not None:
374
all_ids = interesting_ids
376
all_ids = set(base_tree)
377
all_ids.update(other_tree)
378
working_tree.lock_tree_write()
379
self.tt = TreeTransform(working_tree, self.pb)
382
child_pb = ui.ui_factory.nested_progress_bar()
384
for num, file_id in enumerate(all_ids):
385
child_pb.update('Preparing file merge', num, len(all_ids))
386
self.merge_names(file_id)
387
file_status = self.merge_contents(file_id)
388
self.merge_executable(file_id, file_status)
393
child_pb = ui.ui_factory.nested_progress_bar()
395
fs_conflicts = resolve_conflicts(self.tt, child_pb)
398
self.cook_conflicts(fs_conflicts)
399
for conflict in self.cooked_conflicts:
402
results = self.tt.apply()
403
self.write_modified(results)
405
working_tree.add_conflicts(self.cooked_conflicts)
406
except UnsupportedOperation:
410
working_tree.unlock()
415
self.tt.final_kind(self.tt.root)
417
self.tt.cancel_deletion(self.tt.root)
418
if self.tt.final_file_id(self.tt.root) is None:
419
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
421
if self.other_tree.inventory.root is None:
423
other_root_file_id = self.other_tree.inventory.root.file_id
424
other_root = self.tt.trans_id_file_id(other_root_file_id)
425
if other_root == self.tt.root:
428
self.tt.final_kind(other_root)
431
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
432
self.tt.cancel_creation(other_root)
433
self.tt.cancel_versioning(other_root)
435
def reparent_children(self, ie, target):
436
for thing, child in ie.children.iteritems():
437
trans_id = self.tt.trans_id_file_id(child.file_id)
438
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
440
def write_modified(self, results):
442
for path in results.modified_paths:
443
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
446
hash = self.this_tree.get_file_sha1(file_id)
449
modified_hashes[file_id] = hash
450
self.this_tree.set_merge_modified(modified_hashes)
453
def parent(entry, file_id):
454
"""Determine the parent for a file_id (used as a key method)"""
457
return entry.parent_id
460
def name(entry, file_id):
461
"""Determine the name for a file_id (used as a key method)"""
270
for path, file_id in new_entries:
272
del new_inventory[file_id]
275
new_path_list.append((path, file_id))
276
if file_id not in old_entries:
278
# Ensure no file is added before its parent
280
for path, file_id in new_path_list:
284
parent = by_path[os.path.dirname(path)]
285
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
286
new_inventory[file_id] = (path, file_id, parent, kind)
287
by_path[path] = file_id
289
# Get a list in insertion order
290
new_inventory_list = new_inventory.values()
291
mutter ("""Inventory regeneration:
292
old length: %i insertions: %i deletions: %i new_length: %i"""\
293
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
294
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
295
new_inventory_list.sort()
296
return new_inventory_list
467
def contents_sha1(tree, file_id):
468
"""Determine the sha1 of the file contents (used as a key method)."""
469
if file_id not in tree:
471
return tree.get_file_sha1(file_id)
474
def executable(tree, file_id):
475
"""Determine the executability of a file-id (used as a key method)."""
476
if file_id not in tree:
478
if tree.kind(file_id) != "file":
480
return tree.is_executable(file_id)
483
def kind(tree, file_id):
484
"""Determine the kind of a file-id (used as a key method)."""
485
if file_id not in tree:
487
return tree.kind(file_id)
490
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
491
"""Do a three-way test on a scalar.
492
Return "this", "other" or "conflict", depending whether a value wins.
494
key_base = key(base_tree, file_id)
495
key_other = key(other_tree, file_id)
496
#if base == other, either they all agree, or only THIS has changed.
497
if key_base == key_other:
499
key_this = key(this_tree, file_id)
500
if key_this not in (key_base, key_other):
502
# "Ambiguous clean merge"
503
elif key_this == key_other:
506
assert key_this == key_base
509
def merge_names(self, file_id):
510
"""Perform a merge on file_id names and parents"""
512
if file_id in tree.inventory:
513
return tree.inventory[file_id]
516
this_entry = get_entry(self.this_tree)
517
other_entry = get_entry(self.other_tree)
518
base_entry = get_entry(self.base_tree)
519
name_winner = self.scalar_three_way(this_entry, base_entry,
520
other_entry, file_id, self.name)
521
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
522
other_entry, file_id,
524
if this_entry is None:
525
if name_winner == "this":
526
name_winner = "other"
527
if parent_id_winner == "this":
528
parent_id_winner = "other"
529
if name_winner == "this" and parent_id_winner == "this":
531
if name_winner == "conflict":
532
trans_id = self.tt.trans_id_file_id(file_id)
533
self._raw_conflicts.append(('name conflict', trans_id,
534
self.name(this_entry, file_id),
535
self.name(other_entry, file_id)))
536
if parent_id_winner == "conflict":
537
trans_id = self.tt.trans_id_file_id(file_id)
538
self._raw_conflicts.append(('parent conflict', trans_id,
539
self.parent(this_entry, file_id),
540
self.parent(other_entry, file_id)))
541
if other_entry is None:
542
# it doesn't matter whether the result was 'other' or
543
# 'conflict'-- if there's no 'other', we leave it alone.
545
# if we get here, name_winner and parent_winner are set to safe values.
546
winner_entry = {"this": this_entry, "other": other_entry,
547
"conflict": other_entry}
548
trans_id = self.tt.trans_id_file_id(file_id)
549
parent_id = winner_entry[parent_id_winner].parent_id
550
if parent_id is not None:
551
parent_trans_id = self.tt.trans_id_file_id(parent_id)
552
self.tt.adjust_path(winner_entry[name_winner].name,
553
parent_trans_id, trans_id)
555
def merge_contents(self, file_id):
556
"""Performa a merge on file_id contents."""
557
def contents_pair(tree):
558
if file_id not in tree:
560
kind = tree.kind(file_id)
562
contents = tree.get_file_sha1(file_id)
563
elif kind == "symlink":
564
contents = tree.get_symlink_target(file_id)
567
return kind, contents
569
def contents_conflict():
570
trans_id = self.tt.trans_id_file_id(file_id)
571
name = self.tt.final_name(trans_id)
572
parent_id = self.tt.final_parent(trans_id)
573
if file_id in self.this_tree.inventory:
574
self.tt.unversion_file(trans_id)
575
self.tt.delete_contents(trans_id)
576
file_group = self._dump_conflicts(name, parent_id, file_id,
578
self._raw_conflicts.append(('contents conflict', file_group))
580
# See SPOT run. run, SPOT, run.
581
# So we're not QUITE repeating ourselves; we do tricky things with
583
base_pair = contents_pair(self.base_tree)
584
other_pair = contents_pair(self.other_tree)
585
if base_pair == other_pair:
586
# OTHER introduced no changes
588
this_pair = contents_pair(self.this_tree)
589
if this_pair == other_pair:
590
# THIS and OTHER introduced the same changes
593
trans_id = self.tt.trans_id_file_id(file_id)
594
if this_pair == base_pair:
595
# only OTHER introduced changes
596
if file_id in self.this_tree:
597
# Remove any existing contents
598
self.tt.delete_contents(trans_id)
599
if file_id in self.other_tree:
600
# OTHER changed the file
601
create_by_entry(self.tt,
602
self.other_tree.inventory[file_id],
603
self.other_tree, trans_id)
604
if file_id not in self.this_tree.inventory:
605
self.tt.version_file(file_id, trans_id)
607
elif file_id in self.this_tree.inventory:
608
# OTHER deleted the file
609
self.tt.unversion_file(trans_id)
611
#BOTH THIS and OTHER introduced changes; scalar conflict
612
elif this_pair[0] == "file" and other_pair[0] == "file":
613
# THIS and OTHER are both files, so text merge. Either
614
# BASE is a file, or both converted to files, so at least we
615
# have agreement that output should be a file.
617
self.text_merge(file_id, trans_id)
619
return contents_conflict()
620
if file_id not in self.this_tree.inventory:
621
self.tt.version_file(file_id, trans_id)
623
self.tt.tree_kind(trans_id)
624
self.tt.delete_contents(trans_id)
629
# Scalar conflict, can't text merge. Dump conflicts
630
return contents_conflict()
632
def get_lines(self, tree, file_id):
633
"""Return the lines in a file, or an empty list."""
635
return tree.get_file(file_id).readlines()
639
def text_merge(self, file_id, trans_id):
640
"""Perform a three-way text merge on a file_id"""
641
# it's possible that we got here with base as a different type.
642
# if so, we just want two-way text conflicts.
643
if file_id in self.base_tree and \
644
self.base_tree.kind(file_id) == "file":
645
base_lines = self.get_lines(self.base_tree, file_id)
648
other_lines = self.get_lines(self.other_tree, file_id)
649
this_lines = self.get_lines(self.this_tree, file_id)
650
m3 = Merge3(base_lines, this_lines, other_lines)
651
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
652
if self.show_base is True:
653
base_marker = '|' * 7
657
def iter_merge3(retval):
658
retval["text_conflicts"] = False
659
for line in m3.merge_lines(name_a = "TREE",
660
name_b = "MERGE-SOURCE",
661
name_base = "BASE-REVISION",
662
start_marker=start_marker,
663
base_marker=base_marker,
664
reprocess=self.reprocess):
665
if line.startswith(start_marker):
666
retval["text_conflicts"] = True
667
yield line.replace(start_marker, '<' * 7)
671
merge3_iterator = iter_merge3(retval)
672
self.tt.create_file(merge3_iterator, trans_id)
673
if retval["text_conflicts"] is True:
674
self._raw_conflicts.append(('text conflict', trans_id))
675
name = self.tt.final_name(trans_id)
676
parent_id = self.tt.final_parent(trans_id)
677
file_group = self._dump_conflicts(name, parent_id, file_id,
678
this_lines, base_lines,
680
file_group.append(trans_id)
682
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
683
base_lines=None, other_lines=None, set_version=False,
685
"""Emit conflict files.
686
If this_lines, base_lines, or other_lines are omitted, they will be
687
determined automatically. If set_version is true, the .OTHER, .THIS
688
or .BASE (in that order) will be created as versioned files.
690
data = [('OTHER', self.other_tree, other_lines),
691
('THIS', self.this_tree, this_lines)]
693
data.append(('BASE', self.base_tree, base_lines))
696
for suffix, tree, lines in data:
698
trans_id = self._conflict_file(name, parent_id, tree, file_id,
700
file_group.append(trans_id)
701
if set_version and not versioned:
702
self.tt.version_file(file_id, trans_id)
706
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
708
"""Emit a single conflict file."""
709
name = name + '.' + suffix
710
trans_id = self.tt.create_path(name, parent_id)
711
entry = tree.inventory[file_id]
712
create_by_entry(self.tt, entry, tree, trans_id, lines)
715
def merge_executable(self, file_id, file_status):
716
"""Perform a merge on the execute bit."""
717
if file_status == "deleted":
719
trans_id = self.tt.trans_id_file_id(file_id)
721
if self.tt.final_kind(trans_id) != "file":
725
winner = self.scalar_three_way(self.this_tree, self.base_tree,
726
self.other_tree, file_id,
728
if winner == "conflict":
729
# There must be a None in here, if we have a conflict, but we
730
# need executability since file status was not deleted.
731
if self.executable(self.other_tree, file_id) is None:
736
if file_status == "modified":
737
executability = self.this_tree.is_executable(file_id)
738
if executability is not None:
739
trans_id = self.tt.trans_id_file_id(file_id)
740
self.tt.set_executability(executability, trans_id)
742
assert winner == "other"
743
if file_id in self.other_tree:
744
executability = self.other_tree.is_executable(file_id)
745
elif file_id in self.this_tree:
746
executability = self.this_tree.is_executable(file_id)
747
elif file_id in self.base_tree:
748
executability = self.base_tree.is_executable(file_id)
749
if executability is not None:
750
trans_id = self.tt.trans_id_file_id(file_id)
751
self.tt.set_executability(executability, trans_id)
753
def cook_conflicts(self, fs_conflicts):
754
"""Convert all conflicts into a form that doesn't depend on trans_id"""
755
from conflicts import Conflict
757
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
758
fp = FinalPaths(self.tt)
759
for conflict in self._raw_conflicts:
760
conflict_type = conflict[0]
761
if conflict_type in ('name conflict', 'parent conflict'):
762
trans_id = conflict[1]
763
conflict_args = conflict[2:]
764
if trans_id not in name_conflicts:
765
name_conflicts[trans_id] = {}
766
unique_add(name_conflicts[trans_id], conflict_type,
768
if conflict_type == 'contents conflict':
769
for trans_id in conflict[1]:
770
file_id = self.tt.final_file_id(trans_id)
771
if file_id is not None:
773
path = fp.get_path(trans_id)
774
for suffix in ('.BASE', '.THIS', '.OTHER'):
775
if path.endswith(suffix):
776
path = path[:-len(suffix)]
778
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
779
self.cooked_conflicts.append(c)
780
if conflict_type == 'text conflict':
781
trans_id = conflict[1]
782
path = fp.get_path(trans_id)
783
file_id = self.tt.final_file_id(trans_id)
784
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
785
self.cooked_conflicts.append(c)
787
for trans_id, conflicts in name_conflicts.iteritems():
789
this_parent, other_parent = conflicts['parent conflict']
790
assert this_parent != other_parent
792
this_parent = other_parent = \
793
self.tt.final_file_id(self.tt.final_parent(trans_id))
795
this_name, other_name = conflicts['name conflict']
796
assert this_name != other_name
798
this_name = other_name = self.tt.final_name(trans_id)
799
other_path = fp.get_path(trans_id)
800
if this_parent is not None:
802
fp.get_path(self.tt.trans_id_file_id(this_parent))
803
this_path = pathjoin(this_parent_path, this_name)
805
this_path = "<deleted>"
806
file_id = self.tt.final_file_id(trans_id)
807
c = Conflict.factory('path conflict', path=this_path,
808
conflict_path=other_path, file_id=file_id)
809
self.cooked_conflicts.append(c)
810
self.cooked_conflicts.sort(key=Conflict.sort_key)
813
class WeaveMerger(Merge3Merger):
814
"""Three-way tree merger, text weave merger."""
815
supports_reprocess = True
816
supports_show_base = False
818
def __init__(self, working_tree, this_tree, base_tree, other_tree,
819
interesting_ids=None, pb=DummyProgress(), pp=None,
821
self.this_revision_tree = self._get_revision_tree(this_tree)
822
self.other_revision_tree = self._get_revision_tree(other_tree)
823
super(WeaveMerger, self).__init__(working_tree, this_tree,
824
base_tree, other_tree,
825
interesting_ids=interesting_ids,
826
pb=pb, pp=pp, reprocess=reprocess)
828
def _get_revision_tree(self, tree):
829
"""Return a revision tree related to this tree.
830
If the tree is a WorkingTree, the basis will be returned.
832
if getattr(tree, 'get_weave', False) is False:
833
# If we have a WorkingTree, try using the basis
834
return tree.branch.basis_tree()
838
def _check_file(self, file_id):
839
"""Check that the revision tree's version of the file matches."""
840
for tree, rt in ((self.this_tree, self.this_revision_tree),
841
(self.other_tree, self.other_revision_tree)):
844
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
845
raise WorkingTreeNotRevision(self.this_tree)
847
def _merged_lines(self, file_id):
848
"""Generate the merged lines.
849
There is no distinction between lines that are meant to contain <<<<<<<
852
weave = self.this_revision_tree.get_weave(file_id)
853
this_revision_id = self.this_revision_tree.inventory[file_id].revision
854
other_revision_id = \
855
self.other_revision_tree.inventory[file_id].revision
856
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
857
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
858
return wm.merge_lines(self.reprocess)
860
def text_merge(self, file_id, trans_id):
861
"""Perform a (weave) text merge for a given file and file-id.
862
If conflicts are encountered, .THIS and .OTHER files will be emitted,
863
and a conflict will be noted.
865
self._check_file(file_id)
866
lines, conflicts = self._merged_lines(file_id)
868
# Note we're checking whether the OUTPUT is binary in this case,
869
# because we don't want to get into weave merge guts.
870
check_text_lines(lines)
871
self.tt.create_file(lines, trans_id)
873
self._raw_conflicts.append(('text conflict', trans_id))
874
name = self.tt.final_name(trans_id)
875
parent_id = self.tt.final_parent(trans_id)
876
file_group = self._dump_conflicts(name, parent_id, file_id,
878
file_group.append(trans_id)
881
class Diff3Merger(Merge3Merger):
882
"""Three-way merger using external diff3 for text merging"""
884
def dump_file(self, temp_dir, name, tree, file_id):
885
out_path = pathjoin(temp_dir, name)
886
out_file = open(out_path, "wb")
888
in_file = tree.get_file(file_id)
895
def text_merge(self, file_id, trans_id):
896
"""Perform a diff3 merge using a specified file-id and trans-id.
897
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
898
will be dumped, and a will be conflict noted.
901
temp_dir = osutils.mkdtemp(prefix="bzr-")
903
new_file = pathjoin(temp_dir, "new")
904
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
905
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
906
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
907
status = bzrlib.patch.diff3(new_file, this, base, other)
908
if status not in (0, 1):
909
raise BzrError("Unhandled diff3 exit code")
910
f = open(new_file, 'rb')
912
self.tt.create_file(f, trans_id)
916
name = self.tt.final_name(trans_id)
917
parent_id = self.tt.final_parent(trans_id)
918
self._dump_conflicts(name, parent_id, file_id)
919
self._raw_conflicts.append(('text conflict', trans_id))
921
osutils.rmtree(temp_dir)
924
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
926
merge_type=Merge3Merger,
927
interesting_ids=None,
931
interesting_files=None,
934
"""Primary interface for merging.
936
typical use is probably
937
'merge_inner(branch, branch.get_revision_tree(other_revision),
938
branch.get_revision_tree(base_revision))'
940
if this_tree is None:
941
warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
942
"bzrlib version 0.8.",
945
this_tree = this_branch.bzrdir.open_workingtree()
946
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
948
merger.backup_files = backup_files
949
merger.merge_type = merge_type
950
merger.interesting_ids = interesting_ids
951
merger.ignore_zero = ignore_zero
952
if interesting_files:
953
assert not interesting_ids, ('Only supply interesting_ids'
954
' or interesting_files')
955
merger._set_interesting_files(interesting_files)
956
merger.show_base = show_base
957
merger.reprocess = reprocess
958
merger.other_rev_id = other_rev_id
959
merger.other_basis = other_rev_id
960
return merger.do_merge()
963
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
964
"diff3": (Diff3Merger, "Merge using external diff3"),
965
'weave': (WeaveMerger, "Weave-based merge")
969
def merge_type_help():
970
templ = '%s%%7s: %%s' % (' '*12)
971
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
972
return '\n'.join(lines)