1
# Copyright (C) 2005, 2006 Canonical Ltd
1
# Copyright (C) 2005 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
20
from tempfile import mkdtemp
24
import bzrlib.revision
25
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
26
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
27
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
23
28
from bzrlib.branch import Branch
24
from bzrlib.conflicts import ConflictList, Conflict
25
from bzrlib.errors import (BzrCommandError,
35
WorkingTreeNotRevision,
38
from bzrlib.merge3 import Merge3
40
from bzrlib.osutils import rename, pathjoin, rmtree
41
from progress import DummyProgress, ProgressPhase
42
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
43
from bzrlib.textfile import check_text_lines
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
30
from bzrlib.errors import NoCommits
31
from bzrlib.delta import compare_trees
44
32
from bzrlib.trace import mutter, warning, note
45
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
46
FinalPaths, create_by_entry, unique_add)
47
from bzrlib.versionedfile import WeaveMerge
50
# TODO: Report back as changes are merged in
52
def _get_tree(treespec, local_branch=None):
53
from bzrlib import workingtree
33
from bzrlib.fetch import greedy_fetch, fetch
34
from bzrlib.revision import is_ancestor
35
from bzrlib.osutils import rename
36
from bzrlib.revision import common_ancestor, MultipleRevisionSources
37
from bzrlib.errors import NoSuchRevision
39
# TODO: build_working_dir can be built on something simpler than merge()
41
# FIXME: merge() parameters seem oriented towards the command line
42
# NOTABUG: merge is a helper for commandline functions. merge_inner is the
43
# the core functionality.
45
# comments from abentley on irc: merge happens in two stages, each
46
# of which generates a changeset object
48
# stage 1: generate OLD->OTHER,
49
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
51
class MergeConflictHandler(ExceptionConflictHandler):
52
"""Handle conflicts encountered while merging.
54
This subclasses ExceptionConflictHandler, so that any types of
55
conflict that are not explicitly handled cause an exception and
58
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
59
ExceptionConflictHandler.__init__(self)
61
self.ignore_zero = ignore_zero
62
self.this_tree = this_tree
63
self.base_tree = base_tree
64
self.other_tree = other_tree
66
def copy(self, source, dest):
67
"""Copy the text and mode of a file
68
:param source: The path of the file to copy
69
:param dest: The distination file to create
71
s_file = file(source, "rb")
72
d_file = file(dest, "wb")
75
os.chmod(dest, 0777 & os.stat(source).st_mode)
77
def dump(self, lines, dest):
78
"""Copy the text and mode of a file
79
:param source: The path of the file to copy
80
:param dest: The distination file to create
82
d_file = file(dest, "wb")
86
def add_suffix(self, name, suffix, last_new_name=None):
87
"""Rename a file to append a suffix. If the new name exists, the
88
suffix is added repeatedly until a non-existant name is found
90
:param name: The path of the file
91
:param suffix: The suffix to append
92
:param last_new_name: (used for recursive calls) the last name tried
94
if last_new_name is None:
96
new_name = last_new_name+suffix
98
rename(name, new_name)
101
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
103
return self.add_suffix(name, suffix, last_new_name=new_name)
105
def conflict(self, text):
110
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
112
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
113
main file will be a version with diff3 conflicts.
114
:param new_file: Path to the output file with diff3 markers
115
:param this_path: Path to the file text for the THIS tree
116
:param base_path: Path to the file text for the BASE tree
117
:param other_path: Path to the file text for the OTHER tree
119
self.add_suffix(this_path, ".THIS")
120
self.dump(base_lines, this_path+".BASE")
121
self.dump(other_lines, this_path+".OTHER")
122
rename(new_file, this_path)
123
self.conflict("Diff3 conflict encountered in %s" % this_path)
125
def new_contents_conflict(self, filename, other_contents):
126
"""Conflicting contents for newly added file."""
127
other.contents.apply(filename + ".OTHER")
128
self.conflict("Conflict in newly added file %s" % filename)
131
def target_exists(self, entry, target, old_path):
132
"""Handle the case when the target file or dir exists"""
133
moved_path = self.add_suffix(target, ".moved")
134
self.conflict("Moved existing %s to %s" % (target, moved_path))
136
def rmdir_non_empty(self, filename):
137
"""Handle the case where the dir to be removed still has contents"""
138
self.conflict("Directory %s not removed because it is not empty"\
142
def rem_contents_conflict(self, filename, this_contents, base_contents):
143
base_contents(filename+".BASE", self, False)
144
this_contents(filename+".THIS", self, False)
145
return ReplaceContents(this_contents, None)
147
def rem_contents_conflict(self, filename, this_contents, base_contents):
148
base_contents(filename+".BASE", self, False)
149
this_contents(filename+".THIS", self, False)
150
self.conflict("Other branch deleted locally modified file %s" %
152
return ReplaceContents(this_contents, None)
154
def abs_this_path(self, file_id):
155
"""Return the absolute path for a file_id in the this tree."""
156
relpath = self.this_tree.id2path(file_id)
157
return self.this_tree.tree.abspath(relpath)
159
def add_missing_parents(self, file_id, tree):
160
"""If some of the parents for file_id are missing, add them."""
161
entry = tree.tree.inventory[file_id]
162
if entry.parent_id not in self.this_tree:
163
return self.create_all_missing(entry.parent_id, tree)
165
return self.abs_this_path(entry.parent_id)
167
def create_all_missing(self, file_id, tree):
168
"""Add contents for a file_id and all its parents to a tree."""
169
entry = tree.tree.inventory[file_id]
170
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
171
abspath = self.create_all_missing(entry.parent_id, tree)
173
abspath = self.abs_this_path(entry.parent_id)
174
entry_path = os.path.join(abspath, entry.name)
175
if not os.path.isdir(entry_path):
176
self.create(file_id, entry_path, tree)
179
def create(self, file_id, path, tree, reverse=False):
180
"""Uses tree data to create a filesystem object for the file_id"""
181
from changeset import get_contents
182
get_contents(tree, file_id)(path, self, reverse)
184
def missing_for_merge(self, file_id, other_path):
185
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
186
self.conflict("Other branch modified locally deleted file %s" %
188
parent_dir = self.add_missing_parents(file_id, self.other_tree)
189
stem = os.path.join(parent_dir, os.path.basename(other_path))
190
self.create(file_id, stem+".OTHER", self.other_tree)
191
self.create(file_id, stem+".BASE", self.base_tree)
193
def threeway_contents_conflict(filename, this_contents, base_contents,
195
self.conflict("Three-way conflict merging %s" % filename)
198
if not self.ignore_zero:
199
note("%d conflicts encountered.\n" % self.conflicts)
201
def get_tree(treespec, temp_root, label, local_branch=None):
54
202
location, revno = treespec
203
branch = Branch.open_containing(location)
56
tree = workingtree.WorkingTree.open_containing(location)[0]
57
return tree.branch, tree
58
branch = Branch.open_containing(location)[0]
60
207
revision = branch.last_revision()
62
209
revision = branch.get_rev_id(revno)
64
revision = NULL_REVISION
65
return branch, _get_revid_tree(branch, revision, local_branch)
68
def _get_revid_tree(branch, revision, local_branch):
210
return branch, get_revid_tree(branch, revision, temp_root, label,
213
def get_revid_tree(branch, revision, temp_root, label, local_branch):
69
214
if revision is None:
70
base_tree = branch.bzrdir.open_workingtree()
215
base_tree = branch.working_tree()
72
217
if local_branch is not None:
73
if local_branch.base != branch.base:
74
local_branch.fetch(branch, revision)
75
base_tree = local_branch.repository.revision_tree(revision)
218
greedy_fetch(local_branch, branch, revision)
219
base_tree = local_branch.revision_tree(revision)
77
base_tree = branch.repository.revision_tree(revision)
81
def transform_tree(from_tree, to_tree, interesting_ids=None):
82
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
83
interesting_ids=interesting_ids, this_tree=from_tree)
87
def __init__(self, this_branch, other_tree=None, base_tree=None,
88
this_tree=None, pb=DummyProgress()):
221
base_tree = branch.revision_tree(revision)
222
temp_path = os.path.join(temp_root, label)
224
return MergeAdapterTree(base_tree, temp_path)
227
def file_exists(tree, file_id):
228
return tree.has_filename(tree.id2path(file_id))
231
class MergeAdapterTree(object):
232
"""MergeAdapterTree adapts a normal tree for merge_inner to use.
234
The interface the merge_inner needs is nearly but not quite
235
the same as that of bzrlib.tree.
238
def __init__(self, tree, tempdir):
89
239
object.__init__(self)
90
assert this_tree is not None, "this_tree is required"
91
self.this_branch = this_branch
92
self.this_basis = this_branch.last_revision()
93
self.this_rev_id = None
94
self.this_tree = this_tree
95
self.this_revision_tree = None
96
self.this_basis_tree = None
97
self.other_tree = other_tree
98
self.base_tree = base_tree
99
self.ignore_zero = False
100
self.backup_files = False
101
self.interesting_ids = None
102
self.show_base = False
103
self.reprocess = False
108
def revision_tree(self, revision_id):
109
return self.this_branch.repository.revision_tree(revision_id)
111
def ensure_revision_trees(self):
112
if self.this_revision_tree is None:
113
self.this_basis_tree = self.this_branch.repository.revision_tree(
115
if self.this_basis == self.this_rev_id:
116
self.this_revision_tree = self.this_basis_tree
118
if self.other_rev_id is None:
119
other_basis_tree = self.revision_tree(self.other_basis)
120
changes = other_basis_tree.changes_from(self.other_tree)
121
if changes.has_changed():
122
raise WorkingTreeNotRevision(self.this_tree)
123
other_rev_id = self.other_basis
124
self.other_tree = other_basis_tree
126
def file_revisions(self, file_id):
127
self.ensure_revision_trees()
128
def get_id(tree, file_id):
129
revision_id = tree.inventory[file_id].revision
130
assert revision_id is not None
132
if self.this_rev_id is None:
133
if self.this_basis_tree.get_file_sha1(file_id) != \
134
self.this_tree.get_file_sha1(file_id):
135
raise WorkingTreeNotRevision(self.this_tree)
137
trees = (self.this_basis_tree, self.other_tree)
138
return [get_id(tree, file_id) for tree in trees]
140
def check_basis(self, check_clean, require_commits=True):
141
if self.this_basis is None and require_commits is True:
240
if hasattr(tree, "basedir"):
241
self.root = tree.basedir
245
self.tempdir = tempdir
246
os.mkdir(os.path.join(self.tempdir, "texts"))
247
os.mkdir(os.path.join(self.tempdir, "symlinks"))
251
return self.tree.__iter__()
253
def __contains__(self, file_id):
254
return file_id in self.tree
256
def get_file(self, file_id):
257
return self.tree.get_file(file_id)
259
def get_file_sha1(self, id):
260
return self.tree.get_file_sha1(id)
262
def is_executable(self, id):
263
return self.tree.is_executable(id)
265
def id2path(self, file_id):
266
return self.tree.id2path(file_id)
268
def has_id(self, file_id):
269
return self.tree.has_id(file_id)
271
def has_or_had_id(self, file_id):
272
if file_id == self.tree.inventory.root.file_id:
274
return self.tree.inventory.has_id(file_id)
276
def kind(self, file_id):
277
return self.tree.kind(file_id)
279
def get_symlink_target(self, file_id):
280
return self.tree.get_symlink_target(file_id)
282
def id2abspath(self, file_id):
283
return self.tree.id2abspath(file_id)
286
def build_working_dir(to_dir):
287
"""Build a working directory in an empty directory.
289
to_dir is a directory containing branch metadata but no working files,
290
typically constructed by cloning an existing branch.
292
This is split out as a special idiomatic case of merge. It could
293
eventually be done by just building the tree directly calling into
294
lower-level code (e.g. constructing a changeset).
296
merge((to_dir, -1), (to_dir, 0), this_dir=to_dir,
297
check_clean=False, ignore_zero=True)
300
def merge(other_revision, base_revision,
301
check_clean=True, ignore_zero=False,
302
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
304
"""Merge changes into a tree.
307
tuple(path, revision) Base for three-way merge.
309
tuple(path, revision) Other revision for three-way merge.
311
Directory to merge changes into; '.' by default.
313
If true, this_dir must have no uncommitted changes before the
315
ignore_zero - If true, suppress the "zero conflicts" message when
316
there are no conflicts; should be set when doing something we expect
317
to complete perfectly.
319
All available ancestors of other_revision and base_revision are
320
automatically pulled into the branch.
322
tempdir = tempfile.mkdtemp(prefix="bzr-")
326
this_branch = Branch.open_containing(this_dir)
327
this_rev_id = this_branch.last_revision()
328
if this_rev_id is None:
142
329
raise BzrCommandError("This branch has no commits")
145
if self.this_basis != self.this_rev_id:
331
changes = compare_trees(this_branch.working_tree(),
332
this_branch.basis_tree(), False)
333
if changes.has_changed():
146
334
raise BzrCommandError("Working tree has uncommitted changes.")
148
def compare_basis(self):
149
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
150
if not changes.has_changed():
151
self.this_rev_id = self.this_basis
153
def set_interesting_files(self, file_list):
155
self._set_interesting_files(file_list)
156
except NotVersionedError, e:
157
raise BzrCommandError("%s is not a source file in any"
160
def _set_interesting_files(self, file_list):
161
"""Set the list of interesting ids from a list of files."""
162
if file_list is None:
163
self.interesting_ids = None
166
interesting_ids = set()
167
for path in file_list:
169
for tree in (self.this_tree, self.base_tree, self.other_tree):
170
file_id = tree.inventory.path2id(path)
171
if file_id is not None:
172
interesting_ids.add(file_id)
175
raise NotVersionedError(path=path)
176
self.interesting_ids = interesting_ids
178
def set_pending(self):
179
if not self.base_is_ancestor:
181
if self.other_rev_id is None:
183
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
184
if self.other_rev_id in ancestry:
186
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
188
def set_other(self, other_revision):
189
"""Set the revision and tree to merge from.
191
This sets the other_tree, other_rev_id, other_basis attributes.
193
:param other_revision: The [path, revision] list to merge from.
195
other_branch, self.other_tree = _get_tree(other_revision,
335
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
197
337
if other_revision[1] == -1:
198
self.other_rev_id = other_branch.last_revision()
199
if self.other_rev_id is None:
338
other_rev_id = other_branch.last_revision()
339
if other_rev_id is None:
200
340
raise NoCommits(other_branch)
201
self.other_basis = self.other_rev_id
341
other_basis = other_rev_id
202
342
elif other_revision[1] is not None:
203
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
204
self.other_basis = self.other_rev_id
343
other_rev_id = other_branch.get_rev_id(other_revision[1])
344
other_basis = other_rev_id
206
self.other_rev_id = None
207
self.other_basis = other_branch.last_revision()
208
if self.other_basis is None:
347
other_basis = other_branch.last_revision()
348
if other_basis is None:
209
349
raise NoCommits(other_branch)
210
if other_branch.base != self.this_branch.base:
211
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
214
self.set_base([None, None])
216
def set_base(self, base_revision):
217
"""Set the base revision to use for the merge.
219
:param base_revision: A 2-list containing a path and revision number.
221
mutter("doing merge() with no base_revision specified")
222
350
if base_revision == [None, None]:
224
pb = bzrlib.ui.ui_factory.nested_progress_bar()
226
this_repo = self.this_branch.repository
227
self.base_rev_id = common_ancestor(self.this_basis,
352
base_rev_id = common_ancestor(this_rev_id, other_basis,
232
354
except NoCommonAncestor:
233
355
raise UnrelatedBranches()
234
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
236
self.base_is_ancestor = True
356
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
358
base_is_ancestor = True
238
base_branch, self.base_tree = _get_tree(base_revision)
360
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
239
361
if base_revision[1] == -1:
240
self.base_rev_id = base_branch.last_revision()
362
base_rev_id = base_branch.last_revision()
241
363
elif base_revision[1] is None:
242
self.base_rev_id = None
244
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
245
if self.this_branch.base != base_branch.base:
246
self.this_branch.fetch(base_branch)
247
self.base_is_ancestor = is_ancestor(self.this_basis,
252
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
253
'other_tree': self.other_tree,
254
'interesting_ids': self.interesting_ids,
256
if self.merge_type.requires_base:
257
kwargs['base_tree'] = self.base_tree
258
if self.merge_type.supports_reprocess:
259
kwargs['reprocess'] = self.reprocess
261
raise BzrError("Conflict reduction is not supported for merge"
262
" type %s." % self.merge_type)
263
if self.merge_type.supports_show_base:
264
kwargs['show_base'] = self.show_base
266
raise BzrError("Showing base is not supported for this"
267
" merge type. %s" % self.merge_type)
268
merge = self.merge_type(pb=self._pb, **kwargs)
269
if len(merge.cooked_conflicts) == 0:
270
if not self.ignore_zero:
271
note("All changes applied successfully.")
273
note("%d conflicts encountered." % len(merge.cooked_conflicts))
275
return len(merge.cooked_conflicts)
277
def regen_inventory(self, new_entries):
278
old_entries = self.this_tree.read_working_inventory()
282
for path, file_id in new_entries:
285
new_entries_map[file_id] = path
287
def id2path(file_id):
288
path = new_entries_map.get(file_id)
291
entry = old_entries[file_id]
292
if entry.parent_id is None:
294
return pathjoin(id2path(entry.parent_id), entry.name)
296
for file_id in old_entries:
297
entry = old_entries[file_id]
298
path = id2path(file_id)
299
if file_id in self.base_tree.inventory:
300
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
302
executable = getattr(entry, 'executable', False)
303
new_inventory[file_id] = (path, file_id, entry.parent_id,
304
entry.kind, executable)
306
by_path[path] = file_id
311
for path, file_id in new_entries:
313
del new_inventory[file_id]
316
new_path_list.append((path, file_id))
317
if file_id not in old_entries:
319
# Ensure no file is added before its parent
321
for path, file_id in new_path_list:
325
parent = by_path[os.path.dirname(path)]
326
abspath = pathjoin(self.this_tree.basedir, path)
327
kind = bzrlib.osutils.file_kind(abspath)
328
if file_id in self.base_tree.inventory:
329
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
332
new_inventory[file_id] = (path, file_id, parent, kind, executable)
333
by_path[path] = file_id
335
# Get a list in insertion order
336
new_inventory_list = new_inventory.values()
337
mutter ("""Inventory regeneration:
338
old length: %i insertions: %i deletions: %i new_length: %i"""\
339
% (len(old_entries), insertions, deletions,
340
len(new_inventory_list)))
341
assert len(new_inventory_list) == len(old_entries) + insertions\
343
new_inventory_list.sort()
344
return new_inventory_list
347
class Merge3Merger(object):
348
"""Three-way merger that uses the merge3 text merger"""
350
supports_reprocess = True
351
supports_show_base = True
352
history_based = False
354
def __init__(self, working_tree, this_tree, base_tree, other_tree,
355
interesting_ids=None, reprocess=False, show_base=False,
356
pb=DummyProgress(), pp=None):
357
"""Initialize the merger object and perform the merge."""
358
object.__init__(self)
359
self.this_tree = working_tree
360
self.base_tree = base_tree
361
self.other_tree = other_tree
362
self._raw_conflicts = []
363
self.cooked_conflicts = []
364
self.reprocess = reprocess
365
self.show_base = show_base
369
self.pp = ProgressPhase("Merge phase", 3, self.pb)
371
if interesting_ids is not None:
372
all_ids = interesting_ids
374
all_ids = set(base_tree)
375
all_ids.update(other_tree)
376
working_tree.lock_write()
377
self.tt = TreeTransform(working_tree, self.pb)
380
child_pb = ui.ui_factory.nested_progress_bar()
382
for num, file_id in enumerate(all_ids):
383
child_pb.update('Preparing file merge', num, len(all_ids))
384
self.merge_names(file_id)
385
file_status = self.merge_contents(file_id)
386
self.merge_executable(file_id, file_status)
391
child_pb = ui.ui_factory.nested_progress_bar()
393
fs_conflicts = resolve_conflicts(self.tt, child_pb)
396
self.cook_conflicts(fs_conflicts)
397
for conflict in self.cooked_conflicts:
400
results = self.tt.apply()
401
self.write_modified(results)
403
working_tree.add_conflicts(self.cooked_conflicts)
404
except UnsupportedOperation:
408
working_tree.unlock()
411
def write_modified(self, results):
413
for path in results.modified_paths:
414
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
417
hash = self.this_tree.get_file_sha1(file_id)
420
modified_hashes[file_id] = hash
421
self.this_tree.set_merge_modified(modified_hashes)
424
def parent(entry, file_id):
425
"""Determine the parent for a file_id (used as a key method)"""
428
return entry.parent_id
431
def name(entry, file_id):
432
"""Determine the name for a file_id (used as a key method)"""
438
def contents_sha1(tree, file_id):
439
"""Determine the sha1 of the file contents (used as a key method)."""
440
if file_id not in tree:
442
return tree.get_file_sha1(file_id)
445
def executable(tree, file_id):
446
"""Determine the executability of a file-id (used as a key method)."""
447
if file_id not in tree:
449
if tree.kind(file_id) != "file":
451
return tree.is_executable(file_id)
454
def kind(tree, file_id):
455
"""Determine the kind of a file-id (used as a key method)."""
456
if file_id not in tree:
458
return tree.kind(file_id)
461
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
462
"""Do a three-way test on a scalar.
463
Return "this", "other" or "conflict", depending whether a value wins.
465
key_base = key(base_tree, file_id)
466
key_other = key(other_tree, file_id)
467
#if base == other, either they all agree, or only THIS has changed.
468
if key_base == key_other:
470
key_this = key(this_tree, file_id)
471
if key_this not in (key_base, key_other):
473
# "Ambiguous clean merge"
474
elif key_this == key_other:
477
assert key_this == key_base
480
def merge_names(self, file_id):
481
"""Perform a merge on file_id names and parents"""
483
if file_id in tree.inventory:
484
return tree.inventory[file_id]
487
this_entry = get_entry(self.this_tree)
488
other_entry = get_entry(self.other_tree)
489
base_entry = get_entry(self.base_tree)
490
name_winner = self.scalar_three_way(this_entry, base_entry,
491
other_entry, file_id, self.name)
492
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
493
other_entry, file_id,
495
if this_entry is None:
496
if name_winner == "this":
497
name_winner = "other"
498
if parent_id_winner == "this":
499
parent_id_winner = "other"
500
if name_winner == "this" and parent_id_winner == "this":
502
if name_winner == "conflict":
503
trans_id = self.tt.trans_id_file_id(file_id)
504
self._raw_conflicts.append(('name conflict', trans_id,
505
self.name(this_entry, file_id),
506
self.name(other_entry, file_id)))
507
if parent_id_winner == "conflict":
508
trans_id = self.tt.trans_id_file_id(file_id)
509
self._raw_conflicts.append(('parent conflict', trans_id,
510
self.parent(this_entry, file_id),
511
self.parent(other_entry, file_id)))
512
if other_entry is None:
513
# it doesn't matter whether the result was 'other' or
514
# 'conflict'-- if there's no 'other', we leave it alone.
516
# if we get here, name_winner and parent_winner are set to safe values.
517
winner_entry = {"this": this_entry, "other": other_entry,
518
"conflict": other_entry}
519
trans_id = self.tt.trans_id_file_id(file_id)
520
parent_id = winner_entry[parent_id_winner].parent_id
521
parent_trans_id = self.tt.trans_id_file_id(parent_id)
522
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
525
def merge_contents(self, file_id):
526
"""Performa a merge on file_id contents."""
527
def contents_pair(tree):
528
if file_id not in tree:
530
kind = tree.kind(file_id)
532
contents = tree.get_file_sha1(file_id)
533
elif kind == "symlink":
534
contents = tree.get_symlink_target(file_id)
537
return kind, contents
539
def contents_conflict():
540
trans_id = self.tt.trans_id_file_id(file_id)
541
name = self.tt.final_name(trans_id)
542
parent_id = self.tt.final_parent(trans_id)
543
if file_id in self.this_tree.inventory:
544
self.tt.unversion_file(trans_id)
545
self.tt.delete_contents(trans_id)
546
file_group = self._dump_conflicts(name, parent_id, file_id,
548
self._raw_conflicts.append(('contents conflict', file_group))
550
# See SPOT run. run, SPOT, run.
551
# So we're not QUITE repeating ourselves; we do tricky things with
553
base_pair = contents_pair(self.base_tree)
554
other_pair = contents_pair(self.other_tree)
555
if base_pair == other_pair:
556
# OTHER introduced no changes
558
this_pair = contents_pair(self.this_tree)
559
if this_pair == other_pair:
560
# THIS and OTHER introduced the same changes
563
trans_id = self.tt.trans_id_file_id(file_id)
564
if this_pair == base_pair:
565
# only OTHER introduced changes
566
if file_id in self.this_tree:
567
# Remove any existing contents
568
self.tt.delete_contents(trans_id)
569
if file_id in self.other_tree:
570
# OTHER changed the file
571
create_by_entry(self.tt,
572
self.other_tree.inventory[file_id],
573
self.other_tree, trans_id)
574
if file_id not in self.this_tree.inventory:
575
self.tt.version_file(file_id, trans_id)
577
elif file_id in self.this_tree.inventory:
578
# OTHER deleted the file
579
self.tt.unversion_file(trans_id)
581
#BOTH THIS and OTHER introduced changes; scalar conflict
582
elif this_pair[0] == "file" and other_pair[0] == "file":
583
# THIS and OTHER are both files, so text merge. Either
584
# BASE is a file, or both converted to files, so at least we
585
# have agreement that output should be a file.
587
self.text_merge(file_id, trans_id)
589
return contents_conflict()
590
if file_id not in self.this_tree.inventory:
591
self.tt.version_file(file_id, trans_id)
593
self.tt.tree_kind(trans_id)
594
self.tt.delete_contents(trans_id)
599
# Scalar conflict, can't text merge. Dump conflicts
600
return contents_conflict()
602
def get_lines(self, tree, file_id):
603
"""Return the lines in a file, or an empty list."""
605
return tree.get_file(file_id).readlines()
609
def text_merge(self, file_id, trans_id):
610
"""Perform a three-way text merge on a file_id"""
611
# it's possible that we got here with base as a different type.
612
# if so, we just want two-way text conflicts.
613
if file_id in self.base_tree and \
614
self.base_tree.kind(file_id) == "file":
615
base_lines = self.get_lines(self.base_tree, file_id)
618
other_lines = self.get_lines(self.other_tree, file_id)
619
this_lines = self.get_lines(self.this_tree, file_id)
620
m3 = Merge3(base_lines, this_lines, other_lines)
621
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
622
if self.show_base is True:
623
base_marker = '|' * 7
627
def iter_merge3(retval):
628
retval["text_conflicts"] = False
629
for line in m3.merge_lines(name_a = "TREE",
630
name_b = "MERGE-SOURCE",
631
name_base = "BASE-REVISION",
632
start_marker=start_marker,
633
base_marker=base_marker,
634
reprocess=self.reprocess):
635
if line.startswith(start_marker):
636
retval["text_conflicts"] = True
637
yield line.replace(start_marker, '<' * 7)
641
merge3_iterator = iter_merge3(retval)
642
self.tt.create_file(merge3_iterator, trans_id)
643
if retval["text_conflicts"] is True:
644
self._raw_conflicts.append(('text conflict', trans_id))
645
name = self.tt.final_name(trans_id)
646
parent_id = self.tt.final_parent(trans_id)
647
file_group = self._dump_conflicts(name, parent_id, file_id,
648
this_lines, base_lines,
650
file_group.append(trans_id)
652
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
653
base_lines=None, other_lines=None, set_version=False,
655
"""Emit conflict files.
656
If this_lines, base_lines, or other_lines are omitted, they will be
657
determined automatically. If set_version is true, the .OTHER, .THIS
658
or .BASE (in that order) will be created as versioned files.
660
data = [('OTHER', self.other_tree, other_lines),
661
('THIS', self.this_tree, this_lines)]
663
data.append(('BASE', self.base_tree, base_lines))
666
for suffix, tree, lines in data:
668
trans_id = self._conflict_file(name, parent_id, tree, file_id,
670
file_group.append(trans_id)
671
if set_version and not versioned:
672
self.tt.version_file(file_id, trans_id)
676
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
678
"""Emit a single conflict file."""
679
name = name + '.' + suffix
680
trans_id = self.tt.create_path(name, parent_id)
681
entry = tree.inventory[file_id]
682
create_by_entry(self.tt, entry, tree, trans_id, lines)
685
def merge_executable(self, file_id, file_status):
686
"""Perform a merge on the execute bit."""
687
if file_status == "deleted":
689
trans_id = self.tt.trans_id_file_id(file_id)
691
if self.tt.final_kind(trans_id) != "file":
695
winner = self.scalar_three_way(self.this_tree, self.base_tree,
696
self.other_tree, file_id,
698
if winner == "conflict":
699
# There must be a None in here, if we have a conflict, but we
700
# need executability since file status was not deleted.
701
if self.executable(self.other_tree, file_id) is None:
706
if file_status == "modified":
707
executability = self.this_tree.is_executable(file_id)
708
if executability is not None:
709
trans_id = self.tt.trans_id_file_id(file_id)
710
self.tt.set_executability(executability, trans_id)
712
assert winner == "other"
713
if file_id in self.other_tree:
714
executability = self.other_tree.is_executable(file_id)
715
elif file_id in self.this_tree:
716
executability = self.this_tree.is_executable(file_id)
717
elif file_id in self.base_tree:
718
executability = self.base_tree.is_executable(file_id)
719
if executability is not None:
720
trans_id = self.tt.trans_id_file_id(file_id)
721
self.tt.set_executability(executability, trans_id)
723
def cook_conflicts(self, fs_conflicts):
724
"""Convert all conflicts into a form that doesn't depend on trans_id"""
725
from conflicts import Conflict
727
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
728
fp = FinalPaths(self.tt)
729
for conflict in self._raw_conflicts:
730
conflict_type = conflict[0]
731
if conflict_type in ('name conflict', 'parent conflict'):
732
trans_id = conflict[1]
733
conflict_args = conflict[2:]
734
if trans_id not in name_conflicts:
735
name_conflicts[trans_id] = {}
736
unique_add(name_conflicts[trans_id], conflict_type,
738
if conflict_type == 'contents conflict':
739
for trans_id in conflict[1]:
740
file_id = self.tt.final_file_id(trans_id)
366
base_rev_id = base_branch.get_rev_id(base_revision[1])
367
fetch(from_branch=base_branch, to_branch=this_branch)
368
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
370
if file_list is None:
371
interesting_ids = None
373
interesting_ids = set()
374
this_tree = this_branch.working_tree()
375
for fname in file_list:
376
path = this_branch.relpath(fname)
378
for tree in (this_tree, base_tree.tree, other_tree.tree):
379
file_id = tree.inventory.path2id(path)
741
380
if file_id is not None:
743
path = fp.get_path(trans_id)
744
for suffix in ('.BASE', '.THIS', '.OTHER'):
745
if path.endswith(suffix):
746
path = path[:-len(suffix)]
748
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
749
self.cooked_conflicts.append(c)
750
if conflict_type == 'text conflict':
751
trans_id = conflict[1]
752
path = fp.get_path(trans_id)
753
file_id = self.tt.final_file_id(trans_id)
754
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
755
self.cooked_conflicts.append(c)
757
for trans_id, conflicts in name_conflicts.iteritems():
759
this_parent, other_parent = conflicts['parent conflict']
760
assert this_parent != other_parent
762
this_parent = other_parent = \
763
self.tt.final_file_id(self.tt.final_parent(trans_id))
765
this_name, other_name = conflicts['name conflict']
766
assert this_name != other_name
768
this_name = other_name = self.tt.final_name(trans_id)
769
other_path = fp.get_path(trans_id)
770
if this_parent is not None:
772
fp.get_path(self.tt.trans_id_file_id(this_parent))
773
this_path = pathjoin(this_parent_path, this_name)
381
interesting_ids.add(file_id)
384
raise BzrCommandError("%s is not a source file in any"
386
merge_inner(this_branch, other_tree, base_tree, tempdir,
387
ignore_zero=ignore_zero, backup_files=backup_files,
388
merge_type=merge_type, interesting_ids=interesting_ids)
389
if base_is_ancestor and other_rev_id is not None\
390
and other_rev_id not in this_branch.revision_history():
391
this_branch.add_pending_merge(other_rev_id)
393
shutil.rmtree(tempdir)
396
def set_interesting(inventory_a, inventory_b, interesting_ids):
397
"""Mark files whose ids are in interesting_ids as interesting
399
for inventory in (inventory_a, inventory_b):
400
for path, source_file in inventory.iteritems():
401
source_file.interesting = source_file.id in interesting_ids
404
def merge_inner(this_branch, other_tree, base_tree, tempdir,
405
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
406
interesting_ids=None):
408
def merge_factory(file_id, base, other):
409
contents_change = merge_type(file_id, base, other)
411
contents_change = BackupBeforeChange(contents_change)
412
return contents_change
414
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
416
def get_inventory(tree):
417
return tree.tree.inventory
419
inv_changes = merge_flex(this_tree, base_tree, other_tree,
420
generate_changeset, get_inventory,
421
MergeConflictHandler(this_tree, base_tree,
422
other_tree, ignore_zero=ignore_zero),
423
merge_factory=merge_factory,
424
interesting_ids=interesting_ids)
427
for id, path in inv_changes.iteritems():
775
this_path = "<deleted>"
776
file_id = self.tt.final_file_id(trans_id)
777
c = Conflict.factory('path conflict', path=this_path,
778
conflict_path=other_path, file_id=file_id)
779
self.cooked_conflicts.append(c)
780
self.cooked_conflicts.sort(key=Conflict.sort_key)
783
class WeaveMerger(Merge3Merger):
784
"""Three-way tree merger, text weave merger."""
785
supports_reprocess = True
786
supports_show_base = False
788
def __init__(self, working_tree, this_tree, base_tree, other_tree,
789
interesting_ids=None, pb=DummyProgress(), pp=None,
791
self.this_revision_tree = self._get_revision_tree(this_tree)
792
self.other_revision_tree = self._get_revision_tree(other_tree)
793
super(WeaveMerger, self).__init__(working_tree, this_tree,
794
base_tree, other_tree,
795
interesting_ids=interesting_ids,
796
pb=pb, pp=pp, reprocess=reprocess)
798
def _get_revision_tree(self, tree):
799
"""Return a revision tree related to this tree.
800
If the tree is a WorkingTree, the basis will be returned.
802
if getattr(tree, 'get_weave', False) is False:
803
# If we have a WorkingTree, try using the basis
804
return tree.branch.basis_tree()
808
def _check_file(self, file_id):
809
"""Check that the revision tree's version of the file matches."""
810
for tree, rt in ((self.this_tree, self.this_revision_tree),
811
(self.other_tree, self.other_revision_tree)):
814
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
815
raise WorkingTreeNotRevision(self.this_tree)
817
def _merged_lines(self, file_id):
818
"""Generate the merged lines.
819
There is no distinction between lines that are meant to contain <<<<<<<
822
weave = self.this_revision_tree.get_weave(file_id)
823
this_revision_id = self.this_revision_tree.inventory[file_id].revision
824
other_revision_id = \
825
self.other_revision_tree.inventory[file_id].revision
826
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
827
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
828
return wm.merge_lines(self.reprocess)
830
def text_merge(self, file_id, trans_id):
831
"""Perform a (weave) text merge for a given file and file-id.
832
If conflicts are encountered, .THIS and .OTHER files will be emitted,
833
and a conflict will be noted.
835
self._check_file(file_id)
836
lines, conflicts = self._merged_lines(file_id)
838
# Note we're checking whether the OUTPUT is binary in this case,
839
# because we don't want to get into weave merge guts.
840
check_text_lines(lines)
841
self.tt.create_file(lines, trans_id)
843
self._raw_conflicts.append(('text conflict', trans_id))
844
name = self.tt.final_name(trans_id)
845
parent_id = self.tt.final_parent(trans_id)
846
file_group = self._dump_conflicts(name, parent_id, file_id,
848
file_group.append(trans_id)
851
class Diff3Merger(Merge3Merger):
852
"""Three-way merger using external diff3 for text merging"""
854
def dump_file(self, temp_dir, name, tree, file_id):
855
out_path = pathjoin(temp_dir, name)
856
out_file = open(out_path, "wb")
858
in_file = tree.get_file(file_id)
865
def text_merge(self, file_id, trans_id):
866
"""Perform a diff3 merge using a specified file-id and trans-id.
867
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
868
will be dumped, and a will be conflict noted.
871
temp_dir = mkdtemp(prefix="bzr-")
873
new_file = pathjoin(temp_dir, "new")
874
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
875
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
876
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
877
status = bzrlib.patch.diff3(new_file, this, base, other)
878
if status not in (0, 1):
879
raise BzrError("Unhandled diff3 exit code")
880
f = open(new_file, 'rb')
882
self.tt.create_file(f, trans_id)
886
name = self.tt.final_name(trans_id)
887
parent_id = self.tt.final_parent(trans_id)
888
self._dump_conflicts(name, parent_id, file_id)
889
self._raw_conflicts.append(('text conflict', trans_id))
894
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
896
merge_type=Merge3Merger,
897
interesting_ids=None,
901
interesting_files=None,
904
"""Primary interface for merging.
906
typical use is probably
907
'merge_inner(branch, branch.get_revision_tree(other_revision),
908
branch.get_revision_tree(base_revision))'
910
if this_tree is None:
911
warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
912
"bzrlib version 0.8.",
915
this_tree = this_branch.bzrdir.open_workingtree()
916
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
918
merger.backup_files = backup_files
919
merger.merge_type = merge_type
920
merger.interesting_ids = interesting_ids
921
merger.ignore_zero = ignore_zero
922
if interesting_files:
923
assert not interesting_ids, ('Only supply interesting_ids'
924
' or interesting_files')
925
merger._set_interesting_files(interesting_files)
926
merger.show_base = show_base
927
merger.reprocess = reprocess
928
merger.other_rev_id = other_rev_id
929
merger.other_basis = other_rev_id
930
return merger.do_merge()
933
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
934
"diff3": (Diff3Merger, "Merge using external diff3"),
935
'weave': (WeaveMerger, "Weave-based merge")
432
assert path.startswith('.' + os.sep), "path is %s" % path
434
adjust_ids.append((path, id))
435
if len(adjust_ids) > 0:
436
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
440
def regen_inventory(this_branch, root, new_entries):
441
old_entries = this_branch.read_working_inventory()
445
for path, file_id in new_entries:
448
new_entries_map[file_id] = path
450
def id2path(file_id):
451
path = new_entries_map.get(file_id)
454
entry = old_entries[file_id]
455
if entry.parent_id is None:
457
return os.path.join(id2path(entry.parent_id), entry.name)
459
for file_id in old_entries:
460
entry = old_entries[file_id]
461
path = id2path(file_id)
462
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
463
by_path[path] = file_id
468
for path, file_id in new_entries:
470
del new_inventory[file_id]
473
new_path_list.append((path, file_id))
474
if file_id not in old_entries:
476
# Ensure no file is added before its parent
478
for path, file_id in new_path_list:
482
parent = by_path[os.path.dirname(path)]
483
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
484
new_inventory[file_id] = (path, file_id, parent, kind)
485
by_path[path] = file_id
487
# Get a list in insertion order
488
new_inventory_list = new_inventory.values()
489
mutter ("""Inventory regeneration:
490
old length: %i insertions: %i deletions: %i new_length: %i"""\
491
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
492
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
493
new_inventory_list.sort()
494
return new_inventory_list
496
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
497
"diff3": (Diff3Merge, "Merge using external diff3")
939
def merge_type_help():
940
templ = '%s%%7s: %%s' % (' '*12)
941
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
942
return '\n'.join(lines)