1
# Copyright (C) 2005 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
21
from shutil import rmtree
24
from bzrlib.branch import Branch
25
from bzrlib.delta import compare_trees
26
from bzrlib.errors import (BzrCommandError,
35
WorkingTreeNotRevision,
37
from bzrlib.fetch import greedy_fetch, fetch
39
from bzrlib.merge3 import Merge3
40
from bzrlib.osutils import rename, pathjoin
41
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
42
from bzrlib.transform import TreeTransform, resolve_conflicts, FinalPaths, create_by_entry, unique_add
43
from bzrlib.trace import mutter, warning, note
45
# TODO: Report back as changes are merged in
47
# comments from abentley on irc: merge happens in two stages, each
48
# of which generates a changeset object
50
# stage 1: generate OLD->OTHER,
51
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
53
def _get_tree(treespec, local_branch=None):
54
location, revno = treespec
55
branch = Branch.open_containing(location)[0]
59
revision = branch.last_revision()
61
revision = branch.get_rev_id(revno)
63
revision = NULL_REVISION
64
return branch, _get_revid_tree(branch, revision, local_branch)
67
def _get_revid_tree(branch, revision, local_branch):
69
base_tree = branch.bzrdir.open_workingtree()
71
if local_branch is not None:
72
if local_branch.base != branch.base:
73
greedy_fetch(local_branch, branch, revision)
74
base_tree = local_branch.repository.revision_tree(revision)
76
base_tree = branch.repository.revision_tree(revision)
80
def build_working_dir(to_dir):
81
"""Build a working directory in an empty directory.
83
to_dir is a directory containing branch metadata but no working files,
84
typically constructed by cloning an existing branch.
86
This is split out as a special idiomatic case of merge. It could
87
eventually be done by just building the tree directly calling into
88
lower-level code (e.g. constructing a changeset).
90
# RBC 20051019 is this not just 'export' ?
91
# AB Well, export doesn't take care of inventory...
92
from transform import build_tree
93
this_branch = Branch.open_containing(to_dir)[0]
94
build_tree(this_branch, this_branch.basis_tree())
97
def transform_tree(from_tree, to_tree, interesting_ids=None):
98
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
99
interesting_ids=interesting_ids)
102
class Merger(object):
103
def __init__(self, this_branch, other_tree=None, base_tree=None, this_tree=None):
104
object.__init__(self)
105
assert this_tree is not None, "this_tree is required"
106
self.this_branch = this_branch
107
self.this_basis = this_branch.last_revision()
108
self.this_rev_id = None
109
self.this_tree = this_tree
110
self.this_revision_tree = None
111
self.this_basis_tree = None
112
self.other_tree = other_tree
113
self.base_tree = base_tree
114
self.ignore_zero = False
115
self.backup_files = False
116
self.interesting_ids = None
117
self.show_base = False
118
self.reprocess = False
120
def revision_tree(self, revision_id):
121
return self.this_branch.repository.revision_tree(revision_id)
123
def ensure_revision_trees(self):
124
if self.this_revision_tree is None:
125
self.this_basis_tree = self.this_branch.repository.revision_tree(
127
if self.this_basis == self.this_rev_id:
128
self.this_revision_tree = self.this_basis_tree
130
if self.other_rev_id is None:
131
other_basis_tree = self.revision_tree(self.other_basis)
132
changes = compare_trees(self.other_tree, other_basis_tree)
133
if changes.has_changed():
134
raise WorkingTreeNotRevision(self.this_tree)
135
other_rev_id = other_basis
136
self.other_tree = other_basis_tree
138
def file_revisions(self, file_id):
139
self.ensure_revision_trees()
140
def get_id(tree, file_id):
141
revision_id = tree.inventory[file_id].revision
142
assert revision_id is not None
144
if self.this_rev_id is None:
145
if self.this_basis_tree.get_file_sha1(file_id) != \
146
self.this_tree.get_file_sha1(file_id):
147
raise WorkingTreeNotRevision(self.this_tree)
149
trees = (self.this_basis_tree, self.other_tree)
150
return [get_id(tree, file_id) for tree in trees]
152
def check_basis(self, check_clean):
153
if self.this_basis is None:
154
raise BzrCommandError("This branch has no commits")
157
if self.this_basis != self.this_rev_id:
158
raise BzrCommandError("Working tree has uncommitted changes.")
160
def compare_basis(self):
161
changes = compare_trees(self.this_tree,
162
self.this_tree.basis_tree(), False)
163
if not changes.has_changed():
164
self.this_rev_id = self.this_basis
166
def set_interesting_files(self, file_list):
168
self._set_interesting_files(file_list)
169
except NotVersionedError, e:
170
raise BzrCommandError("%s is not a source file in any"
173
def _set_interesting_files(self, file_list):
174
"""Set the list of interesting ids from a list of files."""
175
if file_list is None:
176
self.interesting_ids = None
179
interesting_ids = set()
180
for path in file_list:
182
for tree in (self.this_tree, self.base_tree, self.other_tree):
183
file_id = tree.inventory.path2id(path)
184
if file_id is not None:
185
interesting_ids.add(file_id)
188
raise NotVersionedError(path=path)
189
self.interesting_ids = interesting_ids
191
def set_pending(self):
192
if not self.base_is_ancestor:
194
if self.other_rev_id is None:
196
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
197
if self.other_rev_id in ancestry:
199
self.this_tree.add_pending_merge(self.other_rev_id)
201
def set_other(self, other_revision):
202
other_branch, self.other_tree = _get_tree(other_revision,
204
if other_revision[1] == -1:
205
self.other_rev_id = other_branch.last_revision()
206
if self.other_rev_id is None:
207
raise NoCommits(other_branch)
208
self.other_basis = self.other_rev_id
209
elif other_revision[1] is not None:
210
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
211
self.other_basis = self.other_rev_id
213
self.other_rev_id = None
214
self.other_basis = other_branch.last_revision()
215
if self.other_basis is None:
216
raise NoCommits(other_branch)
217
if other_branch.base != self.this_branch.base:
218
fetch(from_branch=other_branch, to_branch=self.this_branch,
219
last_revision=self.other_basis)
221
def set_base(self, base_revision):
222
mutter("doing merge() with no base_revision specified")
223
if base_revision == [None, None]:
225
self.base_rev_id = common_ancestor(self.this_basis,
227
self.this_branch.repository)
228
except NoCommonAncestor:
229
raise UnrelatedBranches()
230
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
232
self.base_is_ancestor = True
234
base_branch, self.base_tree = _get_tree(base_revision)
235
if base_revision[1] == -1:
236
self.base_rev_id = base_branch.last_revision()
237
elif base_revision[1] is None:
238
self.base_rev_id = None
240
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
241
fetch(from_branch=base_branch, to_branch=self.this_branch)
242
self.base_is_ancestor = is_ancestor(self.this_basis,
247
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
248
'other_tree': self.other_tree}
249
if self.merge_type.requires_base:
250
kwargs['base_tree'] = self.base_tree
251
if self.merge_type.supports_reprocess:
252
kwargs['reprocess'] = self.reprocess
254
raise BzrError("Reprocess is not supported for this merge"
255
" type. %s" % merge_type)
256
if self.merge_type.supports_show_base:
257
kwargs['show_base'] = self.show_base
259
raise BzrError("Showing base is not supported for this"
260
" merge type. %s" % self.merge_type)
261
merge = self.merge_type(**kwargs)
262
if len(merge.cooked_conflicts) == 0:
263
if not self.ignore_zero:
264
note("All changes applied successfully.")
266
note("%d conflicts encountered." % len(merge.cooked_conflicts))
268
return len(merge.cooked_conflicts)
270
def regen_inventory(self, new_entries):
271
old_entries = self.this_tree.read_working_inventory()
275
for path, file_id in new_entries:
278
new_entries_map[file_id] = path
280
def id2path(file_id):
281
path = new_entries_map.get(file_id)
284
entry = old_entries[file_id]
285
if entry.parent_id is None:
287
return pathjoin(id2path(entry.parent_id), entry.name)
289
for file_id in old_entries:
290
entry = old_entries[file_id]
291
path = id2path(file_id)
292
new_inventory[file_id] = (path, file_id, entry.parent_id,
294
by_path[path] = file_id
299
for path, file_id in new_entries:
301
del new_inventory[file_id]
304
new_path_list.append((path, file_id))
305
if file_id not in old_entries:
307
# Ensure no file is added before its parent
309
for path, file_id in new_path_list:
313
parent = by_path[os.path.dirname(path)]
314
abspath = pathjoin(self.this_tree.basedir, path)
315
kind = bzrlib.osutils.file_kind(abspath)
316
new_inventory[file_id] = (path, file_id, parent, kind)
317
by_path[path] = file_id
319
# Get a list in insertion order
320
new_inventory_list = new_inventory.values()
321
mutter ("""Inventory regeneration:
322
old length: %i insertions: %i deletions: %i new_length: %i"""\
323
% (len(old_entries), insertions, deletions,
324
len(new_inventory_list)))
325
assert len(new_inventory_list) == len(old_entries) + insertions\
327
new_inventory_list.sort()
328
return new_inventory_list
331
class Merge3Merger(object):
333
supports_reprocess = True
334
supports_show_base = True
335
history_based = False
336
def __init__(self, working_tree, this_tree, base_tree, other_tree,
337
reprocess=False, show_base=False):
338
"""Initialize the merger object and perform the merge."""
339
object.__init__(self)
340
self.this_tree = working_tree
341
self.base_tree = base_tree
342
self.other_tree = other_tree
343
self._raw_conflicts = []
344
self.cooked_conflicts = []
345
self.reprocess = reprocess
346
self.show_base = show_base
348
all_ids = set(base_tree)
349
all_ids.update(other_tree)
350
self.tt = TreeTransform(working_tree)
352
for file_id in all_ids:
353
self.merge_names(file_id)
354
file_status = self.merge_contents(file_id)
355
self.merge_executable(file_id, file_status)
357
resolve_conflicts(self.tt)
358
self.cook_conflicts()
359
for line in conflicts_strings(self.cooked_conflicts):
369
def parent(entry, file_id):
370
"""Determine the parent for a file_id (used as a key method)"""
373
return entry.parent_id
376
def name(entry, file_id):
377
"""Determine the name for a file_id (used as a key method)"""
383
def contents_sha1(tree, file_id):
384
"""Determine the sha1 of the file contents (used as a key method)."""
385
if file_id not in tree:
387
return tree.get_file_sha1(file_id)
390
def executable(tree, file_id):
391
"""Determine the executability of a file-id (used as a key method)."""
392
if file_id not in tree:
394
if tree.kind(file_id) != "file":
396
return tree.is_executable(file_id)
399
def kind(tree, file_id):
400
"""Determine the kind of a file-id (used as a key method)."""
401
if file_id not in tree:
403
return tree.kind(file_id)
406
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
407
"""Do a three-way test on a scalar.
408
Return "this", "other" or "conflict", depending whether a value wins.
410
key_base = key(base_tree, file_id)
411
key_other = key(other_tree, file_id)
412
#if base == other, either they all agree, or only THIS has changed.
413
if key_base == key_other:
415
key_this = key(this_tree, file_id)
416
if key_this not in (key_base, key_other):
418
# "Ambiguous clean merge"
419
elif key_this == key_other:
422
assert key_this == key_base
425
def merge_names(self, file_id):
426
"""Perform a merge on file_id names and parents"""
428
if file_id in tree.inventory:
429
return tree.inventory[file_id]
432
this_entry = get_entry(self.this_tree)
433
other_entry = get_entry(self.other_tree)
434
base_entry = get_entry(self.base_tree)
435
name_winner = self.scalar_three_way(this_entry, base_entry,
436
other_entry, file_id, self.name)
437
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
438
other_entry, file_id,
440
if this_entry is None:
441
if name_winner == "this":
442
name_winner = "other"
443
if parent_id_winner == "this":
444
parent_id_winner = "other"
445
if name_winner == "this" and parent_id_winner == "this":
447
if name_winner == "conflict":
448
trans_id = self.tt.get_trans_id(file_id)
449
self._raw_conflicts.append(('name conflict', trans_id,
450
self.name(this_entry, file_id),
451
self.name(other_entry, file_id)))
452
if parent_id_winner == "conflict":
453
trans_id = self.tt.get_trans_id(file_id)
454
self._raw_conflicts.append(('parent conflict', trans_id,
455
self.parent(this_entry, file_id),
456
self.parent(other_entry, file_id)))
457
if other_entry is None:
458
# it doesn't matter whether the result was 'other' or
459
# 'conflict'-- if there's no 'other', we leave it alone.
461
# if we get here, name_winner and parent_winner are set to safe values.
462
winner_entry = {"this": this_entry, "other": other_entry,
463
"conflict": other_entry}
464
trans_id = self.tt.get_trans_id(file_id)
465
parent_id = winner_entry[parent_id_winner].parent_id
466
parent_trans_id = self.tt.get_trans_id(parent_id)
467
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
471
def merge_contents(self, file_id):
472
"""Performa a merge on file_id contents."""
473
def contents_pair(tree):
474
if file_id not in tree:
476
kind = tree.kind(file_id)
477
if kind == "root_directory":
480
contents = tree.get_file_sha1(file_id)
481
elif kind == "symlink":
482
contents = tree.get_symlink_target(file_id)
485
return kind, contents
486
# See SPOT run. run, SPOT, run.
487
# So we're not QUITE repeating ourselves; we do tricky things with
489
base_pair = contents_pair(self.base_tree)
490
other_pair = contents_pair(self.other_tree)
491
if base_pair == other_pair:
492
# OTHER introduced no changes
494
this_pair = contents_pair(self.this_tree)
495
if this_pair == other_pair:
496
# THIS and OTHER introduced the same changes
499
trans_id = self.tt.get_trans_id(file_id)
500
if this_pair == base_pair:
501
# only OTHER introduced changes
502
if file_id in self.this_tree:
503
# Remove any existing contents
504
self.tt.delete_contents(trans_id)
505
if file_id in self.other_tree:
506
# OTHER changed the file
507
create_by_entry(self.tt,
508
self.other_tree.inventory[file_id],
509
self.other_tree, trans_id)
510
if file_id not in self.this_tree.inventory:
511
self.tt.version_file(file_id, trans_id)
513
elif file_id in self.this_tree.inventory:
514
# OTHER deleted the file
515
self.tt.unversion_file(trans_id)
517
#BOTH THIS and OTHER introduced changes; scalar conflict
518
elif this_pair[0] == "file" and other_pair[0] == "file":
519
# THIS and OTHER are both files, so text merge. Either
520
# BASE is a file, or both converted to files, so at least we
521
# have agreement that output should be a file.
522
if file_id not in self.this_tree.inventory:
523
self.tt.version_file(file_id, trans_id)
524
self.text_merge(file_id, trans_id)
526
self.tt.tree_kind(trans_id)
527
self.tt.delete_contents(trans_id)
532
# Scalar conflict, can't text merge. Dump conflicts
533
trans_id = self.tt.get_trans_id(file_id)
534
name = self.tt.final_name(trans_id)
535
parent_id = self.tt.final_parent(trans_id)
536
if file_id in self.this_tree.inventory:
537
self.tt.unversion_file(trans_id)
538
self.tt.delete_contents(trans_id)
539
file_group = self._dump_conflicts(name, parent_id, file_id,
541
self._raw_conflicts.append(('contents conflict', file_group))
543
def get_lines(self, tree, file_id):
544
"""Return the lines in a file, or an empty list."""
546
return tree.get_file(file_id).readlines()
550
def text_merge(self, file_id, trans_id):
551
"""Perform a three-way text merge on a file_id"""
552
# it's possible that we got here with base as a different type.
553
# if so, we just want two-way text conflicts.
554
if file_id in self.base_tree and \
555
self.base_tree.kind(file_id) == "file":
556
base_lines = self.get_lines(self.base_tree, file_id)
559
other_lines = self.get_lines(self.other_tree, file_id)
560
this_lines = self.get_lines(self.this_tree, file_id)
561
m3 = Merge3(base_lines, this_lines, other_lines)
562
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
563
if self.show_base is True:
564
base_marker = '|' * 7
568
def iter_merge3(retval):
569
retval["text_conflicts"] = False
570
for line in m3.merge_lines(name_a = "TREE",
571
name_b = "MERGE-SOURCE",
572
name_base = "BASE-REVISION",
573
start_marker=start_marker,
574
base_marker=base_marker,
575
reprocess=self.reprocess):
576
if line.startswith(start_marker):
577
retval["text_conflicts"] = True
578
yield line.replace(start_marker, '<' * 7)
582
merge3_iterator = iter_merge3(retval)
583
self.tt.create_file(merge3_iterator, trans_id)
584
if retval["text_conflicts"] is True:
585
self._raw_conflicts.append(('text conflict', trans_id))
586
name = self.tt.final_name(trans_id)
587
parent_id = self.tt.final_parent(trans_id)
588
file_group = self._dump_conflicts(name, parent_id, file_id,
589
this_lines, base_lines,
591
file_group.append(trans_id)
593
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
594
base_lines=None, other_lines=None, set_version=False,
596
"""Emit conflict files.
597
If this_lines, base_lines, or other_lines are omitted, they will be
598
determined automatically. If set_version is true, the .OTHER, .THIS
599
or .BASE (in that order) will be created as versioned files.
601
data = [('OTHER', self.other_tree, other_lines),
602
('THIS', self.this_tree, this_lines)]
604
data.append(('BASE', self.base_tree, base_lines))
607
for suffix, tree, lines in data:
609
trans_id = self._conflict_file(name, parent_id, tree, file_id,
611
file_group.append(trans_id)
612
if set_version and not versioned:
613
self.tt.version_file(file_id, trans_id)
617
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
619
"""Emit a single conflict file."""
620
name = name + '.' + suffix
621
trans_id = self.tt.create_path(name, parent_id)
622
entry = tree.inventory[file_id]
623
create_by_entry(self.tt, entry, tree, trans_id, lines)
626
def merge_executable(self, file_id, file_status):
627
"""Perform a merge on the execute bit."""
628
if file_status == "deleted":
630
trans_id = self.tt.get_trans_id(file_id)
632
if self.tt.final_kind(trans_id) != "file":
636
winner = self.scalar_three_way(self.this_tree, self.base_tree,
637
self.other_tree, file_id,
639
if winner == "conflict":
640
# There must be a None in here, if we have a conflict, but we
641
# need executability since file status was not deleted.
642
if self.other_tree.is_executable(file_id) is None:
647
if file_status == "modified":
648
executability = self.this_tree.is_executable(file_id)
649
if executability is not None:
650
trans_id = self.tt.get_trans_id(file_id)
651
self.tt.set_executability(executability, trans_id)
653
assert winner == "other"
654
if file_id in self.other_tree:
655
executability = self.other_tree.is_executable(file_id)
656
elif file_id in self.this_tree:
657
executability = self.this_tree.is_executable(file_id)
658
elif file_id in self.base_tree:
659
executability = self.base_tree.is_executable(file_id)
660
if executability is not None:
661
trans_id = self.tt.get_trans_id(file_id)
662
self.tt.set_executability(executability, trans_id)
664
def cook_conflicts(self):
665
"""Convert all conflicts into a form that doesn't depend on trans_id"""
667
fp = FinalPaths(self.tt)
668
for conflict in self._raw_conflicts:
669
conflict_type = conflict[0]
670
if conflict_type in ('name conflict', 'parent conflict'):
671
trans_id = conflict[1]
672
conflict_args = conflict[2:]
673
if trans_id not in name_conflicts:
674
name_conflicts[trans_id] = {}
675
unique_add(name_conflicts[trans_id], conflict_type,
677
if conflict_type == 'contents conflict':
678
for trans_id in conflict[1]:
679
file_id = self.tt.final_file_id(trans_id)
680
if file_id is not None:
682
path = fp.get_path(trans_id)
683
for suffix in ('.BASE', '.THIS', '.OTHER'):
684
if path.endswith(suffix):
685
path = path[:-len(suffix)]
687
self.cooked_conflicts.append((conflict_type, file_id, path))
688
if conflict_type == 'text conflict':
689
trans_id = conflict[1]
690
path = fp.get_path(trans_id)
691
file_id = self.tt.final_file_id(trans_id)
692
self.cooked_conflicts.append((conflict_type, file_id, path))
694
for trans_id, conflicts in name_conflicts.iteritems():
696
this_parent, other_parent = conflicts['parent conflict']
697
assert this_parent != other_parent
699
this_parent = other_parent = \
700
self.tt.final_file_id(self.tt.final_parent(trans_id))
702
this_name, other_name = conflicts['name conflict']
703
assert this_name != other_name
705
this_name = other_name = self.tt.final_name(trans_id)
706
other_path = fp.get_path(trans_id)
707
if this_parent is not None:
709
fp.get_path(self.tt.get_trans_id(this_parent))
710
this_path = os.path.join(this_parent_path, this_name)
712
this_path = "<deleted>"
713
file_id = self.tt.final_file_id(trans_id)
714
self.cooked_conflicts.append(('path conflict', file_id, this_path,
718
def conflicts_strings(conflicts):
719
"""Generate strings for the provided conflicts"""
720
for conflict in conflicts:
721
conflict_type = conflict[0]
722
if conflict_type == 'text conflict':
723
yield 'Text conflict in %s' % conflict[2]
724
elif conflict_type == 'contents conflict':
725
yield 'Contents conflict in %s' % conflict[2]
726
elif conflict_type == 'path conflict':
727
yield 'Path conflict: %s / %s' % conflict[2:]
730
class WeaveMerger(Merge3Merger):
731
"""Merger that does weave merges."""
732
supports_reprocess = False
733
supports_show_base = False
735
def __init__(self, working_tree, this_tree, base_tree, other_tree):
736
self.this_revision_tree = self._get_revision_tree(this_tree)
737
self.other_revision_tree = self._get_revision_tree(other_tree)
738
super(WeaveMerger, self).__init__(working_tree, this_tree,
739
base_tree, other_tree)
741
def _get_revision_tree(self, tree):
742
"""Return a revision tree releated to this tree.
743
If the tree is a WorkingTree, the basis will be returned.
745
if getattr(tree, 'get_weave', False) is False:
746
# If we have a WorkingTree, try using the basis
747
return tree.branch.basis_tree()
751
def _check_file(self, file_id):
752
"""Check that the revision tree's version of the file matches."""
753
for tree, rt in ((self.this_tree, self.this_revision_tree),
754
(self.other_tree, self.other_revision_tree)):
757
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
758
raise WorkingTreeNotRevision(self.this_tree)
760
def _merged_lines(self, file_id):
761
"""Generate the merged lines.
762
There is no distinction between lines that are meant to contain <<<<<<<
765
weave = self.this_revision_tree.get_weave(file_id)
766
this_revision_id = self.this_revision_tree.inventory[file_id].revision
767
other_revision_id = \
768
self.other_revision_tree.inventory[file_id].revision
769
this_i = weave.lookup(this_revision_id)
770
other_i = weave.lookup(other_revision_id)
771
plan = weave.plan_merge(this_i, other_i)
772
return weave.weave_merge(plan)
774
def text_merge(self, file_id, trans_id):
775
"""Perform a (weave) text merge for a given file and file-id.
776
If conflicts are encountered, .THIS and .OTHER files will be emitted,
777
and a conflict will be noted.
779
self._check_file(file_id)
780
lines = self._merged_lines(file_id)
781
conflicts = '<<<<<<<\n' in lines
782
self.tt.create_file(lines, trans_id)
784
self._raw_conflicts.append(('text conflict', trans_id))
785
name = self.tt.final_name(trans_id)
786
parent_id = self.tt.final_parent(trans_id)
787
file_group = self._dump_conflicts(name, parent_id, file_id,
789
file_group.append(trans_id)
792
class Diff3Merger(Merge3Merger):
793
"""Use good ol' diff3 to do text merges"""
794
def dump_file(self, temp_dir, name, tree, file_id):
795
out_path = pathjoin(temp_dir, name)
796
out_file = file(out_path, "wb")
797
in_file = tree.get_file(file_id)
802
def text_merge(self, file_id, trans_id):
803
"""Perform a diff3 merge using a specified file-id and trans-id.
804
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
805
will be dumped, and a will be conflict noted.
808
temp_dir = mkdtemp(prefix="bzr-")
810
new_file = os.path.join(temp_dir, "new")
811
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
812
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
813
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
814
status = bzrlib.patch.diff3(new_file, this, base, other)
815
if status not in (0, 1):
816
raise BzrError("Unhandled diff3 exit code")
817
self.tt.create_file(file(new_file, "rb"), trans_id)
819
name = self.tt.final_name(trans_id)
820
parent_id = self.tt.final_parent(trans_id)
821
self._dump_conflicts(name, parent_id, file_id)
822
self._raw_conflicts.append(('text conflict', trans_id))
827
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
829
merge_type=Merge3Merger,
830
interesting_ids=None,
834
interesting_files=None,
836
"""Primary interface for merging.
838
typical use is probably
839
'merge_inner(branch, branch.get_revision_tree(other_revision),
840
branch.get_revision_tree(base_revision))'
842
if this_tree is None:
843
this_tree = this_branch.working_tree()
844
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree)
845
merger.backup_files = backup_files
846
merger.merge_type = merge_type
847
merger.interesting_ids = interesting_ids
848
if interesting_files:
849
assert not interesting_ids, ('Only supply interesting_ids'
850
' or interesting_files')
851
merger._set_interesting_files(interesting_files)
852
merger.show_base = show_base
853
merger.reprocess = reprocess
854
merger.other_rev_id = other_rev_id
855
merger.other_basis = other_rev_id
856
return merger.do_merge()
859
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
860
"diff3": (Diff3Merger, "Merge using external diff3"),
861
'weave': (WeaveMerger, "Weave-based merge")