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
24
import bzrlib.revision
25
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
26
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
27
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
28
25
from bzrlib.branch import Branch
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
30
from bzrlib.errors import NoCommits
31
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning
33
from bzrlib.fetch import greedy_fetch, fetch
34
from bzrlib.revision import is_ancestor
35
from bzrlib.osutils import rename
36
from bzrlib.revision import common_ancestor, MultipleRevisionSources
37
from bzrlib.errors import NoSuchRevision
39
# TODO: build_working_dir can be built on something simpler than merge()
41
# FIXME: merge() parameters seem oriented towards the command line
43
# comments from abentley on irc: merge happens in two stages, each
44
# of which generates a changeset object
46
# stage 1: generate OLD->OTHER,
47
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
49
class MergeConflictHandler(ExceptionConflictHandler):
50
"""Handle conflicts encountered while merging.
52
This subclasses ExceptionConflictHandler, so that any types of
53
conflict that are not explicitly handled cause an exception and
56
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
57
ExceptionConflictHandler.__init__(self)
59
self.ignore_zero = ignore_zero
60
self.this_tree = this_tree
61
self.base_tree = base_tree
62
self.other_tree = other_tree
64
def copy(self, source, dest):
65
"""Copy the text and mode of a file
66
:param source: The path of the file to copy
67
:param dest: The distination file to create
69
s_file = file(source, "rb")
70
d_file = file(dest, "wb")
73
os.chmod(dest, 0777 & os.stat(source).st_mode)
75
def dump(self, lines, dest):
76
"""Copy the text and mode of a file
77
:param source: The path of the file to copy
78
:param dest: The distination file to create
80
d_file = file(dest, "wb")
84
def add_suffix(self, name, suffix, last_new_name=None):
85
"""Rename a file to append a suffix. If the new name exists, the
86
suffix is added repeatedly until a non-existant name is found
88
:param name: The path of the file
89
:param suffix: The suffix to append
90
:param last_new_name: (used for recursive calls) the last name tried
92
if last_new_name is None:
94
new_name = last_new_name+suffix
96
rename(name, new_name)
99
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
101
return self.add_suffix(name, suffix, last_new_name=new_name)
103
def conflict(self, text):
108
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
110
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
111
main file will be a version with diff3 conflicts.
112
:param new_file: Path to the output file with diff3 markers
113
:param this_path: Path to the file text for the THIS tree
114
:param base_path: Path to the file text for the BASE tree
115
:param other_path: Path to the file text for the OTHER tree
117
self.add_suffix(this_path, ".THIS")
118
self.dump(base_lines, this_path+".BASE")
119
self.dump(other_lines, this_path+".OTHER")
120
rename(new_file, this_path)
121
self.conflict("Diff3 conflict encountered in %s" % this_path)
123
def new_contents_conflict(self, filename, other_contents):
124
"""Conflicting contents for newly added file."""
125
self.copy(other_contents, filename + ".OTHER")
126
self.conflict("Conflict in newly added file %s" % filename)
129
def target_exists(self, entry, target, old_path):
130
"""Handle the case when the target file or dir exists"""
131
moved_path = self.add_suffix(target, ".moved")
132
self.conflict("Moved existing %s to %s" % (target, moved_path))
134
def rmdir_non_empty(self, filename):
135
"""Handle the case where the dir to be removed still has contents"""
136
self.conflict("Directory %s not removed because it is not empty"\
140
def rem_contents_conflict(self, filename, this_contents, base_contents):
141
base_contents(filename+".BASE", self, False)
142
this_contents(filename+".THIS", self, False)
143
self.conflict("Other branch deleted locally modified file %s" %
145
return ReplaceContents(this_contents, None)
147
def abs_this_path(self, file_id):
148
"""Return the absolute path for a file_id in the this tree."""
149
relpath = self.this_tree.id2path(file_id)
150
return self.this_tree.tree.abspath(relpath)
152
def add_missing_parents(self, file_id, tree):
153
"""If some of the parents for file_id are missing, add them."""
154
entry = tree.tree.inventory[file_id]
155
if entry.parent_id not in self.this_tree:
156
return self.create_all_missing(entry.parent_id, tree)
158
return self.abs_this_path(entry.parent_id)
160
def create_all_missing(self, file_id, tree):
161
"""Add contents for a file_id and all its parents to a tree."""
162
entry = tree.tree.inventory[file_id]
163
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
164
abspath = self.create_all_missing(entry.parent_id, tree)
166
abspath = self.abs_this_path(entry.parent_id)
167
entry_path = os.path.join(abspath, entry.name)
168
if not os.path.isdir(entry_path):
169
self.create(file_id, entry_path, tree)
172
def create(self, file_id, path, tree, reverse=False):
173
"""Uses tree data to create a filesystem object for the file_id"""
174
from merge_core import get_id_contents
175
get_id_contents(file_id, tree)(path, self, reverse)
177
def missing_for_merge(self, file_id, other_path):
178
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
179
self.conflict("Other branch modified locally deleted file %s" %
181
parent_dir = self.add_missing_parents(file_id, self.other_tree)
182
stem = os.path.join(parent_dir, os.path.basename(other_path))
183
self.create(file_id, stem+".OTHER", self.other_tree)
184
self.create(file_id, stem+".BASE", self.base_tree)
187
if not self.ignore_zero:
188
print "%d conflicts encountered.\n" % self.conflicts
190
def get_tree(treespec, temp_root, label, local_branch=None):
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
191
56
location, revno = treespec
192
branch = Branch.open_containing(location)
58
tree = workingtree.WorkingTree.open_containing(location)[0]
59
return tree.branch, tree
60
branch = Branch.open_containing(location)[0]
196
62
revision = branch.last_revision()
198
64
revision = branch.get_rev_id(revno)
199
return branch, get_revid_tree(branch, revision, temp_root, label,
202
def get_revid_tree(branch, revision, temp_root, label, local_branch):
66
revision = NULL_REVISION
67
return branch, _get_revid_tree(branch, revision, local_branch)
70
def _get_revid_tree(branch, revision, local_branch):
203
71
if revision is None:
204
base_tree = branch.working_tree()
72
base_tree = branch.bzrdir.open_workingtree()
206
74
if local_branch is not None:
207
greedy_fetch(local_branch, branch, revision)
208
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)
210
base_tree = branch.revision_tree(revision)
211
temp_path = os.path.join(temp_root, label)
213
return MergeTree(base_tree, temp_path)
216
def file_exists(tree, file_id):
217
return tree.has_filename(tree.id2path(file_id))
220
class MergeTree(object):
221
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()):
222
91
object.__init__(self)
223
if hasattr(tree, "basedir"):
224
self.root = tree.basedir
228
self.tempdir = tempdir
229
os.mkdir(os.path.join(self.tempdir, "texts"))
230
os.mkdir(os.path.join(self.tempdir, "symlinks"))
234
return self.tree.__iter__()
236
def __contains__(self, file_id):
237
return file_id in self.tree
239
def get_file(self, file_id):
240
return self.tree.get_file(file_id)
242
def get_file_sha1(self, id):
243
return self.tree.get_file_sha1(id)
245
def is_executable(self, id):
246
return self.tree.is_executable(id)
248
def id2path(self, file_id):
249
return self.tree.id2path(file_id)
251
def has_id(self, file_id):
252
return self.tree.has_id(file_id)
254
def has_or_had_id(self, file_id):
255
if file_id == self.tree.inventory.root.file_id:
257
return self.tree.inventory.has_id(file_id)
259
def has_or_had_id(self, file_id):
260
if file_id == self.tree.inventory.root.file_id:
262
return self.tree.inventory.has_id(file_id)
264
def readonly_path(self, id):
265
if id not in self.tree:
267
if self.root is not None:
268
return self.tree.abspath(self.tree.id2path(id))
270
kind = self.tree.inventory[id].kind
271
if kind in ("directory", "root_directory"):
273
if not self.cached.has_key(id):
275
path = os.path.join(self.tempdir, "texts", id)
276
outfile = file(path, "wb")
277
outfile.write(self.tree.get_file(id).read())
278
assert(bzrlib.osutils.lexists(path))
279
if self.tree.is_executable(id):
282
assert kind == "symlink"
283
path = os.path.join(self.tempdir, "symlinks", id)
284
target = self.tree.get_symlink_target(id)
285
os.symlink(target, path)
286
self.cached[id] = path
287
return self.cached[id]
290
def build_working_dir(to_dir):
291
"""Build a working directory in an empty directory.
293
to_dir is a directory containing branch metadata but no working files,
294
typically constructed by cloning an existing branch.
296
This is split out as a special idiomatic case of merge. It could
297
eventually be done by just building the tree directly calling into
298
lower-level code (e.g. constructing a changeset).
300
merge((to_dir, -1), (to_dir, 0), this_dir=to_dir,
301
check_clean=False, ignore_zero=True)
304
def merge(other_revision, base_revision,
305
check_clean=True, ignore_zero=False,
306
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
308
"""Merge changes into a tree.
311
tuple(path, revision) Base for three-way merge.
313
tuple(path, revision) Other revision for three-way merge.
315
Directory to merge changes into; '.' by default.
317
If true, this_dir must have no uncommitted changes before the
319
ignore_zero - If true, suppress the "zero conflicts" message when
320
there are no conflicts; should be set when doing something we expect
321
to complete perfectly.
323
All available ancestors of other_revision and base_revision are
324
automatically pulled into the branch.
326
tempdir = tempfile.mkdtemp(prefix="bzr-")
330
this_branch = Branch.open_containing(this_dir)
331
this_rev_id = this_branch.last_revision()
332
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:
333
144
raise BzrCommandError("This branch has no commits")
335
changes = compare_trees(this_branch.working_tree(),
336
this_branch.basis_tree(), False)
337
if changes.has_changed():
147
if self.this_basis != self.this_rev_id:
338
148
raise BzrCommandError("Working tree has uncommitted changes.")
339
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,
341
199
if other_revision[1] == -1:
342
other_rev_id = other_branch.last_revision()
343
if other_rev_id is None:
200
self.other_rev_id = other_branch.last_revision()
201
if self.other_rev_id is None:
344
202
raise NoCommits(other_branch)
345
other_basis = other_rev_id
203
self.other_basis = self.other_rev_id
346
204
elif other_revision[1] is not None:
347
other_rev_id = other_branch.get_rev_id(other_revision[1])
348
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
351
other_basis = other_branch.last_revision()
352
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:
353
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")
354
224
if base_revision == [None, None]:
356
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,
358
234
except NoCommonAncestor:
359
235
raise UnrelatedBranches()
360
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
362
base_is_ancestor = True
236
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
238
self.base_is_ancestor = True
364
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
240
base_branch, self.base_tree = _get_tree(base_revision)
365
241
if base_revision[1] == -1:
366
base_rev_id = base_branch.last_revision()
242
self.base_rev_id = base_branch.last_revision()
367
243
elif base_revision[1] is None:
370
base_rev_id = base_branch.get_rev_id(base_revision[1])
371
fetch(from_branch=base_branch, to_branch=this_branch)
372
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
374
if file_list is None:
375
interesting_ids = None
377
interesting_ids = set()
378
this_tree = this_branch.working_tree()
379
for fname in file_list:
380
path = this_branch.relpath(fname)
382
for tree in (this_tree, base_tree.tree, other_tree.tree):
383
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)
384
771
if file_id is not None:
385
interesting_ids.add(file_id)
388
raise BzrCommandError("%s is not a source file in any"
390
merge_inner(this_branch, other_tree, base_tree, tempdir,
391
ignore_zero=ignore_zero, backup_files=backup_files,
392
merge_type=merge_type, interesting_ids=interesting_ids)
393
if base_is_ancestor and other_rev_id is not None\
394
and other_rev_id not in this_branch.revision_history():
395
this_branch.add_pending_merge(other_rev_id)
397
shutil.rmtree(tempdir)
400
def set_interesting(inventory_a, inventory_b, interesting_ids):
401
"""Mark files whose ids are in interesting_ids as interesting
403
for inventory in (inventory_a, inventory_b):
404
for path, source_file in inventory.iteritems():
405
source_file.interesting = source_file.id in interesting_ids
408
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
409
"""Generate a changeset. If interesting_ids is supplied, only changes
410
to those files will be shown. Metadata changes are stripped.
412
cset = generate_changeset(tree_a, tree_b, interesting_ids)
413
for entry in cset.entries.itervalues():
414
entry.metadata_change = None
418
def merge_inner(this_branch, other_tree, base_tree, tempdir,
419
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
420
interesting_ids=None):
422
def merge_factory(file_id, base, other):
423
contents_change = merge_type(file_id, base, other)
425
contents_change = BackupBeforeChange(contents_change)
426
return contents_change
428
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
430
def get_inventory(tree):
431
return tree.tree.inventory
433
inv_changes = merge_flex(this_tree, base_tree, other_tree,
434
generate_cset_optimized, get_inventory,
435
MergeConflictHandler(this_tree, base_tree,
436
other_tree, ignore_zero=ignore_zero),
437
merge_factory=merge_factory,
438
interesting_ids=interesting_ids)
441
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)
446
assert path.startswith('.' + os.sep), "path is %s" % path
448
adjust_ids.append((path, id))
449
if len(adjust_ids) > 0:
450
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
454
def regen_inventory(this_branch, root, new_entries):
455
old_entries = this_branch.read_working_inventory()
459
for path, file_id in new_entries:
462
new_entries_map[file_id] = path
464
def id2path(file_id):
465
path = new_entries_map.get(file_id)
468
entry = old_entries[file_id]
469
if entry.parent_id is None:
471
return os.path.join(id2path(entry.parent_id), entry.name)
473
for file_id in old_entries:
474
entry = old_entries[file_id]
475
path = id2path(file_id)
476
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
477
by_path[path] = file_id
482
for path, file_id in new_entries:
484
del new_inventory[file_id]
487
new_path_list.append((path, file_id))
488
if file_id not in old_entries:
490
# Ensure no file is added before its parent
492
for path, file_id in new_path_list:
496
parent = by_path[os.path.dirname(path)]
497
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
498
new_inventory[file_id] = (path, file_id, parent, kind)
499
by_path[path] = file_id
501
# Get a list in insertion order
502
new_inventory_list = new_inventory.values()
503
mutter ("""Inventory regeneration:
504
old length: %i insertions: %i deletions: %i new_length: %i"""\
505
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
506
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
507
new_inventory_list.sort()
508
return new_inventory_list
510
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
511
"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)