1
from merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory, Diff3Merge
4
from bzrlib import find_branch
6
from bzrlib.errors import BzrCommandError
7
from bzrlib.delta 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):
26
from bzrlib.branch import Branch
27
from bzrlib.conflicts import ConflictList, Conflict
28
from bzrlib.errors import (BzrCommandError,
38
WorkingTreeNotRevision,
41
from bzrlib.merge3 import Merge3
42
from bzrlib.osutils import rename, pathjoin
43
from progress import DummyProgress, ProgressPhase
44
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
45
from bzrlib.textfile import check_text_lines
46
from bzrlib.trace import mutter, warning, note
47
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
48
FinalPaths, create_by_entry, unique_add,
50
from bzrlib.versionedfile import WeaveMerge
53
# TODO: Report back as changes are merged in
55
def _get_tree(treespec, local_branch=None):
56
from bzrlib import workingtree
105
57
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):
59
tree = workingtree.WorkingTree.open_containing(location)[0]
60
return tree.branch, tree
61
branch = Branch.open_containing(location)[0]
63
revision = branch.last_revision()
65
revision = branch.get_rev_id(revno)
67
revision = NULL_REVISION
68
return branch, _get_revid_tree(branch, revision, local_branch)
71
def _get_revid_tree(branch, revision, local_branch):
73
base_tree = branch.bzrdir.open_workingtree()
75
if local_branch is not None:
76
if local_branch.base != branch.base:
77
local_branch.fetch(branch, revision)
78
base_tree = local_branch.repository.revision_tree(revision)
80
base_tree = branch.repository.revision_tree(revision)
84
def transform_tree(from_tree, to_tree, interesting_ids=None):
85
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
86
interesting_ids=interesting_ids, this_tree=from_tree)
90
def __init__(self, this_branch, other_tree=None, base_tree=None,
91
this_tree=None, pb=DummyProgress(), change_reporter=None,
137
93
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,
168
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
170
"""Merge changes into a tree.
173
Base for three-way merge.
175
Other revision for three-way merge.
177
Directory to merge changes into; '.' by default.
179
If true, this_dir must have no uncommitted changes before the
182
tempdir = tempfile.mkdtemp(prefix="bzr-")
186
this_branch = find_branch(this_dir)
94
assert this_tree is not None, "this_tree is required"
95
self.this_branch = this_branch
96
self.this_basis = this_branch.last_revision()
97
self.this_rev_id = None
98
self.this_tree = this_tree
99
self.this_revision_tree = None
100
self.this_basis_tree = None
101
self.other_tree = other_tree
102
self.other_branch = None
103
self.base_tree = base_tree
104
self.ignore_zero = False
105
self.backup_files = False
106
self.interesting_ids = None
107
self.show_base = False
108
self.reprocess = False
111
self.recurse = recurse
112
self.change_reporter = change_reporter
114
def revision_tree(self, revision_id):
115
return self.this_branch.repository.revision_tree(revision_id)
117
def ensure_revision_trees(self):
118
if self.this_revision_tree is None:
119
self.this_basis_tree = self.this_branch.repository.revision_tree(
121
if self.this_basis == self.this_rev_id:
122
self.this_revision_tree = self.this_basis_tree
124
if self.other_rev_id is None:
125
other_basis_tree = self.revision_tree(self.other_basis)
126
changes = other_basis_tree.changes_from(self.other_tree)
127
if changes.has_changed():
128
raise WorkingTreeNotRevision(self.this_tree)
129
other_rev_id = self.other_basis
130
self.other_tree = other_basis_tree
132
def file_revisions(self, file_id):
133
self.ensure_revision_trees()
134
def get_id(tree, file_id):
135
revision_id = tree.inventory[file_id].revision
136
assert revision_id is not None
138
if self.this_rev_id is None:
139
if self.this_basis_tree.get_file_sha1(file_id) != \
140
self.this_tree.get_file_sha1(file_id):
141
raise WorkingTreeNotRevision(self.this_tree)
143
trees = (self.this_basis_tree, self.other_tree)
144
return [get_id(tree, file_id) for tree in trees]
146
def check_basis(self, check_clean, require_commits=True):
147
if self.this_basis is None and require_commits is True:
148
raise BzrCommandError("This branch has no commits."
149
" (perhaps you would prefer 'bzr pull')")
188
changes = compare_trees(this_branch.working_tree(),
189
this_branch.basis_tree(), False)
190
if changes.has_changed():
152
if self.this_basis != self.this_rev_id:
191
153
raise BzrCommandError("Working tree has uncommitted changes.")
192
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
155
def compare_basis(self):
156
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
157
if not changes.has_changed():
158
self.this_rev_id = self.this_basis
160
def set_interesting_files(self, file_list):
162
self._set_interesting_files(file_list)
163
except NotVersionedError, e:
164
raise BzrCommandError("%s is not a source file in any"
167
def _set_interesting_files(self, file_list):
168
"""Set the list of interesting ids from a list of files."""
169
if file_list is None:
170
self.interesting_ids = None
173
interesting_ids = set()
174
for path in file_list:
176
# TODO: jam 20070226 The trees are not locked at this time,
177
# wouldn't it make merge faster if it locks everything in the
178
# beginning? It locks at do_merge time, but this happens
180
for tree in (self.this_tree, self.base_tree, self.other_tree):
181
file_id = tree.path2id(path)
182
if file_id is not None:
183
interesting_ids.add(file_id)
186
raise NotVersionedError(path=path)
187
self.interesting_ids = interesting_ids
189
def set_pending(self):
190
if not self.base_is_ancestor:
192
if self.other_rev_id is None:
194
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
195
if self.other_rev_id in ancestry:
197
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
199
def set_other(self, other_revision):
200
"""Set the revision and tree to merge from.
202
This sets the other_tree, other_rev_id, other_basis attributes.
204
:param other_revision: The [path, revision] list to merge from.
206
self.other_branch, self.other_tree = _get_tree(other_revision,
208
if other_revision[1] == -1:
209
self.other_rev_id = self.other_branch.last_revision()
210
if self.other_rev_id is None:
211
raise NoCommits(self.other_branch)
212
self.other_basis = self.other_rev_id
213
elif other_revision[1] is not None:
214
self.other_rev_id = self.other_branch.get_rev_id(other_revision[1])
215
self.other_basis = self.other_rev_id
217
self.other_rev_id = None
218
self.other_basis = self.other_branch.last_revision()
219
if self.other_basis is None:
220
raise NoCommits(self.other_branch)
221
if self.other_branch.base != self.this_branch.base:
222
self.this_branch.fetch(self.other_branch,
223
last_revision=self.other_basis)
225
def set_other_revision(self, revision_id, other_branch):
226
"""Set 'other' based on a branch and revision id
228
:param revision_id: The revision to use for a tree
229
:param other_branch: The branch containing this tree
231
self.other_rev_id = revision_id
232
self.other_branch = other_branch
233
self.this_branch.fetch(other_branch, self.other_rev_id)
234
self.other_tree = self.revision_tree(revision_id)
235
self.other_basis = revision_id
238
self.set_base([None, None])
240
def set_base(self, base_revision):
241
"""Set the base revision to use for the merge.
243
:param base_revision: A 2-list containing a path and revision number.
245
mutter("doing merge() with no base_revision specified")
193
246
if base_revision == [None, None]:
194
if other_revision[1] == -1:
197
o_revno = other_revision[1]
198
base_revno = this_branch.common_ancestor(other_branch,
199
other_revno=o_revno)[0]
200
if base_revno is None:
248
pb = ui.ui_factory.nested_progress_bar()
250
this_repo = self.this_branch.repository
251
self.base_rev_id = common_ancestor(self.this_basis,
256
except NoCommonAncestor:
201
257
raise UnrelatedBranches()
202
base_revision = ['.', base_revno]
203
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
204
if file_list is None:
205
interesting_ids = None
207
interesting_ids = set()
208
this_tree = this_branch.working_tree()
209
for fname in file_list:
210
path = this_branch.relpath(fname)
212
for tree in (this_tree, base_tree.tree, other_tree.tree):
213
file_id = tree.inventory.path2id(path)
258
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
260
self.base_is_ancestor = True
262
base_branch, self.base_tree = _get_tree(base_revision)
263
if base_revision[1] == -1:
264
self.base_rev_id = base_branch.last_revision()
265
elif base_revision[1] is None:
266
self.base_rev_id = None
268
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
269
if self.this_branch.base != base_branch.base:
270
self.this_branch.fetch(base_branch)
271
self.base_is_ancestor = is_ancestor(self.this_basis,
276
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
277
'other_tree': self.other_tree,
278
'interesting_ids': self.interesting_ids,
280
if self.merge_type.requires_base:
281
kwargs['base_tree'] = self.base_tree
282
if self.merge_type.supports_reprocess:
283
kwargs['reprocess'] = self.reprocess
285
raise BzrError("Conflict reduction is not supported for merge"
286
" type %s." % self.merge_type)
287
if self.merge_type.supports_show_base:
288
kwargs['show_base'] = self.show_base
290
raise BzrError("Showing base is not supported for this"
291
" merge type. %s" % self.merge_type)
292
self.this_tree.lock_tree_write()
293
if self.base_tree is not None:
294
self.base_tree.lock_read()
295
if self.other_tree is not None:
296
self.other_tree.lock_read()
298
merge = self.merge_type(pb=self._pb,
299
change_reporter=self.change_reporter,
301
if self.recurse == 'down':
302
for path, file_id in self.this_tree.iter_references():
303
sub_tree = self.this_tree.get_nested_tree(file_id, path)
304
other_revision = self.other_tree.get_reference_revision(
306
if other_revision == sub_tree.last_revision():
308
sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
309
sub_merge.merge_type = self.merge_type
310
relpath = self.this_tree.relpath(path)
311
other_branch = self.other_branch.reference_parent(file_id, relpath)
312
sub_merge.set_other_revision(other_revision, other_branch)
313
base_revision = self.base_tree.get_reference_revision(file_id)
314
sub_merge.base_tree = \
315
sub_tree.branch.repository.revision_tree(base_revision)
319
if self.other_tree is not None:
320
self.other_tree.unlock()
321
if self.base_tree is not None:
322
self.base_tree.unlock()
323
self.this_tree.unlock()
324
if len(merge.cooked_conflicts) == 0:
325
if not self.ignore_zero:
326
note("All changes applied successfully.")
328
note("%d conflicts encountered." % len(merge.cooked_conflicts))
330
return len(merge.cooked_conflicts)
332
def regen_inventory(self, new_entries):
333
old_entries = self.this_tree.read_working_inventory()
337
for path, file_id in new_entries:
340
new_entries_map[file_id] = path
342
def id2path(file_id):
343
path = new_entries_map.get(file_id)
346
entry = old_entries[file_id]
347
if entry.parent_id is None:
349
return pathjoin(id2path(entry.parent_id), entry.name)
351
for file_id in old_entries:
352
entry = old_entries[file_id]
353
path = id2path(file_id)
354
if file_id in self.base_tree.inventory:
355
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
357
executable = getattr(entry, 'executable', False)
358
new_inventory[file_id] = (path, file_id, entry.parent_id,
359
entry.kind, executable)
361
by_path[path] = file_id
366
for path, file_id in new_entries:
368
del new_inventory[file_id]
371
new_path_list.append((path, file_id))
372
if file_id not in old_entries:
374
# Ensure no file is added before its parent
376
for path, file_id in new_path_list:
380
parent = by_path[os.path.dirname(path)]
381
abspath = pathjoin(self.this_tree.basedir, path)
382
kind = osutils.file_kind(abspath)
383
if file_id in self.base_tree.inventory:
384
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
387
new_inventory[file_id] = (path, file_id, parent, kind, executable)
388
by_path[path] = file_id
390
# Get a list in insertion order
391
new_inventory_list = new_inventory.values()
392
mutter ("""Inventory regeneration:
393
old length: %i insertions: %i deletions: %i new_length: %i"""\
394
% (len(old_entries), insertions, deletions,
395
len(new_inventory_list)))
396
assert len(new_inventory_list) == len(old_entries) + insertions\
398
new_inventory_list.sort()
399
return new_inventory_list
402
class Merge3Merger(object):
403
"""Three-way merger that uses the merge3 text merger"""
405
supports_reprocess = True
406
supports_show_base = True
407
history_based = False
409
def __init__(self, working_tree, this_tree, base_tree, other_tree,
410
interesting_ids=None, reprocess=False, show_base=False,
411
pb=DummyProgress(), pp=None, change_reporter=None):
412
"""Initialize the merger object and perform the merge."""
413
object.__init__(self)
414
self.this_tree = working_tree
415
self.this_tree.lock_tree_write()
416
self.base_tree = base_tree
417
self.base_tree.lock_read()
418
self.other_tree = other_tree
419
self.other_tree.lock_read()
420
self._raw_conflicts = []
421
self.cooked_conflicts = []
422
self.reprocess = reprocess
423
self.show_base = show_base
426
self.change_reporter = change_reporter
428
self.pp = ProgressPhase("Merge phase", 3, self.pb)
430
if interesting_ids is not None:
431
all_ids = interesting_ids
433
all_ids = set(base_tree)
434
all_ids.update(other_tree)
435
self.tt = TreeTransform(working_tree, self.pb)
438
child_pb = ui.ui_factory.nested_progress_bar()
440
for num, file_id in enumerate(all_ids):
441
child_pb.update('Preparing file merge', num, len(all_ids))
442
self.merge_names(file_id)
443
file_status = self.merge_contents(file_id)
444
self.merge_executable(file_id, file_status)
449
child_pb = ui.ui_factory.nested_progress_bar()
451
fs_conflicts = resolve_conflicts(self.tt, child_pb)
454
if change_reporter is not None:
455
from bzrlib import delta
456
delta.report_changes(self.tt._iter_changes(), change_reporter)
457
self.cook_conflicts(fs_conflicts)
458
for conflict in self.cooked_conflicts:
461
results = self.tt.apply()
462
self.write_modified(results)
464
working_tree.add_conflicts(self.cooked_conflicts)
465
except UnsupportedOperation:
469
self.other_tree.unlock()
470
self.base_tree.unlock()
471
self.this_tree.unlock()
476
self.tt.final_kind(self.tt.root)
478
self.tt.cancel_deletion(self.tt.root)
479
if self.tt.final_file_id(self.tt.root) is None:
480
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
482
if self.other_tree.inventory.root is None:
484
other_root_file_id = self.other_tree.inventory.root.file_id
485
other_root = self.tt.trans_id_file_id(other_root_file_id)
486
if other_root == self.tt.root:
489
self.tt.final_kind(other_root)
492
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
493
self.tt.cancel_creation(other_root)
494
self.tt.cancel_versioning(other_root)
496
def reparent_children(self, ie, target):
497
for thing, child in ie.children.iteritems():
498
trans_id = self.tt.trans_id_file_id(child.file_id)
499
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
501
def write_modified(self, results):
503
for path in results.modified_paths:
504
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
507
hash = self.this_tree.get_file_sha1(file_id)
510
modified_hashes[file_id] = hash
511
self.this_tree.set_merge_modified(modified_hashes)
514
def parent(entry, file_id):
515
"""Determine the parent for a file_id (used as a key method)"""
518
return entry.parent_id
521
def name(entry, file_id):
522
"""Determine the name for a file_id (used as a key method)"""
528
def contents_sha1(tree, file_id):
529
"""Determine the sha1 of the file contents (used as a key method)."""
530
if file_id not in tree:
532
return tree.get_file_sha1(file_id)
535
def executable(tree, file_id):
536
"""Determine the executability of a file-id (used as a key method)."""
537
if file_id not in tree:
539
if tree.kind(file_id) != "file":
541
return tree.is_executable(file_id)
544
def kind(tree, file_id):
545
"""Determine the kind of a file-id (used as a key method)."""
546
if file_id not in tree:
548
return tree.kind(file_id)
551
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
552
"""Do a three-way test on a scalar.
553
Return "this", "other" or "conflict", depending whether a value wins.
555
key_base = key(base_tree, file_id)
556
key_other = key(other_tree, file_id)
557
#if base == other, either they all agree, or only THIS has changed.
558
if key_base == key_other:
560
key_this = key(this_tree, file_id)
561
if key_this not in (key_base, key_other):
563
# "Ambiguous clean merge"
564
elif key_this == key_other:
567
assert key_this == key_base
570
def merge_names(self, file_id):
571
"""Perform a merge on file_id names and parents"""
573
if file_id in tree.inventory:
574
return tree.inventory[file_id]
577
this_entry = get_entry(self.this_tree)
578
other_entry = get_entry(self.other_tree)
579
base_entry = get_entry(self.base_tree)
580
name_winner = self.scalar_three_way(this_entry, base_entry,
581
other_entry, file_id, self.name)
582
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
583
other_entry, file_id,
585
if this_entry is None:
586
if name_winner == "this":
587
name_winner = "other"
588
if parent_id_winner == "this":
589
parent_id_winner = "other"
590
if name_winner == "this" and parent_id_winner == "this":
592
if name_winner == "conflict":
593
trans_id = self.tt.trans_id_file_id(file_id)
594
self._raw_conflicts.append(('name conflict', trans_id,
595
self.name(this_entry, file_id),
596
self.name(other_entry, file_id)))
597
if parent_id_winner == "conflict":
598
trans_id = self.tt.trans_id_file_id(file_id)
599
self._raw_conflicts.append(('parent conflict', trans_id,
600
self.parent(this_entry, file_id),
601
self.parent(other_entry, file_id)))
602
if other_entry is None:
603
# it doesn't matter whether the result was 'other' or
604
# 'conflict'-- if there's no 'other', we leave it alone.
606
# if we get here, name_winner and parent_winner are set to safe values.
607
winner_entry = {"this": this_entry, "other": other_entry,
608
"conflict": other_entry}
609
trans_id = self.tt.trans_id_file_id(file_id)
610
parent_id = winner_entry[parent_id_winner].parent_id
611
if parent_id is not None:
612
parent_trans_id = self.tt.trans_id_file_id(parent_id)
613
self.tt.adjust_path(winner_entry[name_winner].name,
614
parent_trans_id, trans_id)
616
def merge_contents(self, file_id):
617
"""Performa a merge on file_id contents."""
618
def contents_pair(tree):
619
if file_id not in tree:
621
kind = tree.kind(file_id)
623
contents = tree.get_file_sha1(file_id)
624
elif kind == "symlink":
625
contents = tree.get_symlink_target(file_id)
628
return kind, contents
630
def contents_conflict():
631
trans_id = self.tt.trans_id_file_id(file_id)
632
name = self.tt.final_name(trans_id)
633
parent_id = self.tt.final_parent(trans_id)
634
if file_id in self.this_tree.inventory:
635
self.tt.unversion_file(trans_id)
636
if file_id in self.this_tree:
637
self.tt.delete_contents(trans_id)
638
file_group = self._dump_conflicts(name, parent_id, file_id,
640
self._raw_conflicts.append(('contents conflict', file_group))
642
# See SPOT run. run, SPOT, run.
643
# So we're not QUITE repeating ourselves; we do tricky things with
645
base_pair = contents_pair(self.base_tree)
646
other_pair = contents_pair(self.other_tree)
647
if base_pair == other_pair:
648
# OTHER introduced no changes
650
this_pair = contents_pair(self.this_tree)
651
if this_pair == other_pair:
652
# THIS and OTHER introduced the same changes
655
trans_id = self.tt.trans_id_file_id(file_id)
656
if this_pair == base_pair:
657
# only OTHER introduced changes
658
if file_id in self.this_tree:
659
# Remove any existing contents
660
self.tt.delete_contents(trans_id)
661
if file_id in self.other_tree:
662
# OTHER changed the file
663
create_by_entry(self.tt,
664
self.other_tree.inventory[file_id],
665
self.other_tree, trans_id)
666
if file_id not in self.this_tree.inventory:
667
self.tt.version_file(file_id, trans_id)
669
elif file_id in self.this_tree.inventory:
670
# OTHER deleted the file
671
self.tt.unversion_file(trans_id)
673
#BOTH THIS and OTHER introduced changes; scalar conflict
674
elif this_pair[0] == "file" and other_pair[0] == "file":
675
# THIS and OTHER are both files, so text merge. Either
676
# BASE is a file, or both converted to files, so at least we
677
# have agreement that output should be a file.
679
self.text_merge(file_id, trans_id)
681
return contents_conflict()
682
if file_id not in self.this_tree.inventory:
683
self.tt.version_file(file_id, trans_id)
685
self.tt.tree_kind(trans_id)
686
self.tt.delete_contents(trans_id)
691
# Scalar conflict, can't text merge. Dump conflicts
692
return contents_conflict()
694
def get_lines(self, tree, file_id):
695
"""Return the lines in a file, or an empty list."""
697
return tree.get_file(file_id).readlines()
701
def text_merge(self, file_id, trans_id):
702
"""Perform a three-way text merge on a file_id"""
703
# it's possible that we got here with base as a different type.
704
# if so, we just want two-way text conflicts.
705
if file_id in self.base_tree and \
706
self.base_tree.kind(file_id) == "file":
707
base_lines = self.get_lines(self.base_tree, file_id)
710
other_lines = self.get_lines(self.other_tree, file_id)
711
this_lines = self.get_lines(self.this_tree, file_id)
712
m3 = Merge3(base_lines, this_lines, other_lines)
713
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
714
if self.show_base is True:
715
base_marker = '|' * 7
719
def iter_merge3(retval):
720
retval["text_conflicts"] = False
721
for line in m3.merge_lines(name_a = "TREE",
722
name_b = "MERGE-SOURCE",
723
name_base = "BASE-REVISION",
724
start_marker=start_marker,
725
base_marker=base_marker,
726
reprocess=self.reprocess):
727
if line.startswith(start_marker):
728
retval["text_conflicts"] = True
729
yield line.replace(start_marker, '<' * 7)
733
merge3_iterator = iter_merge3(retval)
734
self.tt.create_file(merge3_iterator, trans_id)
735
if retval["text_conflicts"] is True:
736
self._raw_conflicts.append(('text conflict', trans_id))
737
name = self.tt.final_name(trans_id)
738
parent_id = self.tt.final_parent(trans_id)
739
file_group = self._dump_conflicts(name, parent_id, file_id,
740
this_lines, base_lines,
742
file_group.append(trans_id)
744
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
745
base_lines=None, other_lines=None, set_version=False,
747
"""Emit conflict files.
748
If this_lines, base_lines, or other_lines are omitted, they will be
749
determined automatically. If set_version is true, the .OTHER, .THIS
750
or .BASE (in that order) will be created as versioned files.
752
data = [('OTHER', self.other_tree, other_lines),
753
('THIS', self.this_tree, this_lines)]
755
data.append(('BASE', self.base_tree, base_lines))
758
for suffix, tree, lines in data:
760
trans_id = self._conflict_file(name, parent_id, tree, file_id,
762
file_group.append(trans_id)
763
if set_version and not versioned:
764
self.tt.version_file(file_id, trans_id)
768
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
770
"""Emit a single conflict file."""
771
name = name + '.' + suffix
772
trans_id = self.tt.create_path(name, parent_id)
773
entry = tree.inventory[file_id]
774
create_by_entry(self.tt, entry, tree, trans_id, lines)
777
def merge_executable(self, file_id, file_status):
778
"""Perform a merge on the execute bit."""
779
if file_status == "deleted":
781
trans_id = self.tt.trans_id_file_id(file_id)
783
if self.tt.final_kind(trans_id) != "file":
787
winner = self.scalar_three_way(self.this_tree, self.base_tree,
788
self.other_tree, file_id,
790
if winner == "conflict":
791
# There must be a None in here, if we have a conflict, but we
792
# need executability since file status was not deleted.
793
if self.executable(self.other_tree, file_id) is None:
798
if file_status == "modified":
799
executability = self.this_tree.is_executable(file_id)
800
if executability is not None:
801
trans_id = self.tt.trans_id_file_id(file_id)
802
self.tt.set_executability(executability, trans_id)
804
assert winner == "other"
805
if file_id in self.other_tree:
806
executability = self.other_tree.is_executable(file_id)
807
elif file_id in self.this_tree:
808
executability = self.this_tree.is_executable(file_id)
809
elif file_id in self.base_tree:
810
executability = self.base_tree.is_executable(file_id)
811
if executability is not None:
812
trans_id = self.tt.trans_id_file_id(file_id)
813
self.tt.set_executability(executability, trans_id)
815
def cook_conflicts(self, fs_conflicts):
816
"""Convert all conflicts into a form that doesn't depend on trans_id"""
817
from conflicts import Conflict
819
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
820
fp = FinalPaths(self.tt)
821
for conflict in self._raw_conflicts:
822
conflict_type = conflict[0]
823
if conflict_type in ('name conflict', 'parent conflict'):
824
trans_id = conflict[1]
825
conflict_args = conflict[2:]
826
if trans_id not in name_conflicts:
827
name_conflicts[trans_id] = {}
828
unique_add(name_conflicts[trans_id], conflict_type,
830
if conflict_type == 'contents conflict':
831
for trans_id in conflict[1]:
832
file_id = self.tt.final_file_id(trans_id)
214
833
if file_id is not None:
215
interesting_ids.add(file_id)
218
raise BzrCommandError("%s is not a source file in any"
220
merge_inner(this_branch, other_tree, base_tree, tempdir,
221
ignore_zero=ignore_zero, backup_files=backup_files,
222
merge_type=merge_type, interesting_ids=interesting_ids)
224
shutil.rmtree(tempdir)
227
def set_interesting(inventory_a, inventory_b, interesting_ids):
228
"""Mark files whose ids are in interesting_ids as interesting
230
for inventory in (inventory_a, inventory_b):
231
for path, source_file in inventory.iteritems():
232
source_file.interesting = source_file.id in interesting_ids
235
def set_optimized(tree_a, tree_b, inventory_a, inventory_b):
236
"""Mark files that have changed texts as interesting
238
for file_id in tree_a.tree.inventory:
239
if file_id not in tree_b.tree.inventory:
241
entry_a = tree_a.tree.inventory[file_id]
242
entry_b = tree_b.tree.inventory[file_id]
243
if (entry_a.kind, entry_b.kind) != ("file", "file"):
245
if None in (entry_a.text_id, entry_b.text_id):
247
if entry_a.text_id != entry_b.text_id:
249
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
250
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
253
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
254
interesting_ids=None):
255
"""Generate a changeset, with preprocessing to select interesting files.
256
using the text_id to mark really-changed files.
257
This permits blazing comparisons when text_ids are present. It also
258
disables metadata comparison for files with identical texts.
260
if interesting_ids is None:
261
set_optimized(tree_a, tree_b, inventory_a, inventory_b)
263
set_interesting(inventory_a, inventory_b, interesting_ids)
264
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
265
for entry in cset.entries.itervalues():
266
entry.metadata_change = None
270
def merge_inner(this_branch, other_tree, base_tree, tempdir,
271
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
272
interesting_ids=None):
274
def merge_factory(base_file, other_file):
275
contents_change = merge_type(base_file, other_file)
277
contents_change = BackupBeforeChange(contents_change)
278
return contents_change
280
def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
281
return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
284
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
286
def get_inventory(tree):
287
return tree.inventory
289
inv_changes = merge_flex(this_tree, base_tree, other_tree,
290
generate_cset, get_inventory,
291
MergeConflictHandler(base_tree.root,
292
ignore_zero=ignore_zero),
293
merge_factory=merge_factory)
296
for id, path in inv_changes.iteritems():
835
path = fp.get_path(trans_id)
836
for suffix in ('.BASE', '.THIS', '.OTHER'):
837
if path.endswith(suffix):
838
path = path[:-len(suffix)]
840
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
841
self.cooked_conflicts.append(c)
842
if conflict_type == 'text conflict':
843
trans_id = conflict[1]
844
path = fp.get_path(trans_id)
845
file_id = self.tt.final_file_id(trans_id)
846
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
847
self.cooked_conflicts.append(c)
849
for trans_id, conflicts in name_conflicts.iteritems():
851
this_parent, other_parent = conflicts['parent conflict']
852
assert this_parent != other_parent
854
this_parent = other_parent = \
855
self.tt.final_file_id(self.tt.final_parent(trans_id))
857
this_name, other_name = conflicts['name conflict']
858
assert this_name != other_name
860
this_name = other_name = self.tt.final_name(trans_id)
861
other_path = fp.get_path(trans_id)
862
if this_parent is not None:
864
fp.get_path(self.tt.trans_id_file_id(this_parent))
865
this_path = pathjoin(this_parent_path, this_name)
301
assert path.startswith('./')
303
adjust_ids.append((path, id))
304
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
307
def regen_inventory(this_branch, root, new_entries):
308
old_entries = this_branch.read_working_inventory()
311
for file_id in old_entries:
312
entry = old_entries[file_id]
313
path = old_entries.id2path(file_id)
314
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
315
by_path[path] = file_id
320
for path, file_id in new_entries:
322
del new_inventory[file_id]
325
new_path_list.append((path, file_id))
326
if file_id not in old_entries:
328
# Ensure no file is added before its parent
330
for path, file_id in new_path_list:
334
parent = by_path[os.path.dirname(path)]
335
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
336
new_inventory[file_id] = (path, file_id, parent, kind)
337
by_path[path] = file_id
339
# Get a list in insertion order
340
new_inventory_list = new_inventory.values()
341
mutter ("""Inventory regeneration:
342
old length: %i insertions: %i deletions: %i new_length: %i"""\
343
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
344
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
345
new_inventory_list.sort()
346
return new_inventory_list
348
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
349
"diff3": (Diff3Merge, "Merge using external diff3")
867
this_path = "<deleted>"
868
file_id = self.tt.final_file_id(trans_id)
869
c = Conflict.factory('path conflict', path=this_path,
870
conflict_path=other_path, file_id=file_id)
871
self.cooked_conflicts.append(c)
872
self.cooked_conflicts.sort(key=Conflict.sort_key)
875
class WeaveMerger(Merge3Merger):
876
"""Three-way tree merger, text weave merger."""
877
supports_reprocess = True
878
supports_show_base = False
880
def __init__(self, working_tree, this_tree, base_tree, other_tree,
881
interesting_ids=None, pb=DummyProgress(), pp=None,
882
reprocess=False, change_reporter=None):
883
self.this_revision_tree = self._get_revision_tree(this_tree)
884
self.other_revision_tree = self._get_revision_tree(other_tree)
885
super(WeaveMerger, self).__init__(working_tree, this_tree,
886
base_tree, other_tree,
887
interesting_ids=interesting_ids,
888
pb=pb, pp=pp, reprocess=reprocess,
889
change_reporter=change_reporter)
891
def _get_revision_tree(self, tree):
892
"""Return a revision tree related to this tree.
893
If the tree is a WorkingTree, the basis will be returned.
895
if getattr(tree, 'get_weave', False) is False:
896
# If we have a WorkingTree, try using the basis
897
return tree.branch.basis_tree()
901
def _check_file(self, file_id):
902
"""Check that the revision tree's version of the file matches."""
903
for tree, rt in ((self.this_tree, self.this_revision_tree),
904
(self.other_tree, self.other_revision_tree)):
907
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
908
raise WorkingTreeNotRevision(self.this_tree)
910
def _merged_lines(self, file_id):
911
"""Generate the merged lines.
912
There is no distinction between lines that are meant to contain <<<<<<<
915
weave = self.this_revision_tree.get_weave(file_id)
916
this_revision_id = self.this_revision_tree.inventory[file_id].revision
917
other_revision_id = \
918
self.other_revision_tree.inventory[file_id].revision
919
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
920
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
921
return wm.merge_lines(self.reprocess)
923
def text_merge(self, file_id, trans_id):
924
"""Perform a (weave) text merge for a given file and file-id.
925
If conflicts are encountered, .THIS and .OTHER files will be emitted,
926
and a conflict will be noted.
928
self._check_file(file_id)
929
lines, conflicts = self._merged_lines(file_id)
931
# Note we're checking whether the OUTPUT is binary in this case,
932
# because we don't want to get into weave merge guts.
933
check_text_lines(lines)
934
self.tt.create_file(lines, trans_id)
936
self._raw_conflicts.append(('text conflict', trans_id))
937
name = self.tt.final_name(trans_id)
938
parent_id = self.tt.final_parent(trans_id)
939
file_group = self._dump_conflicts(name, parent_id, file_id,
941
file_group.append(trans_id)
944
class Diff3Merger(Merge3Merger):
945
"""Three-way merger using external diff3 for text merging"""
947
def dump_file(self, temp_dir, name, tree, file_id):
948
out_path = pathjoin(temp_dir, name)
949
out_file = open(out_path, "wb")
951
in_file = tree.get_file(file_id)
958
def text_merge(self, file_id, trans_id):
959
"""Perform a diff3 merge using a specified file-id and trans-id.
960
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
961
will be dumped, and a will be conflict noted.
964
temp_dir = osutils.mkdtemp(prefix="bzr-")
966
new_file = pathjoin(temp_dir, "new")
967
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
968
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
969
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
970
status = bzrlib.patch.diff3(new_file, this, base, other)
971
if status not in (0, 1):
972
raise BzrError("Unhandled diff3 exit code")
973
f = open(new_file, 'rb')
975
self.tt.create_file(f, trans_id)
979
name = self.tt.final_name(trans_id)
980
parent_id = self.tt.final_parent(trans_id)
981
self._dump_conflicts(name, parent_id, file_id)
982
self._raw_conflicts.append(('text conflict', trans_id))
984
osutils.rmtree(temp_dir)
987
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
989
merge_type=Merge3Merger,
990
interesting_ids=None,
994
interesting_files=None,
997
change_reporter=None):
998
"""Primary interface for merging.
1000
typical use is probably
1001
'merge_inner(branch, branch.get_revision_tree(other_revision),
1002
branch.get_revision_tree(base_revision))'
1004
if this_tree is None:
1005
warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
1006
"bzrlib version 0.8.",
1009
this_tree = this_branch.bzrdir.open_workingtree()
1010
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
1011
pb=pb, change_reporter=change_reporter)
1012
merger.backup_files = backup_files
1013
merger.merge_type = merge_type
1014
merger.interesting_ids = interesting_ids
1015
merger.ignore_zero = ignore_zero
1016
if interesting_files:
1017
assert not interesting_ids, ('Only supply interesting_ids'
1018
' or interesting_files')
1019
merger._set_interesting_files(interesting_files)
1020
merger.show_base = show_base
1021
merger.reprocess = reprocess
1022
merger.other_rev_id = other_rev_id
1023
merger.other_basis = other_rev_id
1024
return merger.do_merge()
1026
def get_merge_type_registry():
1027
"""Merge type registry is in bzrlib.option to avoid circular imports.
1029
This method provides a sanctioned way to retrieve it.
1031
from bzrlib import option
1032
return option._merge_type_registry