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
31
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning
33
from bzrlib.fetch import greedy_fetch
34
from bzrlib.revision import is_ancestor
36
# comments from abentley on irc: merge happens in two stages, each
37
# of which generates a changeset object
39
# stage 1: generate OLD->OTHER,
40
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
42
class MergeConflictHandler(ExceptionConflictHandler):
43
"""Handle conflicts encountered while merging.
45
This subclasses ExceptionConflictHandler, so that any types of
46
conflict that are not explicitly handled cause an exception and
49
def __init__(self, dir, ignore_zero=False):
50
ExceptionConflictHandler.__init__(self, dir)
52
self.ignore_zero = ignore_zero
54
def copy(self, source, dest):
55
"""Copy the text and mode of a file
56
:param source: The path of the file to copy
57
:param dest: The distination file to create
59
s_file = file(source, "rb")
60
d_file = file(dest, "wb")
63
os.chmod(dest, 0777 & os.stat(source).st_mode)
65
def dump(self, lines, dest):
66
"""Copy the text and mode of a file
67
:param source: The path of the file to copy
68
:param dest: The distination file to create
70
d_file = file(dest, "wb")
74
def add_suffix(self, name, suffix, last_new_name=None):
75
"""Rename a file to append a suffix. If the new name exists, the
76
suffix is added repeatedly until a non-existant name is found
78
:param name: The path of the file
79
:param suffix: The suffix to append
80
:param last_new_name: (used for recursive calls) the last name tried
82
if last_new_name is None:
84
new_name = last_new_name+suffix
86
os.rename(name, new_name)
89
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
91
return self.add_suffix(name, suffix, last_new_name=new_name)
93
def conflict(self, text):
98
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
100
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
101
main file will be a version with diff3 conflicts.
102
:param new_file: Path to the output file with diff3 markers
103
:param this_path: Path to the file text for the THIS tree
104
:param base_path: Path to the file text for the BASE tree
105
:param other_path: Path to the file text for the OTHER tree
107
self.add_suffix(this_path, ".THIS")
108
self.dump(base_lines, this_path+".BASE")
109
self.dump(other_lines, this_path+".OTHER")
110
os.rename(new_file, this_path)
111
self.conflict("Diff3 conflict encountered in %s" % this_path)
113
def new_contents_conflict(self, filename, other_contents):
114
"""Conflicting contents for newly added file."""
115
self.copy(other_contents, filename + ".OTHER")
116
self.conflict("Conflict in newly added file %s" % filename)
119
def target_exists(self, entry, target, old_path):
120
"""Handle the case when the target file or dir exists"""
121
moved_path = self.add_suffix(target, ".moved")
122
self.conflict("Moved existing %s to %s" % (target, moved_path))
124
def rmdir_non_empty(self, filename):
125
"""Handle the case where the dir to be removed still has contents"""
126
self.conflict("Directory %s not removed because it is not empty"\
131
if not self.ignore_zero:
132
print "%d conflicts encountered.\n" % self.conflicts
134
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
135
56
location, revno = treespec
136
branch = find_branch(location)
140
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()
142
revision = branch.lookup_revision(revno)
143
return branch, get_revid_tree(branch, revision, temp_root, label,
146
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):
147
71
if revision is None:
148
base_tree = branch.working_tree()
72
base_tree = branch.bzrdir.open_workingtree()
150
74
if local_branch is not None:
151
greedy_fetch(local_branch, branch, revision)
152
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)
154
base_tree = branch.revision_tree(revision)
155
temp_path = os.path.join(temp_root, label)
157
return MergeTree(base_tree, temp_path)
160
def file_exists(tree, file_id):
161
return tree.has_filename(tree.id2path(file_id))
164
class MergeTree(object):
165
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()):
166
91
object.__init__(self)
167
if hasattr(tree, "basedir"):
168
self.root = tree.basedir
172
self.tempdir = tempdir
173
os.mkdir(os.path.join(self.tempdir, "texts"))
177
return self.tree.__iter__()
179
def __contains__(self, file_id):
180
return file_id in self.tree
182
def get_file(self, file_id):
183
return self.tree.get_file(file_id)
185
def get_file_sha1(self, id):
186
return self.tree.get_file_sha1(id)
188
def id2path(self, file_id):
189
return self.tree.id2path(file_id)
191
def has_id(self, file_id):
192
return self.tree.has_id(file_id)
194
def has_or_had_id(self, file_id):
195
if file_id == self.tree.inventory.root.file_id:
197
return self.tree.inventory.has_id(file_id)
199
def has_or_had_id(self, file_id):
200
if file_id == self.tree.inventory.root.file_id:
202
return self.tree.inventory.has_id(file_id)
204
def readonly_path(self, id):
205
if id not in self.tree:
207
if self.root is not None:
208
return self.tree.abspath(self.tree.id2path(id))
210
if self.tree.inventory[id].kind in ("directory", "root_directory"):
212
if not self.cached.has_key(id):
213
path = os.path.join(self.tempdir, "texts", id)
214
outfile = file(path, "wb")
215
outfile.write(self.tree.get_file(id).read())
216
assert(os.path.exists(path))
217
self.cached[id] = path
218
return self.cached[id]
222
def merge(other_revision, base_revision,
223
check_clean=True, ignore_zero=False,
224
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
226
"""Merge changes into a tree.
229
tuple(path, revision) Base for three-way merge.
231
tuple(path, revision) Other revision for three-way merge.
233
Directory to merge changes into; '.' by default.
235
If true, this_dir must have no uncommitted changes before the
237
all available ancestors of other_revision and base_revision are
238
automatically pulled into the branch.
240
from bzrlib.revision import common_ancestor, MultipleRevisionSources
241
from bzrlib.errors import NoSuchRevision
242
tempdir = tempfile.mkdtemp(prefix="bzr-")
246
this_branch = find_branch(this_dir)
247
this_rev_id = this_branch.last_patch()
248
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:
249
144
raise BzrCommandError("This branch has no commits")
251
changes = compare_trees(this_branch.working_tree(),
252
this_branch.basis_tree(), False)
253
if changes.has_changed():
147
if self.this_basis != self.this_rev_id:
254
148
raise BzrCommandError("Working tree has uncommitted changes.")
255
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,
257
199
if other_revision[1] == -1:
258
other_rev_id = other_branch.last_patch()
259
other_basis = other_rev_id
200
self.other_rev_id = other_branch.last_revision()
201
if self.other_rev_id is None:
202
raise NoCommits(other_branch)
203
self.other_basis = self.other_rev_id
260
204
elif other_revision[1] is not None:
261
other_rev_id = other_branch.lookup_revision(other_revision[1])
262
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
265
other_basis = other_branch.last_patch()
208
self.other_rev_id = None
209
self.other_basis = other_branch.last_revision()
210
if self.other_basis is None:
211
raise NoCommits(other_branch)
212
if other_branch.base != self.this_branch.base:
213
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
216
self.set_base([None, None])
218
def set_base(self, base_revision):
219
"""Set the base revision to use for the merge.
221
:param base_revision: A 2-list containing a path and revision number.
223
mutter("doing merge() with no base_revision specified")
266
224
if base_revision == [None, None]:
267
base_rev_id = common_ancestor(this_rev_id, other_basis,
269
if base_rev_id is None:
226
pb = ui.ui_factory.nested_progress_bar()
228
this_repo = self.this_branch.repository
229
self.base_rev_id = common_ancestor(self.this_basis,
234
except NoCommonAncestor:
270
235
raise UnrelatedBranches()
271
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
273
base_is_ancestor = True
236
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
238
self.base_is_ancestor = True
275
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
240
base_branch, self.base_tree = _get_tree(base_revision)
276
241
if base_revision[1] == -1:
277
base_rev_id = base_branch.last_patch()
242
self.base_rev_id = base_branch.last_revision()
278
243
elif base_revision[1] is None:
281
base_rev_id = base_branch.lookup_revision(base_revision[1])
282
if base_rev_id is not None:
283
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
284
MultipleRevisionSources(this_branch,
287
base_is_ancestor = False
288
if file_list is None:
289
interesting_ids = None
291
interesting_ids = set()
292
this_tree = this_branch.working_tree()
293
for fname in file_list:
294
path = this_branch.relpath(fname)
296
for tree in (this_tree, base_tree.tree, other_tree.tree):
297
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)
298
771
if file_id is not None:
299
interesting_ids.add(file_id)
302
raise BzrCommandError("%s is not a source file in any"
304
merge_inner(this_branch, other_tree, base_tree, tempdir,
305
ignore_zero=ignore_zero, backup_files=backup_files,
306
merge_type=merge_type, interesting_ids=interesting_ids)
307
if base_is_ancestor and other_rev_id is not None\
308
and other_rev_id not in this_branch.revision_history():
309
this_branch.add_pending_merge(other_rev_id)
311
shutil.rmtree(tempdir)
314
def set_interesting(inventory_a, inventory_b, interesting_ids):
315
"""Mark files whose ids are in interesting_ids as interesting
317
for inventory in (inventory_a, inventory_b):
318
for path, source_file in inventory.iteritems():
319
source_file.interesting = source_file.id in interesting_ids
322
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
323
"""Generate a changeset. If interesting_ids is supplied, only changes
324
to those files will be shown. Metadata changes are stripped.
326
cset = generate_changeset(tree_a, tree_b, interesting_ids)
327
for entry in cset.entries.itervalues():
328
entry.metadata_change = None
332
def merge_inner(this_branch, other_tree, base_tree, tempdir,
333
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
334
interesting_ids=None):
336
def merge_factory(file_id, base, other):
337
contents_change = merge_type(file_id, base, other)
339
contents_change = BackupBeforeChange(contents_change)
340
return contents_change
342
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
344
def get_inventory(tree):
345
return tree.tree.inventory
347
inv_changes = merge_flex(this_tree, base_tree, other_tree,
348
generate_cset_optimized, get_inventory,
349
MergeConflictHandler(base_tree.root,
350
ignore_zero=ignore_zero),
351
merge_factory=merge_factory,
352
interesting_ids=interesting_ids)
355
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)
360
assert path.startswith('./'), "path is %s" % path
362
adjust_ids.append((path, id))
363
if len(adjust_ids) > 0:
364
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
368
def regen_inventory(this_branch, root, new_entries):
369
old_entries = this_branch.read_working_inventory()
373
for path, file_id in new_entries:
376
new_entries_map[file_id] = path
378
def id2path(file_id):
379
path = new_entries_map.get(file_id)
382
entry = old_entries[file_id]
383
if entry.parent_id is None:
385
return os.path.join(id2path(entry.parent_id), entry.name)
387
for file_id in old_entries:
388
entry = old_entries[file_id]
389
path = id2path(file_id)
390
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
391
by_path[path] = file_id
396
for path, file_id in new_entries:
398
del new_inventory[file_id]
401
new_path_list.append((path, file_id))
402
if file_id not in old_entries:
404
# Ensure no file is added before its parent
406
for path, file_id in new_path_list:
410
parent = by_path[os.path.dirname(path)]
411
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
412
new_inventory[file_id] = (path, file_id, parent, kind)
413
by_path[path] = file_id
415
# Get a list in insertion order
416
new_inventory_list = new_inventory.values()
417
mutter ("""Inventory regeneration:
418
old length: %i insertions: %i deletions: %i new_length: %i"""\
419
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
420
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
421
new_inventory_list.sort()
422
return new_inventory_list
424
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
425
"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)