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.delta import compare_trees
26
from bzrlib.errors import (BzrCommandError,
36
WorkingTreeNotRevision,
39
from bzrlib.merge3 import Merge3
41
from bzrlib.osutils import rename, pathjoin, rmtree
42
from progress import DummyProgress, ProgressPhase
43
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
44
from bzrlib.symbol_versioning import *
45
from bzrlib.textfile import check_text_lines
46
from bzrlib.trace import mutter, warning, note
47
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
48
FinalPaths, create_by_entry, unique_add,
50
from bzrlib.versionedfile import WeaveMerge
53
# TODO: Report back as changes are merged in
55
def _get_tree(treespec, local_branch=None):
56
location, revno = treespec
57
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 = compare_trees(self.other_tree, other_basis_tree)
122
if changes.has_changed():
123
raise WorkingTreeNotRevision(self.this_tree)
124
other_rev_id = 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):
142
if self.this_basis is None:
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 = compare_trees(self.this_tree,
151
self.this_tree.basis_tree(), False)
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_pending_merge(self.other_rev_id)
190
def set_other(self, other_revision):
191
other_branch, self.other_tree = _get_tree(other_revision,
193
if other_revision[1] == -1:
194
self.other_rev_id = other_branch.last_revision()
195
if self.other_rev_id is None:
196
raise NoCommits(other_branch)
197
self.other_basis = self.other_rev_id
198
elif other_revision[1] is not None:
199
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
200
self.other_basis = self.other_rev_id
202
self.other_rev_id = None
203
self.other_basis = other_branch.last_revision()
204
if self.other_basis is None:
205
raise NoCommits(other_branch)
206
if other_branch.base != self.this_branch.base:
207
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
209
def set_base(self, base_revision):
210
mutter("doing merge() with no base_revision specified")
211
if base_revision == [None, None]:
213
pb = bzrlib.ui.ui_factory.nested_progress_bar()
215
this_repo = self.this_branch.repository
216
self.base_rev_id = common_ancestor(self.this_basis,
221
except NoCommonAncestor:
222
raise UnrelatedBranches()
223
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
225
self.base_is_ancestor = True
227
base_branch, self.base_tree = _get_tree(base_revision)
228
if base_revision[1] == -1:
229
self.base_rev_id = base_branch.last_revision()
230
elif base_revision[1] is None:
231
self.base_rev_id = None
233
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
234
if self.this_branch.base != base_branch.base:
235
self.this_branch.fetch(base_branch)
236
self.base_is_ancestor = is_ancestor(self.this_basis,
241
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
242
'other_tree': self.other_tree,
243
'interesting_ids': self.interesting_ids,
245
if self.merge_type.requires_base:
246
kwargs['base_tree'] = self.base_tree
247
if self.merge_type.supports_reprocess:
248
kwargs['reprocess'] = self.reprocess
250
raise BzrError("Conflict reduction is not supported for merge"
251
" type %s." % self.merge_type)
252
if self.merge_type.supports_show_base:
253
kwargs['show_base'] = self.show_base
255
raise BzrError("Showing base is not supported for this"
256
" merge type. %s" % self.merge_type)
257
merge = self.merge_type(pb=self._pb, **kwargs)
258
if len(merge.cooked_conflicts) == 0:
259
if not self.ignore_zero:
260
note("All changes applied successfully.")
262
note("%d conflicts encountered." % len(merge.cooked_conflicts))
264
return len(merge.cooked_conflicts)
266
def regen_inventory(self, new_entries):
267
old_entries = self.this_tree.read_working_inventory()
271
for path, file_id in new_entries:
274
new_entries_map[file_id] = path
276
def id2path(file_id):
277
path = new_entries_map.get(file_id)
280
entry = old_entries[file_id]
281
if entry.parent_id is None:
283
return pathjoin(id2path(entry.parent_id), entry.name)
285
for file_id in old_entries:
286
entry = old_entries[file_id]
287
path = id2path(file_id)
288
if file_id in self.base_tree.inventory:
289
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
291
executable = getattr(entry, 'executable', False)
292
new_inventory[file_id] = (path, file_id, entry.parent_id,
293
entry.kind, executable)
295
by_path[path] = file_id
300
for path, file_id in new_entries:
302
del new_inventory[file_id]
305
new_path_list.append((path, file_id))
306
if file_id not in old_entries:
308
# Ensure no file is added before its parent
310
for path, file_id in new_path_list:
314
parent = by_path[os.path.dirname(path)]
315
abspath = pathjoin(self.this_tree.basedir, path)
316
kind = bzrlib.osutils.file_kind(abspath)
317
if file_id in self.base_tree.inventory:
318
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
321
new_inventory[file_id] = (path, file_id, parent, kind, executable)
322
by_path[path] = file_id
324
# Get a list in insertion order
325
new_inventory_list = new_inventory.values()
326
mutter ("""Inventory regeneration:
327
old length: %i insertions: %i deletions: %i new_length: %i"""\
328
% (len(old_entries), insertions, deletions,
329
len(new_inventory_list)))
330
assert len(new_inventory_list) == len(old_entries) + insertions\
332
new_inventory_list.sort()
333
return new_inventory_list
336
class Merge3Merger(object):
337
"""Three-way merger that uses the merge3 text merger"""
339
supports_reprocess = True
340
supports_show_base = True
341
history_based = False
343
def __init__(self, working_tree, this_tree, base_tree, other_tree,
344
interesting_ids=None, reprocess=False, show_base=False,
345
pb=DummyProgress(), pp=None):
346
"""Initialize the merger object and perform the merge."""
347
object.__init__(self)
348
self.this_tree = working_tree
349
self.base_tree = base_tree
350
self.other_tree = other_tree
351
self._raw_conflicts = []
352
self.cooked_conflicts = []
353
self.reprocess = reprocess
354
self.show_base = show_base
358
self.pp = ProgressPhase("Merge phase", 3, self.pb)
360
if interesting_ids is not None:
361
all_ids = interesting_ids
363
all_ids = set(base_tree)
364
all_ids.update(other_tree)
365
working_tree.lock_write()
366
self.tt = TreeTransform(working_tree, self.pb)
369
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
371
for num, file_id in enumerate(all_ids):
372
child_pb.update('Preparing file merge', num, len(all_ids))
373
self.merge_names(file_id)
374
file_status = self.merge_contents(file_id)
375
self.merge_executable(file_id, file_status)
380
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
382
fs_conflicts = resolve_conflicts(self.tt, child_pb)
385
self.cook_conflicts(fs_conflicts)
386
for conflict in self.cooked_conflicts:
389
results = self.tt.apply()
390
self.write_modified(results)
392
working_tree.set_conflicts(ConflictList(self.cooked_conflicts))
393
except UnsupportedOperation:
400
working_tree.unlock()
405
self.tt.final_kind(self.tt.root)
407
self.tt.cancel_deletion(self.tt.root)
408
if self.tt.final_file_id(self.tt.root) is None:
409
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
411
if self.other_tree.inventory.root is None:
413
other_root_file_id = self.other_tree.inventory.root.file_id
414
other_root = self.tt.trans_id_file_id(other_root_file_id)
415
if other_root == self.tt.root:
418
self.tt.final_kind(other_root)
421
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
422
self.tt.cancel_creation(other_root)
423
self.tt.cancel_versioning(other_root)
425
def reparent_children(self, ie, target):
426
for child in ie.children:
427
if isinstance(child, str):
428
raise "Child of %r is a string: %r" % (ie.file_id, child)
429
trans_id = self.tt.trans_id_file_id(child.file_id)
430
self.tt.adjust_path(self.tt.final_name(trans_id), target)
432
def write_modified(self, results):
434
for path in results.modified_paths:
435
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
438
hash = self.this_tree.get_file_sha1(file_id)
441
modified_hashes[file_id] = hash
442
self.this_tree.set_merge_modified(modified_hashes)
445
def parent(entry, file_id):
446
"""Determine the parent for a file_id (used as a key method)"""
449
return entry.parent_id
452
def name(entry, file_id):
453
"""Determine the name for a file_id (used as a key method)"""
459
def contents_sha1(tree, file_id):
460
"""Determine the sha1 of the file contents (used as a key method)."""
461
if file_id not in tree:
463
return tree.get_file_sha1(file_id)
466
def executable(tree, file_id):
467
"""Determine the executability of a file-id (used as a key method)."""
468
if file_id not in tree:
470
if tree.kind(file_id) != "file":
472
return tree.is_executable(file_id)
475
def kind(tree, file_id):
476
"""Determine the kind of a file-id (used as a key method)."""
477
if file_id not in tree:
479
return tree.kind(file_id)
482
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
483
"""Do a three-way test on a scalar.
484
Return "this", "other" or "conflict", depending whether a value wins.
486
key_base = key(base_tree, file_id)
487
key_other = key(other_tree, file_id)
488
#if base == other, either they all agree, or only THIS has changed.
489
if key_base == key_other:
491
key_this = key(this_tree, file_id)
492
if key_this not in (key_base, key_other):
494
# "Ambiguous clean merge"
495
elif key_this == key_other:
498
assert key_this == key_base
501
def merge_names(self, file_id):
502
"""Perform a merge on file_id names and parents"""
504
if file_id in tree.inventory:
505
return tree.inventory[file_id]
508
this_entry = get_entry(self.this_tree)
509
other_entry = get_entry(self.other_tree)
510
base_entry = get_entry(self.base_tree)
511
name_winner = self.scalar_three_way(this_entry, base_entry,
512
other_entry, file_id, self.name)
513
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
514
other_entry, file_id,
516
if this_entry is None:
517
if name_winner == "this":
518
name_winner = "other"
519
if parent_id_winner == "this":
520
parent_id_winner = "other"
521
if name_winner == "this" and parent_id_winner == "this":
523
if name_winner == "conflict":
524
trans_id = self.tt.trans_id_file_id(file_id)
525
self._raw_conflicts.append(('name conflict', trans_id,
526
self.name(this_entry, file_id),
527
self.name(other_entry, file_id)))
528
if parent_id_winner == "conflict":
529
trans_id = self.tt.trans_id_file_id(file_id)
530
self._raw_conflicts.append(('parent conflict', trans_id,
531
self.parent(this_entry, file_id),
532
self.parent(other_entry, file_id)))
533
if other_entry is None:
534
# it doesn't matter whether the result was 'other' or
535
# 'conflict'-- if there's no 'other', we leave it alone.
537
# if we get here, name_winner and parent_winner are set to safe values.
538
winner_entry = {"this": this_entry, "other": other_entry,
539
"conflict": other_entry}
540
trans_id = self.tt.trans_id_file_id(file_id)
541
parent_id = winner_entry[parent_id_winner].parent_id
542
if parent_id is not None:
543
parent_trans_id = self.tt.trans_id_file_id(parent_id)
544
self.tt.adjust_path(winner_entry[name_winner].name,
545
parent_trans_id, trans_id)
547
def merge_contents(self, file_id):
548
"""Performa a merge on file_id contents."""
549
def contents_pair(tree):
550
if file_id not in tree:
552
kind = tree.kind(file_id)
554
contents = tree.get_file_sha1(file_id)
555
elif kind == "symlink":
556
contents = tree.get_symlink_target(file_id)
559
return kind, contents
561
def contents_conflict():
562
trans_id = self.tt.trans_id_file_id(file_id)
563
name = self.tt.final_name(trans_id)
564
parent_id = self.tt.final_parent(trans_id)
565
if file_id in self.this_tree.inventory:
566
self.tt.unversion_file(trans_id)
567
self.tt.delete_contents(trans_id)
568
file_group = self._dump_conflicts(name, parent_id, file_id,
570
self._raw_conflicts.append(('contents conflict', file_group))
572
# See SPOT run. run, SPOT, run.
573
# So we're not QUITE repeating ourselves; we do tricky things with
575
base_pair = contents_pair(self.base_tree)
576
other_pair = contents_pair(self.other_tree)
577
if base_pair == other_pair:
578
# OTHER introduced no changes
580
this_pair = contents_pair(self.this_tree)
581
if this_pair == other_pair:
582
# THIS and OTHER introduced the same changes
585
trans_id = self.tt.trans_id_file_id(file_id)
586
if this_pair == base_pair:
587
# only OTHER introduced changes
588
if file_id in self.this_tree:
589
# Remove any existing contents
590
self.tt.delete_contents(trans_id)
591
if file_id in self.other_tree:
592
# OTHER changed the file
593
create_by_entry(self.tt,
594
self.other_tree.inventory[file_id],
595
self.other_tree, trans_id)
596
if file_id not in self.this_tree.inventory:
597
self.tt.version_file(file_id, trans_id)
599
elif file_id in self.this_tree.inventory:
600
# OTHER deleted the file
601
self.tt.unversion_file(trans_id)
603
#BOTH THIS and OTHER introduced changes; scalar conflict
604
elif this_pair[0] == "file" and other_pair[0] == "file":
605
# THIS and OTHER are both files, so text merge. Either
606
# BASE is a file, or both converted to files, so at least we
607
# have agreement that output should be a file.
609
self.text_merge(file_id, trans_id)
611
return contents_conflict()
612
if file_id not in self.this_tree.inventory:
613
self.tt.version_file(file_id, trans_id)
615
self.tt.tree_kind(trans_id)
616
self.tt.delete_contents(trans_id)
621
# Scalar conflict, can't text merge. Dump conflicts
622
return contents_conflict()
624
def get_lines(self, tree, file_id):
625
"""Return the lines in a file, or an empty list."""
627
return tree.get_file(file_id).readlines()
631
def text_merge(self, file_id, trans_id):
632
"""Perform a three-way text merge on a file_id"""
633
# it's possible that we got here with base as a different type.
634
# if so, we just want two-way text conflicts.
635
if file_id in self.base_tree and \
636
self.base_tree.kind(file_id) == "file":
637
base_lines = self.get_lines(self.base_tree, file_id)
640
other_lines = self.get_lines(self.other_tree, file_id)
641
this_lines = self.get_lines(self.this_tree, file_id)
642
m3 = Merge3(base_lines, this_lines, other_lines)
643
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
644
if self.show_base is True:
645
base_marker = '|' * 7
649
def iter_merge3(retval):
650
retval["text_conflicts"] = False
651
for line in m3.merge_lines(name_a = "TREE",
652
name_b = "MERGE-SOURCE",
653
name_base = "BASE-REVISION",
654
start_marker=start_marker,
655
base_marker=base_marker,
656
reprocess=self.reprocess):
657
if line.startswith(start_marker):
658
retval["text_conflicts"] = True
659
yield line.replace(start_marker, '<' * 7)
663
merge3_iterator = iter_merge3(retval)
664
self.tt.create_file(merge3_iterator, trans_id)
665
if retval["text_conflicts"] is True:
666
self._raw_conflicts.append(('text conflict', trans_id))
667
name = self.tt.final_name(trans_id)
668
parent_id = self.tt.final_parent(trans_id)
669
file_group = self._dump_conflicts(name, parent_id, file_id,
670
this_lines, base_lines,
672
file_group.append(trans_id)
674
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
675
base_lines=None, other_lines=None, set_version=False,
677
"""Emit conflict files.
678
If this_lines, base_lines, or other_lines are omitted, they will be
679
determined automatically. If set_version is true, the .OTHER, .THIS
680
or .BASE (in that order) will be created as versioned files.
682
data = [('OTHER', self.other_tree, other_lines),
683
('THIS', self.this_tree, this_lines)]
685
data.append(('BASE', self.base_tree, base_lines))
688
for suffix, tree, lines in data:
690
trans_id = self._conflict_file(name, parent_id, tree, file_id,
692
file_group.append(trans_id)
693
if set_version and not versioned:
694
self.tt.version_file(file_id, trans_id)
698
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
700
"""Emit a single conflict file."""
701
name = name + '.' + suffix
702
trans_id = self.tt.create_path(name, parent_id)
703
entry = tree.inventory[file_id]
704
create_by_entry(self.tt, entry, tree, trans_id, lines)
707
def merge_executable(self, file_id, file_status):
708
"""Perform a merge on the execute bit."""
709
if file_status == "deleted":
711
trans_id = self.tt.trans_id_file_id(file_id)
713
if self.tt.final_kind(trans_id) != "file":
717
winner = self.scalar_three_way(self.this_tree, self.base_tree,
718
self.other_tree, file_id,
720
if winner == "conflict":
721
# There must be a None in here, if we have a conflict, but we
722
# need executability since file status was not deleted.
723
if self.other_tree.is_executable(file_id) is None:
728
if file_status == "modified":
729
executability = self.this_tree.is_executable(file_id)
730
if executability is not None:
731
trans_id = self.tt.trans_id_file_id(file_id)
732
self.tt.set_executability(executability, trans_id)
734
assert winner == "other"
735
if file_id in self.other_tree:
736
executability = self.other_tree.is_executable(file_id)
737
elif file_id in self.this_tree:
738
executability = self.this_tree.is_executable(file_id)
739
elif file_id in self.base_tree:
740
executability = self.base_tree.is_executable(file_id)
741
if executability is not None:
742
trans_id = self.tt.trans_id_file_id(file_id)
743
self.tt.set_executability(executability, trans_id)
745
def cook_conflicts(self, fs_conflicts):
746
"""Convert all conflicts into a form that doesn't depend on trans_id"""
747
from conflicts import Conflict
749
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
750
fp = FinalPaths(self.tt)
751
for conflict in self._raw_conflicts:
752
conflict_type = conflict[0]
753
if conflict_type in ('name conflict', 'parent conflict'):
754
trans_id = conflict[1]
755
conflict_args = conflict[2:]
756
if trans_id not in name_conflicts:
757
name_conflicts[trans_id] = {}
758
unique_add(name_conflicts[trans_id], conflict_type,
760
if conflict_type == 'contents conflict':
761
for trans_id in conflict[1]:
762
file_id = self.tt.final_file_id(trans_id)
763
if file_id is not None:
765
path = fp.get_path(trans_id)
766
for suffix in ('.BASE', '.THIS', '.OTHER'):
767
if path.endswith(suffix):
768
path = path[:-len(suffix)]
770
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
771
self.cooked_conflicts.append(c)
772
if conflict_type == 'text conflict':
773
trans_id = conflict[1]
774
path = fp.get_path(trans_id)
775
file_id = self.tt.final_file_id(trans_id)
776
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
777
self.cooked_conflicts.append(c)
779
for trans_id, conflicts in name_conflicts.iteritems():
781
this_parent, other_parent = conflicts['parent conflict']
782
assert this_parent != other_parent
784
this_parent = other_parent = \
785
self.tt.final_file_id(self.tt.final_parent(trans_id))
787
this_name, other_name = conflicts['name conflict']
788
assert this_name != other_name
790
this_name = other_name = self.tt.final_name(trans_id)
791
other_path = fp.get_path(trans_id)
792
if this_parent is not None:
794
fp.get_path(self.tt.trans_id_file_id(this_parent))
795
this_path = pathjoin(this_parent_path, this_name)
797
this_path = "<deleted>"
798
file_id = self.tt.final_file_id(trans_id)
799
c = Conflict.factory('path conflict', path=this_path,
800
conflict_path=other_path, file_id=file_id)
801
self.cooked_conflicts.append(c)
802
self.cooked_conflicts.sort(key=Conflict.sort_key)
805
class WeaveMerger(Merge3Merger):
806
"""Three-way tree merger, text weave merger."""
807
supports_reprocess = True
808
supports_show_base = False
810
def __init__(self, working_tree, this_tree, base_tree, other_tree,
811
interesting_ids=None, pb=DummyProgress(), pp=None,
813
self.this_revision_tree = self._get_revision_tree(this_tree)
814
self.other_revision_tree = self._get_revision_tree(other_tree)
815
super(WeaveMerger, self).__init__(working_tree, this_tree,
816
base_tree, other_tree,
817
interesting_ids=interesting_ids,
818
pb=pb, pp=pp, reprocess=reprocess)
820
def _get_revision_tree(self, tree):
821
"""Return a revision tree releated to this tree.
822
If the tree is a WorkingTree, the basis will be returned.
824
if getattr(tree, 'get_weave', False) is False:
825
# If we have a WorkingTree, try using the basis
826
return tree.branch.basis_tree()
830
def _check_file(self, file_id):
831
"""Check that the revision tree's version of the file matches."""
832
for tree, rt in ((self.this_tree, self.this_revision_tree),
833
(self.other_tree, self.other_revision_tree)):
836
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
837
raise WorkingTreeNotRevision(self.this_tree)
839
def _merged_lines(self, file_id):
840
"""Generate the merged lines.
841
There is no distinction between lines that are meant to contain <<<<<<<
844
weave = self.this_revision_tree.get_weave(file_id)
845
this_revision_id = self.this_revision_tree.inventory[file_id].revision
846
other_revision_id = \
847
self.other_revision_tree.inventory[file_id].revision
848
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
849
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
850
return wm.merge_lines(self.reprocess)
852
def text_merge(self, file_id, trans_id):
853
"""Perform a (weave) text merge for a given file and file-id.
854
If conflicts are encountered, .THIS and .OTHER files will be emitted,
855
and a conflict will be noted.
857
self._check_file(file_id)
858
lines, conflicts = self._merged_lines(file_id)
860
# Note we're checking whether the OUTPUT is binary in this case,
861
# because we don't want to get into weave merge guts.
862
check_text_lines(lines)
863
self.tt.create_file(lines, trans_id)
865
self._raw_conflicts.append(('text conflict', trans_id))
866
name = self.tt.final_name(trans_id)
867
parent_id = self.tt.final_parent(trans_id)
868
file_group = self._dump_conflicts(name, parent_id, file_id,
870
file_group.append(trans_id)
873
class Diff3Merger(Merge3Merger):
874
"""Three-way merger using external diff3 for text merging"""
875
def dump_file(self, temp_dir, name, tree, file_id):
876
out_path = pathjoin(temp_dir, name)
877
out_file = file(out_path, "wb")
878
in_file = tree.get_file(file_id)
883
def text_merge(self, file_id, trans_id):
884
"""Perform a diff3 merge using a specified file-id and trans-id.
885
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
886
will be dumped, and a will be conflict noted.
889
temp_dir = mkdtemp(prefix="bzr-")
891
new_file = pathjoin(temp_dir, "new")
892
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
893
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
894
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
895
status = bzrlib.patch.diff3(new_file, this, base, other)
896
if status not in (0, 1):
897
raise BzrError("Unhandled diff3 exit code")
898
self.tt.create_file(file(new_file, "rb"), trans_id)
900
name = self.tt.final_name(trans_id)
901
parent_id = self.tt.final_parent(trans_id)
902
self._dump_conflicts(name, parent_id, file_id)
903
self._raw_conflicts.append(('text conflict', trans_id))
908
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
910
merge_type=Merge3Merger,
911
interesting_ids=None,
915
interesting_files=None,
918
"""Primary interface for merging.
920
typical use is probably
921
'merge_inner(branch, branch.get_revision_tree(other_revision),
922
branch.get_revision_tree(base_revision))'
924
if this_tree is None:
925
warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
926
"bzrlib version 0.8.",
929
this_tree = this_branch.bzrdir.open_workingtree()
930
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
932
merger.backup_files = backup_files
933
merger.merge_type = merge_type
934
merger.interesting_ids = interesting_ids
935
merger.ignore_zero = ignore_zero
936
if interesting_files:
937
assert not interesting_ids, ('Only supply interesting_ids'
938
' or interesting_files')
939
merger._set_interesting_files(interesting_files)
940
merger.show_base = show_base
941
merger.reprocess = reprocess
942
merger.other_rev_id = other_rev_id
943
merger.other_basis = other_rev_id
944
return merger.do_merge()
947
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
948
"diff3": (Diff3Merger, "Merge using external diff3"),
949
'weave': (WeaveMerger, "Weave-based merge")
953
def merge_type_help():
954
templ = '%s%%7s: %%s' % (' '*12)
955
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
956
return '\n'.join(lines)