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
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,
48
from bzrlib.versionedfile import WeaveMerge
51
# TODO: Report back as changes are merged in
53
def _get_tree(treespec, local_branch=None):
54
from bzrlib import workingtree
55
location, revno = treespec
57
tree = workingtree.WorkingTree.open_containing(location)[0]
58
return tree.branch, tree
59
branch = Branch.open_containing(location)[0]
61
revision = branch.last_revision()
63
revision = branch.get_rev_id(revno)
65
revision = NULL_REVISION
66
return branch, _get_revid_tree(branch, revision, local_branch)
69
def _get_revid_tree(branch, revision, local_branch):
71
base_tree = branch.bzrdir.open_workingtree()
73
if local_branch is not None:
74
if local_branch.base != branch.base:
75
local_branch.fetch(branch, revision)
76
base_tree = local_branch.repository.revision_tree(revision)
78
base_tree = branch.repository.revision_tree(revision)
82
def transform_tree(from_tree, to_tree, interesting_ids=None):
83
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
84
interesting_ids=interesting_ids, this_tree=from_tree)
88
def __init__(self, this_branch, other_tree=None, base_tree=None,
89
this_tree=None, pb=DummyProgress()):
91
assert this_tree is not None, "this_tree is required"
92
self.this_branch = this_branch
93
self.this_basis = this_branch.last_revision()
94
self.this_rev_id = None
95
self.this_tree = this_tree
96
self.this_revision_tree = None
97
self.this_basis_tree = None
98
self.other_tree = other_tree
99
self.base_tree = base_tree
100
self.ignore_zero = False
101
self.backup_files = False
102
self.interesting_ids = None
103
self.show_base = False
104
self.reprocess = False
109
def revision_tree(self, revision_id):
110
return self.this_branch.repository.revision_tree(revision_id)
112
def ensure_revision_trees(self):
113
if self.this_revision_tree is None:
114
self.this_basis_tree = self.this_branch.repository.revision_tree(
116
if self.this_basis == self.this_rev_id:
117
self.this_revision_tree = self.this_basis_tree
119
if self.other_rev_id is None:
120
other_basis_tree = self.revision_tree(self.other_basis)
121
changes = other_basis_tree.changes_from(self.other_tree)
122
if changes.has_changed():
123
raise WorkingTreeNotRevision(self.this_tree)
124
other_rev_id = self.other_basis
125
self.other_tree = other_basis_tree
127
def file_revisions(self, file_id):
128
self.ensure_revision_trees()
129
def get_id(tree, file_id):
130
revision_id = tree.inventory[file_id].revision
131
assert revision_id is not None
133
if self.this_rev_id is None:
134
if self.this_basis_tree.get_file_sha1(file_id) != \
135
self.this_tree.get_file_sha1(file_id):
136
raise WorkingTreeNotRevision(self.this_tree)
138
trees = (self.this_basis_tree, self.other_tree)
139
return [get_id(tree, file_id) for tree in trees]
141
def check_basis(self, check_clean, require_commits=True):
142
if self.this_basis is None and require_commits is True:
143
raise BzrCommandError("This branch has no commits")
146
if self.this_basis != self.this_rev_id:
147
raise BzrCommandError("Working tree has uncommitted changes.")
149
def compare_basis(self):
150
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
151
if not changes.has_changed():
152
self.this_rev_id = self.this_basis
154
def set_interesting_files(self, file_list):
156
self._set_interesting_files(file_list)
157
except NotVersionedError, e:
158
raise BzrCommandError("%s is not a source file in any"
161
def _set_interesting_files(self, file_list):
162
"""Set the list of interesting ids from a list of files."""
163
if file_list is None:
164
self.interesting_ids = None
167
interesting_ids = set()
168
for path in file_list:
170
for tree in (self.this_tree, self.base_tree, self.other_tree):
171
file_id = tree.inventory.path2id(path)
172
if file_id is not None:
173
interesting_ids.add(file_id)
176
raise NotVersionedError(path=path)
177
self.interesting_ids = interesting_ids
179
def set_pending(self):
180
if not self.base_is_ancestor:
182
if self.other_rev_id is None:
184
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
185
if self.other_rev_id in ancestry:
187
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
189
def set_other(self, other_revision):
190
"""Set the revision and tree to merge from.
192
This sets the other_tree, other_rev_id, other_basis attributes.
194
:param other_revision: The [path, revision] list to merge from.
196
other_branch, self.other_tree = _get_tree(other_revision,
198
if other_revision[1] == -1:
199
self.other_rev_id = other_branch.last_revision()
200
if self.other_rev_id is None:
201
raise NoCommits(other_branch)
202
self.other_basis = self.other_rev_id
203
elif other_revision[1] is not None:
204
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
205
self.other_basis = self.other_rev_id
207
self.other_rev_id = None
208
self.other_basis = other_branch.last_revision()
209
if self.other_basis is None:
210
raise NoCommits(other_branch)
211
if other_branch.base != self.this_branch.base:
212
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
215
self.set_base([None, None])
217
def set_base(self, base_revision):
218
"""Set the base revision to use for the merge.
220
:param base_revision: A 2-list containing a path and revision number.
222
mutter("doing merge() with no base_revision specified")
223
if base_revision == [None, None]:
225
pb = bzrlib.ui.ui_factory.nested_progress_bar()
227
this_repo = self.this_branch.repository
228
self.base_rev_id = common_ancestor(self.this_basis,
233
except NoCommonAncestor:
234
raise UnrelatedBranches()
235
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
237
self.base_is_ancestor = True
239
base_branch, self.base_tree = _get_tree(base_revision)
240
if base_revision[1] == -1:
241
self.base_rev_id = base_branch.last_revision()
242
elif base_revision[1] is None:
243
self.base_rev_id = None
245
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
246
if self.this_branch.base != base_branch.base:
247
self.this_branch.fetch(base_branch)
248
self.base_is_ancestor = is_ancestor(self.this_basis,
253
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
254
'other_tree': self.other_tree,
255
'interesting_ids': self.interesting_ids,
257
if self.merge_type.requires_base:
258
kwargs['base_tree'] = self.base_tree
259
if self.merge_type.supports_reprocess:
260
kwargs['reprocess'] = self.reprocess
262
raise BzrError("Conflict reduction is not supported for merge"
263
" type %s." % self.merge_type)
264
if self.merge_type.supports_show_base:
265
kwargs['show_base'] = self.show_base
267
raise BzrError("Showing base is not supported for this"
268
" merge type. %s" % self.merge_type)
269
merge = self.merge_type(pb=self._pb, **kwargs)
270
if len(merge.cooked_conflicts) == 0:
271
if not self.ignore_zero:
272
note("All changes applied successfully.")
274
note("%d conflicts encountered." % len(merge.cooked_conflicts))
276
return len(merge.cooked_conflicts)
278
def regen_inventory(self, new_entries):
279
old_entries = self.this_tree.read_working_inventory()
283
for path, file_id in new_entries:
286
new_entries_map[file_id] = path
288
def id2path(file_id):
289
path = new_entries_map.get(file_id)
292
entry = old_entries[file_id]
293
if entry.parent_id is None:
295
return pathjoin(id2path(entry.parent_id), entry.name)
297
for file_id in old_entries:
298
entry = old_entries[file_id]
299
path = id2path(file_id)
300
if file_id in self.base_tree.inventory:
301
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
303
executable = getattr(entry, 'executable', False)
304
new_inventory[file_id] = (path, file_id, entry.parent_id,
305
entry.kind, executable)
307
by_path[path] = file_id
312
for path, file_id in new_entries:
314
del new_inventory[file_id]
317
new_path_list.append((path, file_id))
318
if file_id not in old_entries:
320
# Ensure no file is added before its parent
322
for path, file_id in new_path_list:
326
parent = by_path[os.path.dirname(path)]
327
abspath = pathjoin(self.this_tree.basedir, path)
328
kind = bzrlib.osutils.file_kind(abspath)
329
if file_id in self.base_tree.inventory:
330
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
333
new_inventory[file_id] = (path, file_id, parent, kind, executable)
334
by_path[path] = file_id
336
# Get a list in insertion order
337
new_inventory_list = new_inventory.values()
338
mutter ("""Inventory regeneration:
339
old length: %i insertions: %i deletions: %i new_length: %i"""\
340
% (len(old_entries), insertions, deletions,
341
len(new_inventory_list)))
342
assert len(new_inventory_list) == len(old_entries) + insertions\
344
new_inventory_list.sort()
345
return new_inventory_list
348
class Merge3Merger(object):
349
"""Three-way merger that uses the merge3 text merger"""
351
supports_reprocess = True
352
supports_show_base = True
353
history_based = False
355
def __init__(self, working_tree, this_tree, base_tree, other_tree,
356
interesting_ids=None, reprocess=False, show_base=False,
357
pb=DummyProgress(), pp=None):
358
"""Initialize the merger object and perform the merge."""
359
object.__init__(self)
360
self.this_tree = working_tree
361
self.base_tree = base_tree
362
self.other_tree = other_tree
363
self._raw_conflicts = []
364
self.cooked_conflicts = []
365
self.reprocess = reprocess
366
self.show_base = show_base
370
self.pp = ProgressPhase("Merge phase", 3, self.pb)
372
if interesting_ids is not None:
373
all_ids = interesting_ids
375
all_ids = set(base_tree)
376
all_ids.update(other_tree)
377
working_tree.lock_write()
378
self.tt = TreeTransform(working_tree, self.pb)
381
child_pb = ui.ui_factory.nested_progress_bar()
383
for num, file_id in enumerate(all_ids):
384
child_pb.update('Preparing file merge', num, len(all_ids))
385
self.merge_names(file_id)
386
file_status = self.merge_contents(file_id)
387
self.merge_executable(file_id, file_status)
392
child_pb = ui.ui_factory.nested_progress_bar()
394
fs_conflicts = resolve_conflicts(self.tt, child_pb)
397
self.cook_conflicts(fs_conflicts)
398
for conflict in self.cooked_conflicts:
401
results = self.tt.apply()
402
self.write_modified(results)
404
working_tree.add_conflicts(self.cooked_conflicts)
405
except UnsupportedOperation:
409
working_tree.unlock()
414
self.tt.final_kind(self.tt.root)
416
self.tt.cancel_deletion(self.tt.root)
417
if self.tt.final_file_id(self.tt.root) is None:
418
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
420
if self.other_tree.inventory.root is None:
422
other_root_file_id = self.other_tree.inventory.root.file_id
423
other_root = self.tt.trans_id_file_id(other_root_file_id)
424
if other_root == self.tt.root:
427
self.tt.final_kind(other_root)
430
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
431
self.tt.cancel_creation(other_root)
432
self.tt.cancel_versioning(other_root)
434
def reparent_children(self, ie, target):
435
for thing, child in ie.children.iteritems():
436
trans_id = self.tt.trans_id_file_id(child.file_id)
437
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
439
def write_modified(self, results):
441
for path in results.modified_paths:
442
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
445
hash = self.this_tree.get_file_sha1(file_id)
448
modified_hashes[file_id] = hash
449
self.this_tree.set_merge_modified(modified_hashes)
452
def parent(entry, file_id):
453
"""Determine the parent for a file_id (used as a key method)"""
456
return entry.parent_id
459
def name(entry, file_id):
460
"""Determine the name for a file_id (used as a key method)"""
466
def contents_sha1(tree, file_id):
467
"""Determine the sha1 of the file contents (used as a key method)."""
468
if file_id not in tree:
470
return tree.get_file_sha1(file_id)
473
def executable(tree, file_id):
474
"""Determine the executability of a file-id (used as a key method)."""
475
if file_id not in tree:
477
if tree.kind(file_id) != "file":
479
return tree.is_executable(file_id)
482
def kind(tree, file_id):
483
"""Determine the kind of a file-id (used as a key method)."""
484
if file_id not in tree:
486
return tree.kind(file_id)
489
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
490
"""Do a three-way test on a scalar.
491
Return "this", "other" or "conflict", depending whether a value wins.
493
key_base = key(base_tree, file_id)
494
key_other = key(other_tree, file_id)
495
#if base == other, either they all agree, or only THIS has changed.
496
if key_base == key_other:
498
key_this = key(this_tree, file_id)
499
if key_this not in (key_base, key_other):
501
# "Ambiguous clean merge"
502
elif key_this == key_other:
505
assert key_this == key_base
508
def merge_names(self, file_id):
509
"""Perform a merge on file_id names and parents"""
511
if file_id in tree.inventory:
512
return tree.inventory[file_id]
515
this_entry = get_entry(self.this_tree)
516
other_entry = get_entry(self.other_tree)
517
base_entry = get_entry(self.base_tree)
518
name_winner = self.scalar_three_way(this_entry, base_entry,
519
other_entry, file_id, self.name)
520
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
521
other_entry, file_id,
523
if this_entry is None:
524
if name_winner == "this":
525
name_winner = "other"
526
if parent_id_winner == "this":
527
parent_id_winner = "other"
528
if name_winner == "this" and parent_id_winner == "this":
530
if name_winner == "conflict":
531
trans_id = self.tt.trans_id_file_id(file_id)
532
self._raw_conflicts.append(('name conflict', trans_id,
533
self.name(this_entry, file_id),
534
self.name(other_entry, file_id)))
535
if parent_id_winner == "conflict":
536
trans_id = self.tt.trans_id_file_id(file_id)
537
self._raw_conflicts.append(('parent conflict', trans_id,
538
self.parent(this_entry, file_id),
539
self.parent(other_entry, file_id)))
540
if other_entry is None:
541
# it doesn't matter whether the result was 'other' or
542
# 'conflict'-- if there's no 'other', we leave it alone.
544
# if we get here, name_winner and parent_winner are set to safe values.
545
winner_entry = {"this": this_entry, "other": other_entry,
546
"conflict": other_entry}
547
trans_id = self.tt.trans_id_file_id(file_id)
548
parent_id = winner_entry[parent_id_winner].parent_id
549
if parent_id is not None:
550
parent_trans_id = self.tt.trans_id_file_id(parent_id)
551
self.tt.adjust_path(winner_entry[name_winner].name,
552
parent_trans_id, trans_id)
554
def merge_contents(self, file_id):
555
"""Performa a merge on file_id contents."""
556
def contents_pair(tree):
557
if file_id not in tree:
559
kind = tree.kind(file_id)
561
contents = tree.get_file_sha1(file_id)
562
elif kind == "symlink":
563
contents = tree.get_symlink_target(file_id)
566
return kind, contents
568
def contents_conflict():
569
trans_id = self.tt.trans_id_file_id(file_id)
570
name = self.tt.final_name(trans_id)
571
parent_id = self.tt.final_parent(trans_id)
572
if file_id in self.this_tree.inventory:
573
self.tt.unversion_file(trans_id)
574
self.tt.delete_contents(trans_id)
575
file_group = self._dump_conflicts(name, parent_id, file_id,
577
self._raw_conflicts.append(('contents conflict', file_group))
579
# See SPOT run. run, SPOT, run.
580
# So we're not QUITE repeating ourselves; we do tricky things with
582
base_pair = contents_pair(self.base_tree)
583
other_pair = contents_pair(self.other_tree)
584
if base_pair == other_pair:
585
# OTHER introduced no changes
587
this_pair = contents_pair(self.this_tree)
588
if this_pair == other_pair:
589
# THIS and OTHER introduced the same changes
592
trans_id = self.tt.trans_id_file_id(file_id)
593
if this_pair == base_pair:
594
# only OTHER introduced changes
595
if file_id in self.this_tree:
596
# Remove any existing contents
597
self.tt.delete_contents(trans_id)
598
if file_id in self.other_tree:
599
# OTHER changed the file
600
create_by_entry(self.tt,
601
self.other_tree.inventory[file_id],
602
self.other_tree, trans_id)
603
if file_id not in self.this_tree.inventory:
604
self.tt.version_file(file_id, trans_id)
606
elif file_id in self.this_tree.inventory:
607
# OTHER deleted the file
608
self.tt.unversion_file(trans_id)
610
#BOTH THIS and OTHER introduced changes; scalar conflict
611
elif this_pair[0] == "file" and other_pair[0] == "file":
612
# THIS and OTHER are both files, so text merge. Either
613
# BASE is a file, or both converted to files, so at least we
614
# have agreement that output should be a file.
616
self.text_merge(file_id, trans_id)
618
return contents_conflict()
619
if file_id not in self.this_tree.inventory:
620
self.tt.version_file(file_id, trans_id)
622
self.tt.tree_kind(trans_id)
623
self.tt.delete_contents(trans_id)
628
# Scalar conflict, can't text merge. Dump conflicts
629
return contents_conflict()
631
def get_lines(self, tree, file_id):
632
"""Return the lines in a file, or an empty list."""
634
return tree.get_file(file_id).readlines()
638
def text_merge(self, file_id, trans_id):
639
"""Perform a three-way text merge on a file_id"""
640
# it's possible that we got here with base as a different type.
641
# if so, we just want two-way text conflicts.
642
if file_id in self.base_tree and \
643
self.base_tree.kind(file_id) == "file":
644
base_lines = self.get_lines(self.base_tree, file_id)
647
other_lines = self.get_lines(self.other_tree, file_id)
648
this_lines = self.get_lines(self.this_tree, file_id)
649
m3 = Merge3(base_lines, this_lines, other_lines)
650
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
651
if self.show_base is True:
652
base_marker = '|' * 7
656
def iter_merge3(retval):
657
retval["text_conflicts"] = False
658
for line in m3.merge_lines(name_a = "TREE",
659
name_b = "MERGE-SOURCE",
660
name_base = "BASE-REVISION",
661
start_marker=start_marker,
662
base_marker=base_marker,
663
reprocess=self.reprocess):
664
if line.startswith(start_marker):
665
retval["text_conflicts"] = True
666
yield line.replace(start_marker, '<' * 7)
670
merge3_iterator = iter_merge3(retval)
671
self.tt.create_file(merge3_iterator, trans_id)
672
if retval["text_conflicts"] is True:
673
self._raw_conflicts.append(('text conflict', trans_id))
674
name = self.tt.final_name(trans_id)
675
parent_id = self.tt.final_parent(trans_id)
676
file_group = self._dump_conflicts(name, parent_id, file_id,
677
this_lines, base_lines,
679
file_group.append(trans_id)
681
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
682
base_lines=None, other_lines=None, set_version=False,
684
"""Emit conflict files.
685
If this_lines, base_lines, or other_lines are omitted, they will be
686
determined automatically. If set_version is true, the .OTHER, .THIS
687
or .BASE (in that order) will be created as versioned files.
689
data = [('OTHER', self.other_tree, other_lines),
690
('THIS', self.this_tree, this_lines)]
692
data.append(('BASE', self.base_tree, base_lines))
695
for suffix, tree, lines in data:
697
trans_id = self._conflict_file(name, parent_id, tree, file_id,
699
file_group.append(trans_id)
700
if set_version and not versioned:
701
self.tt.version_file(file_id, trans_id)
705
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
707
"""Emit a single conflict file."""
708
name = name + '.' + suffix
709
trans_id = self.tt.create_path(name, parent_id)
710
entry = tree.inventory[file_id]
711
create_by_entry(self.tt, entry, tree, trans_id, lines)
714
def merge_executable(self, file_id, file_status):
715
"""Perform a merge on the execute bit."""
716
if file_status == "deleted":
718
trans_id = self.tt.trans_id_file_id(file_id)
720
if self.tt.final_kind(trans_id) != "file":
724
winner = self.scalar_three_way(self.this_tree, self.base_tree,
725
self.other_tree, file_id,
727
if winner == "conflict":
728
# There must be a None in here, if we have a conflict, but we
729
# need executability since file status was not deleted.
730
if self.executable(self.other_tree, file_id) is None:
735
if file_status == "modified":
736
executability = self.this_tree.is_executable(file_id)
737
if executability is not None:
738
trans_id = self.tt.trans_id_file_id(file_id)
739
self.tt.set_executability(executability, trans_id)
741
assert winner == "other"
742
if file_id in self.other_tree:
743
executability = self.other_tree.is_executable(file_id)
744
elif file_id in self.this_tree:
745
executability = self.this_tree.is_executable(file_id)
746
elif file_id in self.base_tree:
747
executability = self.base_tree.is_executable(file_id)
748
if executability is not None:
749
trans_id = self.tt.trans_id_file_id(file_id)
750
self.tt.set_executability(executability, trans_id)
752
def cook_conflicts(self, fs_conflicts):
753
"""Convert all conflicts into a form that doesn't depend on trans_id"""
754
from conflicts import Conflict
756
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
757
fp = FinalPaths(self.tt)
758
for conflict in self._raw_conflicts:
759
conflict_type = conflict[0]
760
if conflict_type in ('name conflict', 'parent conflict'):
761
trans_id = conflict[1]
762
conflict_args = conflict[2:]
763
if trans_id not in name_conflicts:
764
name_conflicts[trans_id] = {}
765
unique_add(name_conflicts[trans_id], conflict_type,
767
if conflict_type == 'contents conflict':
768
for trans_id in conflict[1]:
769
file_id = self.tt.final_file_id(trans_id)
770
if file_id is not None:
772
path = fp.get_path(trans_id)
773
for suffix in ('.BASE', '.THIS', '.OTHER'):
774
if path.endswith(suffix):
775
path = path[:-len(suffix)]
777
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
778
self.cooked_conflicts.append(c)
779
if conflict_type == 'text conflict':
780
trans_id = conflict[1]
781
path = fp.get_path(trans_id)
782
file_id = self.tt.final_file_id(trans_id)
783
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
784
self.cooked_conflicts.append(c)
786
for trans_id, conflicts in name_conflicts.iteritems():
788
this_parent, other_parent = conflicts['parent conflict']
789
assert this_parent != other_parent
791
this_parent = other_parent = \
792
self.tt.final_file_id(self.tt.final_parent(trans_id))
794
this_name, other_name = conflicts['name conflict']
795
assert this_name != other_name
797
this_name = other_name = self.tt.final_name(trans_id)
798
other_path = fp.get_path(trans_id)
799
if this_parent is not None:
801
fp.get_path(self.tt.trans_id_file_id(this_parent))
802
this_path = pathjoin(this_parent_path, this_name)
804
this_path = "<deleted>"
805
file_id = self.tt.final_file_id(trans_id)
806
c = Conflict.factory('path conflict', path=this_path,
807
conflict_path=other_path, file_id=file_id)
808
self.cooked_conflicts.append(c)
809
self.cooked_conflicts.sort(key=Conflict.sort_key)
812
class WeaveMerger(Merge3Merger):
813
"""Three-way tree merger, text weave merger."""
814
supports_reprocess = True
815
supports_show_base = False
817
def __init__(self, working_tree, this_tree, base_tree, other_tree,
818
interesting_ids=None, pb=DummyProgress(), pp=None,
820
self.this_revision_tree = self._get_revision_tree(this_tree)
821
self.other_revision_tree = self._get_revision_tree(other_tree)
822
super(WeaveMerger, self).__init__(working_tree, this_tree,
823
base_tree, other_tree,
824
interesting_ids=interesting_ids,
825
pb=pb, pp=pp, reprocess=reprocess)
827
def _get_revision_tree(self, tree):
828
"""Return a revision tree related to this tree.
829
If the tree is a WorkingTree, the basis will be returned.
831
if getattr(tree, 'get_weave', False) is False:
832
# If we have a WorkingTree, try using the basis
833
return tree.branch.basis_tree()
837
def _check_file(self, file_id):
838
"""Check that the revision tree's version of the file matches."""
839
for tree, rt in ((self.this_tree, self.this_revision_tree),
840
(self.other_tree, self.other_revision_tree)):
843
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
844
raise WorkingTreeNotRevision(self.this_tree)
846
def _merged_lines(self, file_id):
847
"""Generate the merged lines.
848
There is no distinction between lines that are meant to contain <<<<<<<
851
weave = self.this_revision_tree.get_weave(file_id)
852
this_revision_id = self.this_revision_tree.inventory[file_id].revision
853
other_revision_id = \
854
self.other_revision_tree.inventory[file_id].revision
855
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
856
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
857
return wm.merge_lines(self.reprocess)
859
def text_merge(self, file_id, trans_id):
860
"""Perform a (weave) text merge for a given file and file-id.
861
If conflicts are encountered, .THIS and .OTHER files will be emitted,
862
and a conflict will be noted.
864
self._check_file(file_id)
865
lines, conflicts = self._merged_lines(file_id)
867
# Note we're checking whether the OUTPUT is binary in this case,
868
# because we don't want to get into weave merge guts.
869
check_text_lines(lines)
870
self.tt.create_file(lines, trans_id)
872
self._raw_conflicts.append(('text conflict', trans_id))
873
name = self.tt.final_name(trans_id)
874
parent_id = self.tt.final_parent(trans_id)
875
file_group = self._dump_conflicts(name, parent_id, file_id,
877
file_group.append(trans_id)
880
class Diff3Merger(Merge3Merger):
881
"""Three-way merger using external diff3 for text merging"""
883
def dump_file(self, temp_dir, name, tree, file_id):
884
out_path = pathjoin(temp_dir, name)
885
out_file = open(out_path, "wb")
887
in_file = tree.get_file(file_id)
894
def text_merge(self, file_id, trans_id):
895
"""Perform a diff3 merge using a specified file-id and trans-id.
896
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
897
will be dumped, and a will be conflict noted.
900
temp_dir = mkdtemp(prefix="bzr-")
902
new_file = pathjoin(temp_dir, "new")
903
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
904
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
905
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
906
status = bzrlib.patch.diff3(new_file, this, base, other)
907
if status not in (0, 1):
908
raise BzrError("Unhandled diff3 exit code")
909
f = open(new_file, 'rb')
911
self.tt.create_file(f, trans_id)
915
name = self.tt.final_name(trans_id)
916
parent_id = self.tt.final_parent(trans_id)
917
self._dump_conflicts(name, parent_id, file_id)
918
self._raw_conflicts.append(('text conflict', trans_id))
923
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
925
merge_type=Merge3Merger,
926
interesting_ids=None,
930
interesting_files=None,
933
"""Primary interface for merging.
935
typical use is probably
936
'merge_inner(branch, branch.get_revision_tree(other_revision),
937
branch.get_revision_tree(base_revision))'
939
if this_tree is None:
940
warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
941
"bzrlib version 0.8.",
944
this_tree = this_branch.bzrdir.open_workingtree()
945
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
947
merger.backup_files = backup_files
948
merger.merge_type = merge_type
949
merger.interesting_ids = interesting_ids
950
merger.ignore_zero = ignore_zero
951
if interesting_files:
952
assert not interesting_ids, ('Only supply interesting_ids'
953
' or interesting_files')
954
merger._set_interesting_files(interesting_files)
955
merger.show_base = show_base
956
merger.reprocess = reprocess
957
merger.other_rev_id = other_rev_id
958
merger.other_basis = other_rev_id
959
return merger.do_merge()
962
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
963
"diff3": (Diff3Merger, "Merge using external diff3"),
964
'weave': (WeaveMerger, "Weave-based merge")
968
def merge_type_help():
969
templ = '%s%%7s: %%s' % (' '*12)
970
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
971
return '\n'.join(lines)