1
# Copyright (C) 2005, 2006 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1
from merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory, Diff3Merge
4
from bzrlib import find_branch
6
from bzrlib.errors import BzrCommandError
7
from bzrlib.delta import compare_trees
8
from trace import mutter, warning
20
from tempfile import mkdtemp
23
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
44
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
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
21
class MergeConflictHandler(ExceptionConflictHandler):
22
"""Handle conflicts encountered while merging"""
23
def __init__(self, dir, ignore_zero=False):
24
ExceptionConflictHandler.__init__(self, dir)
26
self.ignore_zero = ignore_zero
28
def copy(self, source, dest):
29
"""Copy the text and mode of a file
30
:param source: The path of the file to copy
31
:param dest: The distination file to create
33
s_file = file(source, "rb")
34
d_file = file(dest, "wb")
37
os.chmod(dest, 0777 & os.stat(source).st_mode)
39
def add_suffix(self, name, suffix, last_new_name=None):
40
"""Rename a file to append a suffix. If the new name exists, the
41
suffix is added repeatedly until a non-existant name is found
43
:param name: The path of the file
44
:param suffix: The suffix to append
45
:param last_new_name: (used for recursive calls) the last name tried
47
if last_new_name is None:
49
new_name = last_new_name+suffix
51
os.rename(name, new_name)
54
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
56
return self.add_suffix(name, suffix, last_new_name=new_name)
58
def conflict(self, text):
63
def merge_conflict(self, new_file, this_path, base_path, other_path):
65
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
66
main file will be a version with diff3 conflicts.
67
:param new_file: Path to the output file with diff3 markers
68
:param this_path: Path to the file text for the THIS tree
69
:param base_path: Path to the file text for the BASE tree
70
:param other_path: Path to the file text for the OTHER tree
72
self.add_suffix(this_path, ".THIS")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
75
os.rename(new_file, this_path)
76
self.conflict("Diff3 conflict encountered in %s" % this_path)
78
def target_exists(self, entry, target, old_path):
79
"""Handle the case when the target file or dir exists"""
80
moved_path = self.add_suffix(target, ".moved")
81
self.conflict("Moved existing %s to %s" % (target, moved_path))
83
def rmdir_non_empty(self, filename):
84
"""Handle the case where the dir to be removed still has contents"""
85
self.conflict("Directory %s not removed because it is not empty"\
90
if not self.ignore_zero:
91
print "%d conflicts encountered.\n" % self.conflicts
93
class SourceFile(object):
94
def __init__(self, path, id, present=None, isdir=None):
97
self.present = present
99
self.interesting = True
102
return "SourceFile(%s, %s)" % (self.path, self.id)
104
def get_tree(treespec, temp_root, label):
54
105
location, revno = treespec
106
branch = find_branch(location)
56
tree = workingtree.WorkingTree.open_containing(location)[0]
57
return tree.branch, tree
58
branch = Branch.open_containing(location)[0]
60
revision = branch.last_revision()
62
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):
70
base_tree = branch.bzrdir.open_workingtree()
72
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)
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()):
108
base_tree = branch.working_tree()
110
base_tree = branch.basis_tree()
112
base_tree = branch.revision_tree(branch.lookup_revision(revno))
113
temp_path = os.path.join(temp_root, label)
115
return branch, MergeTree(base_tree, temp_path)
118
def abspath(tree, file_id):
119
path = tree.inventory.id2path(file_id)
124
def file_exists(tree, file_id):
125
return tree.has_filename(tree.id2path(file_id))
127
def inventory_map(tree):
129
for file_id in tree.inventory:
130
path = abspath(tree, file_id)
131
inventory[path] = SourceFile(path, file_id)
135
class MergeTree(object):
136
def __init__(self, tree, tempdir):
89
137
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)
138
if hasattr(tree, "basedir"):
139
self.root = tree.basedir
142
self.inventory = inventory_map(tree)
144
self.tempdir = tempdir
145
os.mkdir(os.path.join(self.tempdir, "texts"))
148
def readonly_path(self, id):
149
if id not in self.tree:
151
if self.root is not None:
152
return self.tree.abspath(self.tree.id2path(id))
154
if self.tree.inventory[id].kind in ("directory", "root_directory"):
156
if not self.cached.has_key(id):
157
path = os.path.join(self.tempdir, "texts", id)
158
outfile = file(path, "wb")
159
outfile.write(self.tree.get_file(id).read())
160
assert(os.path.exists(path))
161
self.cached[id] = path
162
return self.cached[id]
166
def merge(other_revision, base_revision,
167
check_clean=True, ignore_zero=False,
168
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
170
"""Merge changes into a tree.
173
Base for three-way merge.
175
Other revision for three-way merge.
177
Directory to merge changes into; '.' by default.
179
If true, this_dir must have no uncommitted changes before the
182
tempdir = tempfile.mkdtemp(prefix="bzr-")
186
this_branch = find_branch(this_dir)
188
changes = compare_trees(this_branch.working_tree(),
189
this_branch.basis_tree(), False)
121
190
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:
142
raise BzrCommandError("This branch has no commits")
145
if self.this_basis != self.this_rev_id:
146
191
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,
197
if other_revision[1] == -1:
198
self.other_rev_id = other_branch.last_revision()
199
if self.other_rev_id is None:
200
raise NoCommits(other_branch)
201
self.other_basis = self.other_rev_id
202
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
206
self.other_rev_id = None
207
self.other_basis = other_branch.last_revision()
208
if self.other_basis is None:
209
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")
192
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
222
193
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,
232
except NoCommonAncestor:
194
if other_revision[1] == -1:
197
o_revno = other_revision[1]
198
base_revno = this_branch.common_ancestor(other_branch,
199
other_revno=o_revno)[0]
200
if base_revno is None:
233
201
raise UnrelatedBranches()
234
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
236
self.base_is_ancestor = True
238
base_branch, self.base_tree = _get_tree(base_revision)
239
if base_revision[1] == -1:
240
self.base_rev_id = base_branch.last_revision()
241
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)
202
base_revision = ['.', base_revno]
203
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
204
if file_list is None:
205
interesting_ids = None
207
interesting_ids = set()
208
this_tree = this_branch.working_tree()
209
for fname in file_list:
210
path = this_branch.relpath(fname)
212
for tree in (this_tree, base_tree.tree, other_tree.tree):
213
file_id = tree.inventory.path2id(path)
741
214
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)
215
interesting_ids.add(file_id)
218
raise BzrCommandError("%s is not a source file in any"
220
merge_inner(this_branch, other_tree, base_tree, tempdir,
221
ignore_zero=ignore_zero, backup_files=backup_files,
222
merge_type=merge_type, interesting_ids=interesting_ids)
224
shutil.rmtree(tempdir)
227
def set_interesting(inventory_a, inventory_b, interesting_ids):
228
"""Mark files whose ids are in interesting_ids as interesting
230
for inventory in (inventory_a, inventory_b):
231
for path, source_file in inventory.iteritems():
232
source_file.interesting = source_file.id in interesting_ids
235
def set_optimized(tree_a, tree_b, inventory_a, inventory_b):
236
"""Mark files that have changed texts as interesting
238
for file_id in tree_a.tree.inventory:
239
if file_id not in tree_b.tree.inventory:
241
entry_a = tree_a.tree.inventory[file_id]
242
entry_b = tree_b.tree.inventory[file_id]
243
if (entry_a.kind, entry_b.kind) != ("file", "file"):
245
if None in (entry_a.text_id, entry_b.text_id):
247
if entry_a.text_id != entry_b.text_id:
249
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
250
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
253
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
254
interesting_ids=None):
255
"""Generate a changeset, with preprocessing to select interesting files.
256
using the text_id to mark really-changed files.
257
This permits blazing comparisons when text_ids are present. It also
258
disables metadata comparison for files with identical texts.
260
if interesting_ids is None:
261
set_optimized(tree_a, tree_b, inventory_a, inventory_b)
263
set_interesting(inventory_a, inventory_b, interesting_ids)
264
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
265
for entry in cset.entries.itervalues():
266
entry.metadata_change = None
270
def merge_inner(this_branch, other_tree, base_tree, tempdir,
271
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
272
interesting_ids=None):
274
def merge_factory(base_file, other_file):
275
contents_change = merge_type(base_file, other_file)
277
contents_change = BackupBeforeChange(contents_change)
278
return contents_change
280
def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
281
return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
284
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
286
def get_inventory(tree):
287
return tree.inventory
289
inv_changes = merge_flex(this_tree, base_tree, other_tree,
290
generate_cset, get_inventory,
291
MergeConflictHandler(base_tree.root,
292
ignore_zero=ignore_zero),
293
merge_factory=merge_factory)
296
for id, path in inv_changes.iteritems():
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")
301
assert path.startswith('./')
303
adjust_ids.append((path, id))
304
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
307
def regen_inventory(this_branch, root, new_entries):
308
old_entries = this_branch.read_working_inventory()
311
for file_id in old_entries:
312
entry = old_entries[file_id]
313
path = old_entries.id2path(file_id)
314
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
315
by_path[path] = file_id
320
for path, file_id in new_entries:
322
del new_inventory[file_id]
325
new_path_list.append((path, file_id))
326
if file_id not in old_entries:
328
# Ensure no file is added before its parent
330
for path, file_id in new_path_list:
334
parent = by_path[os.path.dirname(path)]
335
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
336
new_inventory[file_id] = (path, file_id, parent, kind)
337
by_path[path] = file_id
339
# Get a list in insertion order
340
new_inventory_list = new_inventory.values()
341
mutter ("""Inventory regeneration:
342
old length: %i insertions: %i deletions: %i new_length: %i"""\
343
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
344
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
345
new_inventory_list.sort()
346
return new_inventory_list
348
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
349
"diff3": (Diff3Merge, "Merge using external diff3")
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)