1
# Copyright (C) 2005 Canonical Ltd
1
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22
from fetch import greedy_fetch
25
import bzrlib.revision
26
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
27
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
28
from bzrlib.changeset import Inventory, Diff3Merge
29
from bzrlib.branch import find_branch
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
31
from bzrlib.errors import NoCommits
32
from bzrlib.delta import compare_trees
33
from bzrlib.trace import mutter, warning
34
from bzrlib.fetch import greedy_fetch
35
from bzrlib.revision import is_ancestor
37
# comments from abentley on irc: merge happens in two stages, each
38
# of which generates a changeset object
40
# stage 1: generate OLD->OTHER,
41
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
43
class MergeConflictHandler(ExceptionConflictHandler):
44
"""Handle conflicts encountered while merging.
46
This subclasses ExceptionConflictHandler, so that any types of
47
conflict that are not explicitly handled cause an exception and
50
def __init__(self, dir, ignore_zero=False):
51
ExceptionConflictHandler.__init__(self, dir)
53
self.ignore_zero = ignore_zero
55
def copy(self, source, dest):
56
"""Copy the text and mode of a file
57
:param source: The path of the file to copy
58
:param dest: The distination file to create
60
s_file = file(source, "rb")
61
d_file = file(dest, "wb")
64
os.chmod(dest, 0777 & os.stat(source).st_mode)
66
def dump(self, lines, 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
d_file = file(dest, "wb")
75
def add_suffix(self, name, suffix, last_new_name=None):
76
"""Rename a file to append a suffix. If the new name exists, the
77
suffix is added repeatedly until a non-existant name is found
79
:param name: The path of the file
80
:param suffix: The suffix to append
81
:param last_new_name: (used for recursive calls) the last name tried
83
if last_new_name is None:
85
new_name = last_new_name+suffix
87
os.rename(name, new_name)
90
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
92
return self.add_suffix(name, suffix, last_new_name=new_name)
94
def conflict(self, text):
99
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
101
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
102
main file will be a version with diff3 conflicts.
103
:param new_file: Path to the output file with diff3 markers
104
:param this_path: Path to the file text for the THIS tree
105
:param base_path: Path to the file text for the BASE tree
106
:param other_path: Path to the file text for the OTHER tree
108
self.add_suffix(this_path, ".THIS")
109
self.dump(base_lines, this_path+".BASE")
110
self.dump(other_lines, this_path+".OTHER")
111
os.rename(new_file, this_path)
112
self.conflict("Diff3 conflict encountered in %s" % this_path)
114
def new_contents_conflict(self, filename, other_contents):
115
"""Conflicting contents for newly added file."""
116
self.copy(other_contents, filename + ".OTHER")
117
self.conflict("Conflict in newly added file %s" % filename)
120
def target_exists(self, entry, target, old_path):
121
"""Handle the case when the target file or dir exists"""
122
moved_path = self.add_suffix(target, ".moved")
123
self.conflict("Moved existing %s to %s" % (target, moved_path))
125
def rmdir_non_empty(self, filename):
126
"""Handle the case where the dir to be removed still has contents"""
127
self.conflict("Directory %s not removed because it is not empty"\
132
if not self.ignore_zero:
133
print "%d conflicts encountered.\n" % self.conflicts
135
def get_tree(treespec, temp_root, label, local_branch=None):
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
136
56
location, revno = treespec
137
branch = find_branch(location)
141
revision = branch.last_patch()
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()
143
revision = branch.lookup_revision(revno)
144
return branch, get_revid_tree(branch, revision, temp_root, label,
147
def get_revid_tree(branch, revision, temp_root, label, local_branch):
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):
148
71
if revision is None:
149
base_tree = branch.working_tree()
72
base_tree = branch.bzrdir.open_workingtree()
151
74
if local_branch is not None:
152
greedy_fetch(local_branch, branch, revision)
153
base_tree = local_branch.revision_tree(revision)
75
if local_branch.base != branch.base:
76
local_branch.fetch(branch, revision)
77
base_tree = local_branch.repository.revision_tree(revision)
155
base_tree = branch.revision_tree(revision)
156
temp_path = os.path.join(temp_root, label)
158
return MergeTree(base_tree, temp_path)
161
def file_exists(tree, file_id):
162
return tree.has_filename(tree.id2path(file_id))
165
class MergeTree(object):
166
def __init__(self, tree, tempdir):
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()):
167
91
object.__init__(self)
168
if hasattr(tree, "basedir"):
169
self.root = tree.basedir
173
self.tempdir = tempdir
174
os.mkdir(os.path.join(self.tempdir, "texts"))
178
return self.tree.__iter__()
180
def __contains__(self, file_id):
181
return file_id in self.tree
183
def get_file(self, file_id):
184
return self.tree.get_file(file_id)
186
def get_file_sha1(self, id):
187
return self.tree.get_file_sha1(id)
189
def id2path(self, file_id):
190
return self.tree.id2path(file_id)
192
def has_id(self, file_id):
193
return self.tree.has_id(file_id)
195
def has_or_had_id(self, file_id):
196
if file_id == self.tree.inventory.root.file_id:
198
return self.tree.inventory.has_id(file_id)
200
def has_or_had_id(self, file_id):
201
if file_id == self.tree.inventory.root.file_id:
203
return self.tree.inventory.has_id(file_id)
205
def readonly_path(self, id):
206
if id not in self.tree:
208
if self.root is not None:
209
return self.tree.abspath(self.tree.id2path(id))
211
if self.tree.inventory[id].kind in ("directory", "root_directory"):
213
if not self.cached.has_key(id):
214
path = os.path.join(self.tempdir, "texts", id)
215
outfile = file(path, "wb")
216
outfile.write(self.tree.get_file(id).read())
217
assert(os.path.exists(path))
218
self.cached[id] = path
219
return self.cached[id]
223
def merge(other_revision, base_revision,
224
check_clean=True, ignore_zero=False,
225
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
227
"""Merge changes into a tree.
230
tuple(path, revision) Base for three-way merge.
232
tuple(path, revision) Other revision for three-way merge.
234
Directory to merge changes into; '.' by default.
236
If true, this_dir must have no uncommitted changes before the
238
all available ancestors of other_revision and base_revision are
239
automatically pulled into the branch.
241
from bzrlib.revision import common_ancestor, MultipleRevisionSources
242
from bzrlib.errors import NoSuchRevision
243
tempdir = tempfile.mkdtemp(prefix="bzr-")
247
this_branch = find_branch(this_dir)
248
this_rev_id = this_branch.last_patch()
249
if this_rev_id is None:
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:
250
144
raise BzrCommandError("This branch has no commits")
252
changes = compare_trees(this_branch.working_tree(),
253
this_branch.basis_tree(), False)
254
if changes.has_changed():
147
if self.this_basis != self.this_rev_id:
255
148
raise BzrCommandError("Working tree has uncommitted changes.")
256
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,
258
199
if other_revision[1] == -1:
259
other_rev_id = other_branch.last_patch()
260
if other_rev_id is None:
200
self.other_rev_id = other_branch.last_revision()
201
if self.other_rev_id is None:
261
202
raise NoCommits(other_branch)
262
other_basis = other_rev_id
203
self.other_basis = self.other_rev_id
263
204
elif other_revision[1] is not None:
264
other_rev_id = other_branch.lookup_revision(other_revision[1])
265
other_basis = other_rev_id
205
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
206
self.other_basis = self.other_rev_id
268
other_basis = other_branch.last_patch()
269
if other_basis is None:
208
self.other_rev_id = None
209
self.other_basis = other_branch.last_revision()
210
if self.other_basis is None:
270
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")
271
224
if base_revision == [None, None]:
273
base_rev_id = common_ancestor(this_rev_id, other_basis,
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,
275
234
except NoCommonAncestor:
276
235
raise UnrelatedBranches()
277
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
279
base_is_ancestor = True
236
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
238
self.base_is_ancestor = True
281
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
240
base_branch, self.base_tree = _get_tree(base_revision)
282
241
if base_revision[1] == -1:
283
base_rev_id = base_branch.last_patch()
242
self.base_rev_id = base_branch.last_revision()
284
243
elif base_revision[1] is None:
287
base_rev_id = base_branch.lookup_revision(base_revision[1])
288
if base_rev_id is not None:
289
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
290
MultipleRevisionSources(this_branch,
293
base_is_ancestor = False
294
if file_list is None:
295
interesting_ids = None
297
interesting_ids = set()
298
this_tree = this_branch.working_tree()
299
for fname in file_list:
300
path = this_branch.relpath(fname)
302
for tree in (this_tree, base_tree.tree, other_tree.tree):
303
file_id = tree.inventory.path2id(path)
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)"""
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)
304
771
if file_id is not None:
305
interesting_ids.add(file_id)
308
raise BzrCommandError("%s is not a source file in any"
310
merge_inner(this_branch, other_tree, base_tree, tempdir,
311
ignore_zero=ignore_zero, backup_files=backup_files,
312
merge_type=merge_type, interesting_ids=interesting_ids)
313
if base_is_ancestor and other_rev_id is not None\
314
and other_rev_id not in this_branch.revision_history():
315
this_branch.add_pending_merge(other_rev_id)
317
shutil.rmtree(tempdir)
320
def set_interesting(inventory_a, inventory_b, interesting_ids):
321
"""Mark files whose ids are in interesting_ids as interesting
323
for inventory in (inventory_a, inventory_b):
324
for path, source_file in inventory.iteritems():
325
source_file.interesting = source_file.id in interesting_ids
328
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
329
"""Generate a changeset. If interesting_ids is supplied, only changes
330
to those files will be shown. Metadata changes are stripped.
332
cset = generate_changeset(tree_a, tree_b, interesting_ids)
333
for entry in cset.entries.itervalues():
334
entry.metadata_change = None
338
def merge_inner(this_branch, other_tree, base_tree, tempdir,
339
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
340
interesting_ids=None):
342
def merge_factory(file_id, base, other):
343
contents_change = merge_type(file_id, base, other)
345
contents_change = BackupBeforeChange(contents_change)
346
return contents_change
348
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
350
def get_inventory(tree):
351
return tree.tree.inventory
353
inv_changes = merge_flex(this_tree, base_tree, other_tree,
354
generate_cset_optimized, get_inventory,
355
MergeConflictHandler(base_tree.root,
356
ignore_zero=ignore_zero),
357
merge_factory=merge_factory,
358
interesting_ids=interesting_ids)
361
for id, path in inv_changes.iteritems():
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)
366
assert path.startswith('.' + os.sep), "path is %s" % path
368
adjust_ids.append((path, id))
369
if len(adjust_ids) > 0:
370
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
374
def regen_inventory(this_branch, root, new_entries):
375
old_entries = this_branch.read_working_inventory()
379
for path, file_id in new_entries:
382
new_entries_map[file_id] = path
384
def id2path(file_id):
385
path = new_entries_map.get(file_id)
388
entry = old_entries[file_id]
389
if entry.parent_id is None:
391
return os.path.join(id2path(entry.parent_id), entry.name)
393
for file_id in old_entries:
394
entry = old_entries[file_id]
395
path = id2path(file_id)
396
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
397
by_path[path] = file_id
402
for path, file_id in new_entries:
404
del new_inventory[file_id]
407
new_path_list.append((path, file_id))
408
if file_id not in old_entries:
410
# Ensure no file is added before its parent
412
for path, file_id in new_path_list:
416
parent = by_path[os.path.dirname(path)]
417
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
418
new_inventory[file_id] = (path, file_id, parent, kind)
419
by_path[path] = file_id
421
# Get a list in insertion order
422
new_inventory_list = new_inventory.values()
423
mutter ("""Inventory regeneration:
424
old length: %i insertions: %i deletions: %i new_length: %i"""\
425
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
426
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
427
new_inventory_list.sort()
428
return new_inventory_list
430
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
431
"diff3": (Diff3Merge, "Merge using external diff3")
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)