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
25
from bzrlib.branch import Branch
26
from bzrlib.conflicts import ConflictList, Conflict
27
from bzrlib.errors import (BzrCommandError,
37
WorkingTreeNotRevision,
40
from bzrlib.merge3 import Merge3
41
from bzrlib.osutils import rename, pathjoin
42
from progress import DummyProgress, ProgressPhase
43
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
44
from bzrlib.textfile import check_text_lines
45
from bzrlib.trace import mutter, warning, note
46
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
47
FinalPaths, create_by_entry, unique_add,
49
from bzrlib.versionedfile import WeaveMerge
52
# TODO: Report back as changes are merged in
54
def _get_tree(treespec, local_branch=None):
55
from bzrlib import workingtree
56
location, revno = treespec
58
tree = workingtree.WorkingTree.open_containing(location)[0]
59
return tree.branch, tree
60
branch = Branch.open_containing(location)[0]
62
revision = branch.last_revision()
64
revision = branch.get_rev_id(revno)
66
revision = NULL_REVISION
67
return branch, _get_revid_tree(branch, revision, local_branch)
70
def _get_revid_tree(branch, revision, local_branch):
72
base_tree = branch.bzrdir.open_workingtree()
74
if local_branch is not None:
75
if local_branch.base != branch.base:
76
local_branch.fetch(branch, revision)
77
base_tree = local_branch.repository.revision_tree(revision)
79
base_tree = branch.repository.revision_tree(revision)
83
def transform_tree(from_tree, to_tree, interesting_ids=None):
84
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
85
interesting_ids=interesting_ids, this_tree=from_tree)
89
def __init__(self, this_branch, other_tree=None, base_tree=None,
90
this_tree=None, pb=DummyProgress()):
92
assert this_tree is not None, "this_tree is required"
93
self.this_branch = this_branch
94
self.this_basis = this_branch.last_revision()
95
self.this_rev_id = None
96
self.this_tree = this_tree
97
self.this_revision_tree = None
98
self.this_basis_tree = None
99
self.other_tree = other_tree
100
self.base_tree = base_tree
101
self.ignore_zero = False
102
self.backup_files = False
103
self.interesting_ids = None
104
self.show_base = False
105
self.reprocess = False
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."
144
" (perhaps you would prefer 'bzr pull')")
147
if self.this_basis != self.this_rev_id:
148
raise BzrCommandError("Working tree has uncommitted changes.")
150
def compare_basis(self):
151
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
152
if not changes.has_changed():
153
self.this_rev_id = self.this_basis
155
def set_interesting_files(self, file_list):
157
self._set_interesting_files(file_list)
158
except NotVersionedError, e:
159
raise BzrCommandError("%s is not a source file in any"
162
def _set_interesting_files(self, file_list):
163
"""Set the list of interesting ids from a list of files."""
164
if file_list is None:
165
self.interesting_ids = None
168
interesting_ids = set()
169
for path in file_list:
171
for tree in (self.this_tree, self.base_tree, self.other_tree):
172
file_id = tree.inventory.path2id(path)
173
if file_id is not None:
174
interesting_ids.add(file_id)
177
raise NotVersionedError(path=path)
178
self.interesting_ids = interesting_ids
180
def set_pending(self):
181
if not self.base_is_ancestor:
183
if self.other_rev_id is None:
185
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
186
if self.other_rev_id in ancestry:
188
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
190
def set_other(self, other_revision):
191
"""Set the revision and tree to merge from.
193
This sets the other_tree, other_rev_id, other_basis attributes.
195
:param other_revision: The [path, revision] list to merge from.
197
other_branch, self.other_tree = _get_tree(other_revision,
199
if other_revision[1] == -1:
200
self.other_rev_id = other_branch.last_revision()
201
if self.other_rev_id is None:
202
raise NoCommits(other_branch)
203
self.other_basis = self.other_rev_id
204
elif other_revision[1] is not None:
205
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
206
self.other_basis = self.other_rev_id
208
self.other_rev_id = None
209
self.other_basis = other_branch.last_revision()
210
if self.other_basis is None:
211
raise NoCommits(other_branch)
212
if other_branch.base != self.this_branch.base:
213
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
216
self.set_base([None, None])
218
def set_base(self, base_revision):
219
"""Set the base revision to use for the merge.
221
:param base_revision: A 2-list containing a path and revision number.
223
mutter("doing merge() with no base_revision specified")
224
if base_revision == [None, None]:
226
pb = ui.ui_factory.nested_progress_bar()
228
this_repo = self.this_branch.repository
229
self.base_rev_id = common_ancestor(self.this_basis,
234
except NoCommonAncestor:
235
raise UnrelatedBranches()
236
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
238
self.base_is_ancestor = True
240
base_branch, self.base_tree = _get_tree(base_revision)
241
if base_revision[1] == -1:
242
self.base_rev_id = base_branch.last_revision()
243
elif base_revision[1] is None:
244
self.base_rev_id = None
246
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
247
if self.this_branch.base != base_branch.base:
248
self.this_branch.fetch(base_branch)
249
self.base_is_ancestor = is_ancestor(self.this_basis,
254
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
255
'other_tree': self.other_tree,
256
'interesting_ids': self.interesting_ids,
258
if self.merge_type.requires_base:
259
kwargs['base_tree'] = self.base_tree
260
if self.merge_type.supports_reprocess:
261
kwargs['reprocess'] = self.reprocess
263
raise BzrError("Conflict reduction is not supported for merge"
264
" type %s." % self.merge_type)
265
if self.merge_type.supports_show_base:
266
kwargs['show_base'] = self.show_base
268
raise BzrError("Showing base is not supported for this"
269
" merge type. %s" % self.merge_type)
270
self.this_tree.lock_tree_write()
271
if self.base_tree is not None:
272
self.base_tree.lock_read()
273
if self.other_tree is not None:
274
self.other_tree.lock_read()
276
merge = self.merge_type(pb=self._pb, **kwargs)
278
if self.other_tree is not None:
279
self.other_tree.unlock()
280
if self.base_tree is not None:
281
self.base_tree.unlock()
282
self.this_tree.unlock()
283
if len(merge.cooked_conflicts) == 0:
284
if not self.ignore_zero:
285
note("All changes applied successfully.")
287
note("%d conflicts encountered." % len(merge.cooked_conflicts))
289
return len(merge.cooked_conflicts)
291
def regen_inventory(self, new_entries):
292
old_entries = self.this_tree.read_working_inventory()
296
for path, file_id in new_entries:
299
new_entries_map[file_id] = path
301
def id2path(file_id):
302
path = new_entries_map.get(file_id)
305
entry = old_entries[file_id]
306
if entry.parent_id is None:
308
return pathjoin(id2path(entry.parent_id), entry.name)
310
for file_id in old_entries:
311
entry = old_entries[file_id]
312
path = id2path(file_id)
313
if file_id in self.base_tree.inventory:
314
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
316
executable = getattr(entry, 'executable', False)
317
new_inventory[file_id] = (path, file_id, entry.parent_id,
318
entry.kind, executable)
320
by_path[path] = file_id
325
for path, file_id in new_entries:
327
del new_inventory[file_id]
330
new_path_list.append((path, file_id))
331
if file_id not in old_entries:
333
# Ensure no file is added before its parent
335
for path, file_id in new_path_list:
339
parent = by_path[os.path.dirname(path)]
340
abspath = pathjoin(self.this_tree.basedir, path)
341
kind = osutils.file_kind(abspath)
342
if file_id in self.base_tree.inventory:
343
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
346
new_inventory[file_id] = (path, file_id, parent, kind, executable)
347
by_path[path] = file_id
349
# Get a list in insertion order
350
new_inventory_list = new_inventory.values()
351
mutter ("""Inventory regeneration:
352
old length: %i insertions: %i deletions: %i new_length: %i"""\
353
% (len(old_entries), insertions, deletions,
354
len(new_inventory_list)))
355
assert len(new_inventory_list) == len(old_entries) + insertions\
357
new_inventory_list.sort()
358
return new_inventory_list
361
class Merge3Merger(object):
362
"""Three-way merger that uses the merge3 text merger"""
364
supports_reprocess = True
365
supports_show_base = True
366
history_based = False
368
def __init__(self, working_tree, this_tree, base_tree, other_tree,
369
interesting_ids=None, reprocess=False, show_base=False,
370
pb=DummyProgress(), pp=None):
371
"""Initialize the merger object and perform the merge."""
372
object.__init__(self)
373
self.this_tree = working_tree
374
self.base_tree = base_tree
375
self.other_tree = other_tree
376
self._raw_conflicts = []
377
self.cooked_conflicts = []
378
self.reprocess = reprocess
379
self.show_base = show_base
383
self.pp = ProgressPhase("Merge phase", 3, self.pb)
385
if interesting_ids is not None:
386
all_ids = interesting_ids
388
all_ids = set(base_tree)
389
all_ids.update(other_tree)
390
working_tree.lock_tree_write()
391
self.tt = TreeTransform(working_tree, self.pb)
394
child_pb = ui.ui_factory.nested_progress_bar()
396
for num, file_id in enumerate(all_ids):
397
child_pb.update('Preparing file merge', num, len(all_ids))
398
self.merge_names(file_id)
399
file_status = self.merge_contents(file_id)
400
self.merge_executable(file_id, file_status)
405
child_pb = ui.ui_factory.nested_progress_bar()
407
fs_conflicts = resolve_conflicts(self.tt, child_pb)
410
self.cook_conflicts(fs_conflicts)
411
for conflict in self.cooked_conflicts:
414
results = self.tt.apply()
415
self.write_modified(results)
417
working_tree.add_conflicts(self.cooked_conflicts)
418
except UnsupportedOperation:
422
working_tree.unlock()
427
self.tt.final_kind(self.tt.root)
429
self.tt.cancel_deletion(self.tt.root)
430
if self.tt.final_file_id(self.tt.root) is None:
431
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
433
if self.other_tree.inventory.root is None:
435
other_root_file_id = self.other_tree.inventory.root.file_id
436
other_root = self.tt.trans_id_file_id(other_root_file_id)
437
if other_root == self.tt.root:
440
self.tt.final_kind(other_root)
443
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
444
self.tt.cancel_creation(other_root)
445
self.tt.cancel_versioning(other_root)
447
def reparent_children(self, ie, target):
448
for thing, child in ie.children.iteritems():
449
trans_id = self.tt.trans_id_file_id(child.file_id)
450
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
452
def write_modified(self, results):
454
for path in results.modified_paths:
455
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
458
hash = self.this_tree.get_file_sha1(file_id)
461
modified_hashes[file_id] = hash
462
self.this_tree.set_merge_modified(modified_hashes)
465
def parent(entry, file_id):
466
"""Determine the parent for a file_id (used as a key method)"""
469
return entry.parent_id
472
def name(entry, file_id):
473
"""Determine the name for a file_id (used as a key method)"""
479
def contents_sha1(tree, file_id):
480
"""Determine the sha1 of the file contents (used as a key method)."""
481
if file_id not in tree:
483
return tree.get_file_sha1(file_id)
486
def executable(tree, file_id):
487
"""Determine the executability of a file-id (used as a key method)."""
488
if file_id not in tree:
490
if tree.kind(file_id) != "file":
492
return tree.is_executable(file_id)
495
def kind(tree, file_id):
496
"""Determine the kind of a file-id (used as a key method)."""
497
if file_id not in tree:
499
return tree.kind(file_id)
502
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
503
"""Do a three-way test on a scalar.
504
Return "this", "other" or "conflict", depending whether a value wins.
506
key_base = key(base_tree, file_id)
507
key_other = key(other_tree, file_id)
508
#if base == other, either they all agree, or only THIS has changed.
509
if key_base == key_other:
511
key_this = key(this_tree, file_id)
512
if key_this not in (key_base, key_other):
514
# "Ambiguous clean merge"
515
elif key_this == key_other:
518
assert key_this == key_base
521
def merge_names(self, file_id):
522
"""Perform a merge on file_id names and parents"""
524
if file_id in tree.inventory:
525
return tree.inventory[file_id]
528
this_entry = get_entry(self.this_tree)
529
other_entry = get_entry(self.other_tree)
530
base_entry = get_entry(self.base_tree)
531
name_winner = self.scalar_three_way(this_entry, base_entry,
532
other_entry, file_id, self.name)
533
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
534
other_entry, file_id,
536
if this_entry is None:
537
if name_winner == "this":
538
name_winner = "other"
539
if parent_id_winner == "this":
540
parent_id_winner = "other"
541
if name_winner == "this" and parent_id_winner == "this":
543
if name_winner == "conflict":
544
trans_id = self.tt.trans_id_file_id(file_id)
545
self._raw_conflicts.append(('name conflict', trans_id,
546
self.name(this_entry, file_id),
547
self.name(other_entry, file_id)))
548
if parent_id_winner == "conflict":
549
trans_id = self.tt.trans_id_file_id(file_id)
550
self._raw_conflicts.append(('parent conflict', trans_id,
551
self.parent(this_entry, file_id),
552
self.parent(other_entry, file_id)))
553
if other_entry is None:
554
# it doesn't matter whether the result was 'other' or
555
# 'conflict'-- if there's no 'other', we leave it alone.
557
# if we get here, name_winner and parent_winner are set to safe values.
558
winner_entry = {"this": this_entry, "other": other_entry,
559
"conflict": other_entry}
560
trans_id = self.tt.trans_id_file_id(file_id)
561
parent_id = winner_entry[parent_id_winner].parent_id
562
if parent_id is not None:
563
parent_trans_id = self.tt.trans_id_file_id(parent_id)
564
self.tt.adjust_path(winner_entry[name_winner].name,
565
parent_trans_id, trans_id)
567
def merge_contents(self, file_id):
568
"""Performa a merge on file_id contents."""
569
def contents_pair(tree):
570
if file_id not in tree:
572
kind = tree.kind(file_id)
574
contents = tree.get_file_sha1(file_id)
575
elif kind == "symlink":
576
contents = tree.get_symlink_target(file_id)
579
return kind, contents
581
def contents_conflict():
582
trans_id = self.tt.trans_id_file_id(file_id)
583
name = self.tt.final_name(trans_id)
584
parent_id = self.tt.final_parent(trans_id)
585
if file_id in self.this_tree.inventory:
586
self.tt.unversion_file(trans_id)
587
if file_id in self.this_tree:
588
self.tt.delete_contents(trans_id)
589
file_group = self._dump_conflicts(name, parent_id, file_id,
591
self._raw_conflicts.append(('contents conflict', file_group))
593
# See SPOT run. run, SPOT, run.
594
# So we're not QUITE repeating ourselves; we do tricky things with
596
base_pair = contents_pair(self.base_tree)
597
other_pair = contents_pair(self.other_tree)
598
if base_pair == other_pair:
599
# OTHER introduced no changes
601
this_pair = contents_pair(self.this_tree)
602
if this_pair == other_pair:
603
# THIS and OTHER introduced the same changes
606
trans_id = self.tt.trans_id_file_id(file_id)
607
if this_pair == base_pair:
608
# only OTHER introduced changes
609
if file_id in self.this_tree:
610
# Remove any existing contents
611
self.tt.delete_contents(trans_id)
612
if file_id in self.other_tree:
613
# OTHER changed the file
614
create_by_entry(self.tt,
615
self.other_tree.inventory[file_id],
616
self.other_tree, trans_id)
617
if file_id not in self.this_tree.inventory:
618
self.tt.version_file(file_id, trans_id)
620
elif file_id in self.this_tree.inventory:
621
# OTHER deleted the file
622
self.tt.unversion_file(trans_id)
624
#BOTH THIS and OTHER introduced changes; scalar conflict
625
elif this_pair[0] == "file" and other_pair[0] == "file":
626
# THIS and OTHER are both files, so text merge. Either
627
# BASE is a file, or both converted to files, so at least we
628
# have agreement that output should be a file.
630
self.text_merge(file_id, trans_id)
632
return contents_conflict()
633
if file_id not in self.this_tree.inventory:
634
self.tt.version_file(file_id, trans_id)
636
self.tt.tree_kind(trans_id)
637
self.tt.delete_contents(trans_id)
642
# Scalar conflict, can't text merge. Dump conflicts
643
return contents_conflict()
645
def get_lines(self, tree, file_id):
646
"""Return the lines in a file, or an empty list."""
648
return tree.get_file(file_id).readlines()
652
def text_merge(self, file_id, trans_id):
653
"""Perform a three-way text merge on a file_id"""
654
# it's possible that we got here with base as a different type.
655
# if so, we just want two-way text conflicts.
656
if file_id in self.base_tree and \
657
self.base_tree.kind(file_id) == "file":
658
base_lines = self.get_lines(self.base_tree, file_id)
661
other_lines = self.get_lines(self.other_tree, file_id)
662
this_lines = self.get_lines(self.this_tree, file_id)
663
m3 = Merge3(base_lines, this_lines, other_lines)
664
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
665
if self.show_base is True:
666
base_marker = '|' * 7
670
def iter_merge3(retval):
671
retval["text_conflicts"] = False
672
for line in m3.merge_lines(name_a = "TREE",
673
name_b = "MERGE-SOURCE",
674
name_base = "BASE-REVISION",
675
start_marker=start_marker,
676
base_marker=base_marker,
677
reprocess=self.reprocess):
678
if line.startswith(start_marker):
679
retval["text_conflicts"] = True
680
yield line.replace(start_marker, '<' * 7)
684
merge3_iterator = iter_merge3(retval)
685
self.tt.create_file(merge3_iterator, trans_id)
686
if retval["text_conflicts"] is True:
687
self._raw_conflicts.append(('text conflict', trans_id))
688
name = self.tt.final_name(trans_id)
689
parent_id = self.tt.final_parent(trans_id)
690
file_group = self._dump_conflicts(name, parent_id, file_id,
691
this_lines, base_lines,
693
file_group.append(trans_id)
695
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
696
base_lines=None, other_lines=None, set_version=False,
698
"""Emit conflict files.
699
If this_lines, base_lines, or other_lines are omitted, they will be
700
determined automatically. If set_version is true, the .OTHER, .THIS
701
or .BASE (in that order) will be created as versioned files.
703
data = [('OTHER', self.other_tree, other_lines),
704
('THIS', self.this_tree, this_lines)]
706
data.append(('BASE', self.base_tree, base_lines))
709
for suffix, tree, lines in data:
711
trans_id = self._conflict_file(name, parent_id, tree, file_id,
713
file_group.append(trans_id)
714
if set_version and not versioned:
715
self.tt.version_file(file_id, trans_id)
719
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
721
"""Emit a single conflict file."""
722
name = name + '.' + suffix
723
trans_id = self.tt.create_path(name, parent_id)
724
entry = tree.inventory[file_id]
725
create_by_entry(self.tt, entry, tree, trans_id, lines)
728
def merge_executable(self, file_id, file_status):
729
"""Perform a merge on the execute bit."""
730
if file_status == "deleted":
732
trans_id = self.tt.trans_id_file_id(file_id)
734
if self.tt.final_kind(trans_id) != "file":
738
winner = self.scalar_three_way(self.this_tree, self.base_tree,
739
self.other_tree, file_id,
741
if winner == "conflict":
742
# There must be a None in here, if we have a conflict, but we
743
# need executability since file status was not deleted.
744
if self.executable(self.other_tree, file_id) is None:
749
if file_status == "modified":
750
executability = self.this_tree.is_executable(file_id)
751
if executability is not None:
752
trans_id = self.tt.trans_id_file_id(file_id)
753
self.tt.set_executability(executability, trans_id)
755
assert winner == "other"
756
if file_id in self.other_tree:
757
executability = self.other_tree.is_executable(file_id)
758
elif file_id in self.this_tree:
759
executability = self.this_tree.is_executable(file_id)
760
elif file_id in self.base_tree:
761
executability = self.base_tree.is_executable(file_id)
762
if executability is not None:
763
trans_id = self.tt.trans_id_file_id(file_id)
764
self.tt.set_executability(executability, trans_id)
766
def cook_conflicts(self, fs_conflicts):
767
"""Convert all conflicts into a form that doesn't depend on trans_id"""
768
from conflicts import Conflict
770
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
771
fp = FinalPaths(self.tt)
772
for conflict in self._raw_conflicts:
773
conflict_type = conflict[0]
774
if conflict_type in ('name conflict', 'parent conflict'):
775
trans_id = conflict[1]
776
conflict_args = conflict[2:]
777
if trans_id not in name_conflicts:
778
name_conflicts[trans_id] = {}
779
unique_add(name_conflicts[trans_id], conflict_type,
781
if conflict_type == 'contents conflict':
782
for trans_id in conflict[1]:
783
file_id = self.tt.final_file_id(trans_id)
784
if file_id is not None:
786
path = fp.get_path(trans_id)
787
for suffix in ('.BASE', '.THIS', '.OTHER'):
788
if path.endswith(suffix):
789
path = path[:-len(suffix)]
791
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
792
self.cooked_conflicts.append(c)
793
if conflict_type == 'text conflict':
794
trans_id = conflict[1]
795
path = fp.get_path(trans_id)
796
file_id = self.tt.final_file_id(trans_id)
797
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
798
self.cooked_conflicts.append(c)
800
for trans_id, conflicts in name_conflicts.iteritems():
802
this_parent, other_parent = conflicts['parent conflict']
803
assert this_parent != other_parent
805
this_parent = other_parent = \
806
self.tt.final_file_id(self.tt.final_parent(trans_id))
808
this_name, other_name = conflicts['name conflict']
809
assert this_name != other_name
811
this_name = other_name = self.tt.final_name(trans_id)
812
other_path = fp.get_path(trans_id)
813
if this_parent is not None:
815
fp.get_path(self.tt.trans_id_file_id(this_parent))
816
this_path = pathjoin(this_parent_path, this_name)
818
this_path = "<deleted>"
819
file_id = self.tt.final_file_id(trans_id)
820
c = Conflict.factory('path conflict', path=this_path,
821
conflict_path=other_path, file_id=file_id)
822
self.cooked_conflicts.append(c)
823
self.cooked_conflicts.sort(key=Conflict.sort_key)
826
class WeaveMerger(Merge3Merger):
827
"""Three-way tree merger, text weave merger."""
828
supports_reprocess = True
829
supports_show_base = False
831
def __init__(self, working_tree, this_tree, base_tree, other_tree,
832
interesting_ids=None, pb=DummyProgress(), pp=None,
834
self.this_revision_tree = self._get_revision_tree(this_tree)
835
self.other_revision_tree = self._get_revision_tree(other_tree)
836
super(WeaveMerger, self).__init__(working_tree, this_tree,
837
base_tree, other_tree,
838
interesting_ids=interesting_ids,
839
pb=pb, pp=pp, reprocess=reprocess)
841
def _get_revision_tree(self, tree):
842
"""Return a revision tree related to this tree.
843
If the tree is a WorkingTree, the basis will be returned.
845
if getattr(tree, 'get_weave', False) is False:
846
# If we have a WorkingTree, try using the basis
847
return tree.branch.basis_tree()
851
def _check_file(self, file_id):
852
"""Check that the revision tree's version of the file matches."""
853
for tree, rt in ((self.this_tree, self.this_revision_tree),
854
(self.other_tree, self.other_revision_tree)):
857
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
858
raise WorkingTreeNotRevision(self.this_tree)
860
def _merged_lines(self, file_id):
861
"""Generate the merged lines.
862
There is no distinction between lines that are meant to contain <<<<<<<
865
weave = self.this_revision_tree.get_weave(file_id)
866
this_revision_id = self.this_revision_tree.inventory[file_id].revision
867
other_revision_id = \
868
self.other_revision_tree.inventory[file_id].revision
869
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
870
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
871
return wm.merge_lines(self.reprocess)
873
def text_merge(self, file_id, trans_id):
874
"""Perform a (weave) text merge for a given file and file-id.
875
If conflicts are encountered, .THIS and .OTHER files will be emitted,
876
and a conflict will be noted.
878
self._check_file(file_id)
879
lines, conflicts = self._merged_lines(file_id)
881
# Note we're checking whether the OUTPUT is binary in this case,
882
# because we don't want to get into weave merge guts.
883
check_text_lines(lines)
884
self.tt.create_file(lines, trans_id)
886
self._raw_conflicts.append(('text conflict', trans_id))
887
name = self.tt.final_name(trans_id)
888
parent_id = self.tt.final_parent(trans_id)
889
file_group = self._dump_conflicts(name, parent_id, file_id,
891
file_group.append(trans_id)
894
class Diff3Merger(Merge3Merger):
895
"""Three-way merger using external diff3 for text merging"""
897
def dump_file(self, temp_dir, name, tree, file_id):
898
out_path = pathjoin(temp_dir, name)
899
out_file = open(out_path, "wb")
901
in_file = tree.get_file(file_id)
908
def text_merge(self, file_id, trans_id):
909
"""Perform a diff3 merge using a specified file-id and trans-id.
910
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
911
will be dumped, and a will be conflict noted.
914
temp_dir = osutils.mkdtemp(prefix="bzr-")
916
new_file = pathjoin(temp_dir, "new")
917
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
918
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
919
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
920
status = bzrlib.patch.diff3(new_file, this, base, other)
921
if status not in (0, 1):
922
raise BzrError("Unhandled diff3 exit code")
923
f = open(new_file, 'rb')
925
self.tt.create_file(f, trans_id)
929
name = self.tt.final_name(trans_id)
930
parent_id = self.tt.final_parent(trans_id)
931
self._dump_conflicts(name, parent_id, file_id)
932
self._raw_conflicts.append(('text conflict', trans_id))
934
osutils.rmtree(temp_dir)
937
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
939
merge_type=Merge3Merger,
940
interesting_ids=None,
944
interesting_files=None,
947
"""Primary interface for merging.
949
typical use is probably
950
'merge_inner(branch, branch.get_revision_tree(other_revision),
951
branch.get_revision_tree(base_revision))'
953
if this_tree is None:
954
warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
955
"bzrlib version 0.8.",
958
this_tree = this_branch.bzrdir.open_workingtree()
959
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
961
merger.backup_files = backup_files
962
merger.merge_type = merge_type
963
merger.interesting_ids = interesting_ids
964
merger.ignore_zero = ignore_zero
965
if interesting_files:
966
assert not interesting_ids, ('Only supply interesting_ids'
967
' or interesting_files')
968
merger._set_interesting_files(interesting_files)
969
merger.show_base = show_base
970
merger.reprocess = reprocess
971
merger.other_rev_id = other_rev_id
972
merger.other_basis = other_rev_id
973
return merger.do_merge()
976
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
977
"diff3": (Diff3Merger, "Merge using external diff3"),
978
'weave': (WeaveMerger, "Weave-based merge")
982
def merge_type_help():
983
templ = '%s%%7s: %%s' % (' '*12)
984
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
985
return '\n'.join(lines)