1
# Copyright (C) 2005, 2006 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1
from merge_core import merge_flex
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory
4
from bzrlib import Branch
6
from trace import mutter
20
from tempfile import mkdtemp
23
from bzrlib.branch import Branch
24
from bzrlib.conflicts import ConflictList, Conflict
25
from bzrlib.errors import (BzrCommandError,
35
WorkingTreeNotRevision,
38
from bzrlib.merge3 import Merge3
40
from bzrlib.osutils import rename, pathjoin, rmtree
41
from progress import DummyProgress, ProgressPhase
42
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
43
from bzrlib.textfile import check_text_lines
44
from bzrlib.trace import mutter, warning, note
45
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
46
FinalPaths, create_by_entry, unique_add)
47
from bzrlib.versionedfile import WeaveMerge
50
# TODO: Report back as changes are merged in
52
def _get_tree(treespec, local_branch=None):
53
from bzrlib import workingtree
54
location, revno = treespec
12
class MergeConflictHandler(ExceptionConflictHandler):
13
"""Handle conflicts encountered while merging"""
14
def copy(self, source, dest):
15
"""Copy the text and mode of a file
16
:param source: The path of the file to copy
17
:param dest: The distination file to create
19
s_file = file(source, "rb")
20
d_file = file(dest, "wb")
23
os.chmod(dest, 0777 & os.stat(source).st_mode)
25
def add_suffix(self, name, suffix, last_new_name=None):
26
"""Rename a file to append a suffix. If the new name exists, the
27
suffix is added repeatedly until a non-existant name is found
29
:param name: The path of the file
30
:param suffix: The suffix to append
31
:param last_new_name: (used for recursive calls) the last name tried
33
if last_new_name is None:
35
new_name = last_new_name+suffix
37
os.rename(name, new_name)
39
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
41
self.add_suffix(name, suffix, last_new_name=new_name)
43
def merge_conflict(self, new_file, this_path, base_path, other_path):
45
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
46
main file will be a version with diff3 conflicts.
47
:param new_file: Path to the output file with diff3 markers
48
:param this_path: Path to the file text for the THIS tree
49
:param base_path: Path to the file text for the BASE tree
50
:param other_path: Path to the file text for the OTHER tree
52
self.add_suffix(this_path, ".THIS")
53
self.copy(base_path, this_path+".BASE")
54
self.copy(other_path, this_path+".OTHER")
55
os.rename(new_file, this_path)
57
def target_exists(self, entry, target, old_path):
58
"""Handle the case when the target file or dir exists"""
59
self.add_suffix(target, ".moved")
62
def __init__(self, path, id, present=None, isdir=None):
65
self.present = present
67
self.interesting = True
70
return "SourceFile(%s, %s)" % (self.path, self.id)
72
def get_tree(treespec, temp_root, label):
56
tree = workingtree.WorkingTree.open_containing(location)[0]
57
return tree.branch, tree
58
branch = Branch.open_containing(location)[0]
60
revision = branch.last_revision()
62
revision = branch.get_rev_id(revno)
64
revision = NULL_REVISION
65
return branch, _get_revid_tree(branch, revision, local_branch)
68
def _get_revid_tree(branch, revision, local_branch):
70
base_tree = branch.bzrdir.open_workingtree()
72
if local_branch is not None:
73
if local_branch.base != branch.base:
74
local_branch.fetch(branch, revision)
75
base_tree = local_branch.repository.revision_tree(revision)
77
base_tree = branch.repository.revision_tree(revision)
81
def transform_tree(from_tree, to_tree, interesting_ids=None):
82
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
83
interesting_ids=interesting_ids, this_tree=from_tree)
87
def __init__(self, this_branch, other_tree=None, base_tree=None,
88
this_tree=None, pb=DummyProgress()):
90
assert this_tree is not None, "this_tree is required"
91
self.this_branch = this_branch
92
self.this_basis = this_branch.last_revision()
93
self.this_rev_id = None
94
self.this_tree = this_tree
95
self.this_revision_tree = None
96
self.this_basis_tree = None
97
self.other_tree = other_tree
98
self.base_tree = base_tree
99
self.ignore_zero = False
100
self.backup_files = False
101
self.interesting_ids = None
102
self.show_base = False
103
self.reprocess = False
108
def revision_tree(self, revision_id):
109
return self.this_branch.repository.revision_tree(revision_id)
111
def ensure_revision_trees(self):
112
if self.this_revision_tree is None:
113
self.this_basis_tree = self.this_branch.repository.revision_tree(
115
if self.this_basis == self.this_rev_id:
116
self.this_revision_tree = self.this_basis_tree
118
if self.other_rev_id is None:
119
other_basis_tree = self.revision_tree(self.other_basis)
120
changes = other_basis_tree.changes_from(self.other_tree)
121
if changes.has_changed():
122
raise WorkingTreeNotRevision(self.this_tree)
123
other_rev_id = self.other_basis
124
self.other_tree = other_basis_tree
126
def file_revisions(self, file_id):
127
self.ensure_revision_trees()
128
def get_id(tree, file_id):
129
revision_id = tree.inventory[file_id].revision
130
assert revision_id is not None
132
if self.this_rev_id is None:
133
if self.this_basis_tree.get_file_sha1(file_id) != \
134
self.this_tree.get_file_sha1(file_id):
135
raise WorkingTreeNotRevision(self.this_tree)
137
trees = (self.this_basis_tree, self.other_tree)
138
return [get_id(tree, file_id) for tree in trees]
140
def check_basis(self, check_clean, require_commits=True):
141
if self.this_basis is None and require_commits is True:
142
raise BzrCommandError("This branch has no commits")
145
if self.this_basis != self.this_rev_id:
146
raise BzrCommandError("Working tree has uncommitted changes.")
148
def compare_basis(self):
149
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
150
if not changes.has_changed():
151
self.this_rev_id = self.this_basis
153
def set_interesting_files(self, file_list):
155
self._set_interesting_files(file_list)
156
except NotVersionedError, e:
157
raise BzrCommandError("%s is not a source file in any"
160
def _set_interesting_files(self, file_list):
161
"""Set the list of interesting ids from a list of files."""
162
if file_list is None:
163
self.interesting_ids = None
166
interesting_ids = set()
167
for path in file_list:
169
for tree in (self.this_tree, self.base_tree, self.other_tree):
170
file_id = tree.inventory.path2id(path)
171
if file_id is not None:
172
interesting_ids.add(file_id)
175
raise NotVersionedError(path=path)
176
self.interesting_ids = interesting_ids
178
def set_pending(self):
179
if not self.base_is_ancestor:
181
if self.other_rev_id is None:
183
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
184
if self.other_rev_id in ancestry:
186
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
188
def set_other(self, other_revision):
189
"""Set the revision and tree to merge from.
191
This sets the other_tree, other_rev_id, other_basis attributes.
193
:param other_revision: The [path, revision] list to merge from.
195
other_branch, self.other_tree = _get_tree(other_revision,
197
if other_revision[1] == -1:
198
self.other_rev_id = other_branch.last_revision()
199
if self.other_rev_id is None:
200
raise NoCommits(other_branch)
201
self.other_basis = self.other_rev_id
202
elif other_revision[1] is not None:
203
self.other_rev_id = other_branch.get_rev_id(other_revision[1])
204
self.other_basis = self.other_rev_id
206
self.other_rev_id = None
207
self.other_basis = other_branch.last_revision()
208
if self.other_basis is None:
209
raise NoCommits(other_branch)
210
if other_branch.base != self.this_branch.base:
211
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
214
self.set_base([None, None])
216
def set_base(self, base_revision):
217
"""Set the base revision to use for the merge.
219
:param base_revision: A 2-list containing a path and revision number.
221
mutter("doing merge() with no base_revision specified")
222
if base_revision == [None, None]:
224
pb = bzrlib.ui.ui_factory.nested_progress_bar()
226
this_repo = self.this_branch.repository
227
self.base_rev_id = common_ancestor(self.this_basis,
232
except NoCommonAncestor:
233
raise UnrelatedBranches()
234
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
236
self.base_is_ancestor = True
238
base_branch, self.base_tree = _get_tree(base_revision)
239
if base_revision[1] == -1:
240
self.base_rev_id = base_branch.last_revision()
241
elif base_revision[1] is None:
242
self.base_rev_id = None
244
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
245
if self.this_branch.base != base_branch.base:
246
self.this_branch.fetch(base_branch)
247
self.base_is_ancestor = is_ancestor(self.this_basis,
252
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
253
'other_tree': self.other_tree,
254
'interesting_ids': self.interesting_ids,
256
if self.merge_type.requires_base:
257
kwargs['base_tree'] = self.base_tree
258
if self.merge_type.supports_reprocess:
259
kwargs['reprocess'] = self.reprocess
261
raise BzrError("Conflict reduction is not supported for merge"
262
" type %s." % self.merge_type)
263
if self.merge_type.supports_show_base:
264
kwargs['show_base'] = self.show_base
266
raise BzrError("Showing base is not supported for this"
267
" merge type. %s" % self.merge_type)
268
merge = self.merge_type(pb=self._pb, **kwargs)
269
if len(merge.cooked_conflicts) == 0:
270
if not self.ignore_zero:
271
note("All changes applied successfully.")
273
note("%d conflicts encountered." % len(merge.cooked_conflicts))
275
return len(merge.cooked_conflicts)
277
def regen_inventory(self, new_entries):
278
old_entries = self.this_tree.read_working_inventory()
282
for path, file_id in new_entries:
285
new_entries_map[file_id] = path
287
def id2path(file_id):
288
path = new_entries_map.get(file_id)
291
entry = old_entries[file_id]
292
if entry.parent_id is None:
294
return pathjoin(id2path(entry.parent_id), entry.name)
296
for file_id in old_entries:
297
entry = old_entries[file_id]
298
path = id2path(file_id)
299
if file_id in self.base_tree.inventory:
300
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
302
executable = getattr(entry, 'executable', False)
303
new_inventory[file_id] = (path, file_id, entry.parent_id,
304
entry.kind, executable)
306
by_path[path] = file_id
311
for path, file_id in new_entries:
313
del new_inventory[file_id]
316
new_path_list.append((path, file_id))
317
if file_id not in old_entries:
319
# Ensure no file is added before its parent
321
for path, file_id in new_path_list:
325
parent = by_path[os.path.dirname(path)]
326
abspath = pathjoin(self.this_tree.basedir, path)
327
kind = bzrlib.osutils.file_kind(abspath)
328
if file_id in self.base_tree.inventory:
329
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
332
new_inventory[file_id] = (path, file_id, parent, kind, executable)
333
by_path[path] = file_id
335
# Get a list in insertion order
336
new_inventory_list = new_inventory.values()
337
mutter ("""Inventory regeneration:
338
old length: %i insertions: %i deletions: %i new_length: %i"""\
339
% (len(old_entries), insertions, deletions,
340
len(new_inventory_list)))
341
assert len(new_inventory_list) == len(old_entries) + insertions\
343
new_inventory_list.sort()
344
return new_inventory_list
347
class Merge3Merger(object):
348
"""Three-way merger that uses the merge3 text merger"""
350
supports_reprocess = True
351
supports_show_base = True
352
history_based = False
354
def __init__(self, working_tree, this_tree, base_tree, other_tree,
355
interesting_ids=None, reprocess=False, show_base=False,
356
pb=DummyProgress(), pp=None):
357
"""Initialize the merger object and perform the merge."""
358
object.__init__(self)
359
self.this_tree = working_tree
360
self.base_tree = base_tree
361
self.other_tree = other_tree
362
self._raw_conflicts = []
363
self.cooked_conflicts = []
364
self.reprocess = reprocess
365
self.show_base = show_base
369
self.pp = ProgressPhase("Merge phase", 3, self.pb)
371
if interesting_ids is not None:
372
all_ids = interesting_ids
374
all_ids = set(base_tree)
375
all_ids.update(other_tree)
376
working_tree.lock_write()
377
self.tt = TreeTransform(working_tree, self.pb)
380
child_pb = ui.ui_factory.nested_progress_bar()
382
for num, file_id in enumerate(all_ids):
383
child_pb.update('Preparing file merge', num, len(all_ids))
384
self.merge_names(file_id)
385
file_status = self.merge_contents(file_id)
386
self.merge_executable(file_id, file_status)
391
child_pb = ui.ui_factory.nested_progress_bar()
393
fs_conflicts = resolve_conflicts(self.tt, child_pb)
396
self.cook_conflicts(fs_conflicts)
397
for conflict in self.cooked_conflicts:
400
results = self.tt.apply()
401
self.write_modified(results)
403
working_tree.add_conflicts(self.cooked_conflicts)
404
except UnsupportedOperation:
408
working_tree.unlock()
411
def write_modified(self, results):
413
for path in results.modified_paths:
414
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
417
hash = self.this_tree.get_file_sha1(file_id)
420
modified_hashes[file_id] = hash
421
self.this_tree.set_merge_modified(modified_hashes)
424
def parent(entry, file_id):
425
"""Determine the parent for a file_id (used as a key method)"""
428
return entry.parent_id
431
def name(entry, file_id):
432
"""Determine the name for a file_id (used as a key method)"""
438
def contents_sha1(tree, file_id):
439
"""Determine the sha1 of the file contents (used as a key method)."""
440
if file_id not in tree:
442
return tree.get_file_sha1(file_id)
445
def executable(tree, file_id):
446
"""Determine the executability of a file-id (used as a key method)."""
447
if file_id not in tree:
449
if tree.kind(file_id) != "file":
451
return tree.is_executable(file_id)
454
def kind(tree, file_id):
455
"""Determine the kind of a file-id (used as a key method)."""
456
if file_id not in tree:
458
return tree.kind(file_id)
461
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
462
"""Do a three-way test on a scalar.
463
Return "this", "other" or "conflict", depending whether a value wins.
465
key_base = key(base_tree, file_id)
466
key_other = key(other_tree, file_id)
467
#if base == other, either they all agree, or only THIS has changed.
468
if key_base == key_other:
470
key_this = key(this_tree, file_id)
471
if key_this not in (key_base, key_other):
473
# "Ambiguous clean merge"
474
elif key_this == key_other:
477
assert key_this == key_base
480
def merge_names(self, file_id):
481
"""Perform a merge on file_id names and parents"""
483
if file_id in tree.inventory:
484
return tree.inventory[file_id]
487
this_entry = get_entry(self.this_tree)
488
other_entry = get_entry(self.other_tree)
489
base_entry = get_entry(self.base_tree)
490
name_winner = self.scalar_three_way(this_entry, base_entry,
491
other_entry, file_id, self.name)
492
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
493
other_entry, file_id,
495
if this_entry is None:
496
if name_winner == "this":
497
name_winner = "other"
498
if parent_id_winner == "this":
499
parent_id_winner = "other"
500
if name_winner == "this" and parent_id_winner == "this":
502
if name_winner == "conflict":
503
trans_id = self.tt.trans_id_file_id(file_id)
504
self._raw_conflicts.append(('name conflict', trans_id,
505
self.name(this_entry, file_id),
506
self.name(other_entry, file_id)))
507
if parent_id_winner == "conflict":
508
trans_id = self.tt.trans_id_file_id(file_id)
509
self._raw_conflicts.append(('parent conflict', trans_id,
510
self.parent(this_entry, file_id),
511
self.parent(other_entry, file_id)))
512
if other_entry is None:
513
# it doesn't matter whether the result was 'other' or
514
# 'conflict'-- if there's no 'other', we leave it alone.
516
# if we get here, name_winner and parent_winner are set to safe values.
517
winner_entry = {"this": this_entry, "other": other_entry,
518
"conflict": other_entry}
519
trans_id = self.tt.trans_id_file_id(file_id)
520
parent_id = winner_entry[parent_id_winner].parent_id
521
parent_trans_id = self.tt.trans_id_file_id(parent_id)
522
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
525
def merge_contents(self, file_id):
526
"""Performa a merge on file_id contents."""
527
def contents_pair(tree):
528
if file_id not in tree:
530
kind = tree.kind(file_id)
532
contents = tree.get_file_sha1(file_id)
533
elif kind == "symlink":
534
contents = tree.get_symlink_target(file_id)
537
return kind, contents
539
def contents_conflict():
540
trans_id = self.tt.trans_id_file_id(file_id)
541
name = self.tt.final_name(trans_id)
542
parent_id = self.tt.final_parent(trans_id)
543
if file_id in self.this_tree.inventory:
544
self.tt.unversion_file(trans_id)
545
self.tt.delete_contents(trans_id)
546
file_group = self._dump_conflicts(name, parent_id, file_id,
548
self._raw_conflicts.append(('contents conflict', file_group))
550
# See SPOT run. run, SPOT, run.
551
# So we're not QUITE repeating ourselves; we do tricky things with
553
base_pair = contents_pair(self.base_tree)
554
other_pair = contents_pair(self.other_tree)
555
if base_pair == other_pair:
556
# OTHER introduced no changes
558
this_pair = contents_pair(self.this_tree)
559
if this_pair == other_pair:
560
# THIS and OTHER introduced the same changes
563
trans_id = self.tt.trans_id_file_id(file_id)
564
if this_pair == base_pair:
565
# only OTHER introduced changes
566
if file_id in self.this_tree:
567
# Remove any existing contents
568
self.tt.delete_contents(trans_id)
569
if file_id in self.other_tree:
570
# OTHER changed the file
571
create_by_entry(self.tt,
572
self.other_tree.inventory[file_id],
573
self.other_tree, trans_id)
574
if file_id not in self.this_tree.inventory:
575
self.tt.version_file(file_id, trans_id)
577
elif file_id in self.this_tree.inventory:
578
# OTHER deleted the file
579
self.tt.unversion_file(trans_id)
581
#BOTH THIS and OTHER introduced changes; scalar conflict
582
elif this_pair[0] == "file" and other_pair[0] == "file":
583
# THIS and OTHER are both files, so text merge. Either
584
# BASE is a file, or both converted to files, so at least we
585
# have agreement that output should be a file.
587
self.text_merge(file_id, trans_id)
589
return contents_conflict()
590
if file_id not in self.this_tree.inventory:
591
self.tt.version_file(file_id, trans_id)
593
self.tt.tree_kind(trans_id)
594
self.tt.delete_contents(trans_id)
599
# Scalar conflict, can't text merge. Dump conflicts
600
return contents_conflict()
602
def get_lines(self, tree, file_id):
603
"""Return the lines in a file, or an empty list."""
605
return tree.get_file(file_id).readlines()
609
def text_merge(self, file_id, trans_id):
610
"""Perform a three-way text merge on a file_id"""
611
# it's possible that we got here with base as a different type.
612
# if so, we just want two-way text conflicts.
613
if file_id in self.base_tree and \
614
self.base_tree.kind(file_id) == "file":
615
base_lines = self.get_lines(self.base_tree, file_id)
618
other_lines = self.get_lines(self.other_tree, file_id)
619
this_lines = self.get_lines(self.this_tree, file_id)
620
m3 = Merge3(base_lines, this_lines, other_lines)
621
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
622
if self.show_base is True:
623
base_marker = '|' * 7
627
def iter_merge3(retval):
628
retval["text_conflicts"] = False
629
for line in m3.merge_lines(name_a = "TREE",
630
name_b = "MERGE-SOURCE",
631
name_base = "BASE-REVISION",
632
start_marker=start_marker,
633
base_marker=base_marker,
634
reprocess=self.reprocess):
635
if line.startswith(start_marker):
636
retval["text_conflicts"] = True
637
yield line.replace(start_marker, '<' * 7)
641
merge3_iterator = iter_merge3(retval)
642
self.tt.create_file(merge3_iterator, trans_id)
643
if retval["text_conflicts"] is True:
644
self._raw_conflicts.append(('text conflict', trans_id))
645
name = self.tt.final_name(trans_id)
646
parent_id = self.tt.final_parent(trans_id)
647
file_group = self._dump_conflicts(name, parent_id, file_id,
648
this_lines, base_lines,
650
file_group.append(trans_id)
652
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
653
base_lines=None, other_lines=None, set_version=False,
655
"""Emit conflict files.
656
If this_lines, base_lines, or other_lines are omitted, they will be
657
determined automatically. If set_version is true, the .OTHER, .THIS
658
or .BASE (in that order) will be created as versioned files.
660
data = [('OTHER', self.other_tree, other_lines),
661
('THIS', self.this_tree, this_lines)]
663
data.append(('BASE', self.base_tree, base_lines))
666
for suffix, tree, lines in data:
668
trans_id = self._conflict_file(name, parent_id, tree, file_id,
670
file_group.append(trans_id)
671
if set_version and not versioned:
672
self.tt.version_file(file_id, trans_id)
676
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
678
"""Emit a single conflict file."""
679
name = name + '.' + suffix
680
trans_id = self.tt.create_path(name, parent_id)
681
entry = tree.inventory[file_id]
682
create_by_entry(self.tt, entry, tree, trans_id, lines)
685
def merge_executable(self, file_id, file_status):
686
"""Perform a merge on the execute bit."""
687
if file_status == "deleted":
689
trans_id = self.tt.trans_id_file_id(file_id)
691
if self.tt.final_kind(trans_id) != "file":
695
winner = self.scalar_three_way(self.this_tree, self.base_tree,
696
self.other_tree, file_id,
698
if winner == "conflict":
699
# There must be a None in here, if we have a conflict, but we
700
# need executability since file status was not deleted.
701
if self.executable(self.other_tree, file_id) is None:
706
if file_status == "modified":
707
executability = self.this_tree.is_executable(file_id)
708
if executability is not None:
709
trans_id = self.tt.trans_id_file_id(file_id)
710
self.tt.set_executability(executability, trans_id)
712
assert winner == "other"
713
if file_id in self.other_tree:
714
executability = self.other_tree.is_executable(file_id)
715
elif file_id in self.this_tree:
716
executability = self.this_tree.is_executable(file_id)
717
elif file_id in self.base_tree:
718
executability = self.base_tree.is_executable(file_id)
719
if executability is not None:
720
trans_id = self.tt.trans_id_file_id(file_id)
721
self.tt.set_executability(executability, trans_id)
723
def cook_conflicts(self, fs_conflicts):
724
"""Convert all conflicts into a form that doesn't depend on trans_id"""
725
from conflicts import Conflict
727
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
728
fp = FinalPaths(self.tt)
729
for conflict in self._raw_conflicts:
730
conflict_type = conflict[0]
731
if conflict_type in ('name conflict', 'parent conflict'):
732
trans_id = conflict[1]
733
conflict_args = conflict[2:]
734
if trans_id not in name_conflicts:
735
name_conflicts[trans_id] = {}
736
unique_add(name_conflicts[trans_id], conflict_type,
738
if conflict_type == 'contents conflict':
739
for trans_id in conflict[1]:
740
file_id = self.tt.final_file_id(trans_id)
741
if file_id is not None:
743
path = fp.get_path(trans_id)
744
for suffix in ('.BASE', '.THIS', '.OTHER'):
745
if path.endswith(suffix):
746
path = path[:-len(suffix)]
748
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
749
self.cooked_conflicts.append(c)
750
if conflict_type == 'text conflict':
751
trans_id = conflict[1]
752
path = fp.get_path(trans_id)
753
file_id = self.tt.final_file_id(trans_id)
754
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
755
self.cooked_conflicts.append(c)
757
for trans_id, conflicts in name_conflicts.iteritems():
759
this_parent, other_parent = conflicts['parent conflict']
760
assert this_parent != other_parent
762
this_parent = other_parent = \
763
self.tt.final_file_id(self.tt.final_parent(trans_id))
765
this_name, other_name = conflicts['name conflict']
766
assert this_name != other_name
768
this_name = other_name = self.tt.final_name(trans_id)
769
other_path = fp.get_path(trans_id)
770
if this_parent is not None:
772
fp.get_path(self.tt.trans_id_file_id(this_parent))
773
this_path = pathjoin(this_parent_path, this_name)
775
this_path = "<deleted>"
776
file_id = self.tt.final_file_id(trans_id)
777
c = Conflict.factory('path conflict', path=this_path,
778
conflict_path=other_path, file_id=file_id)
779
self.cooked_conflicts.append(c)
780
self.cooked_conflicts.sort(key=Conflict.sort_key)
783
class WeaveMerger(Merge3Merger):
784
"""Three-way tree merger, text weave merger."""
785
supports_reprocess = True
786
supports_show_base = False
788
def __init__(self, working_tree, this_tree, base_tree, other_tree,
789
interesting_ids=None, pb=DummyProgress(), pp=None,
791
self.this_revision_tree = self._get_revision_tree(this_tree)
792
self.other_revision_tree = self._get_revision_tree(other_tree)
793
super(WeaveMerger, self).__init__(working_tree, this_tree,
794
base_tree, other_tree,
795
interesting_ids=interesting_ids,
796
pb=pb, pp=pp, reprocess=reprocess)
798
def _get_revision_tree(self, tree):
799
"""Return a revision tree related to this tree.
800
If the tree is a WorkingTree, the basis will be returned.
802
if getattr(tree, 'get_weave', False) is False:
803
# If we have a WorkingTree, try using the basis
804
return tree.branch.basis_tree()
808
def _check_file(self, file_id):
809
"""Check that the revision tree's version of the file matches."""
810
for tree, rt in ((self.this_tree, self.this_revision_tree),
811
(self.other_tree, self.other_revision_tree)):
814
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
815
raise WorkingTreeNotRevision(self.this_tree)
817
def _merged_lines(self, file_id):
818
"""Generate the merged lines.
819
There is no distinction between lines that are meant to contain <<<<<<<
822
weave = self.this_revision_tree.get_weave(file_id)
823
this_revision_id = self.this_revision_tree.inventory[file_id].revision
824
other_revision_id = \
825
self.other_revision_tree.inventory[file_id].revision
826
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
827
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
828
return wm.merge_lines(self.reprocess)
830
def text_merge(self, file_id, trans_id):
831
"""Perform a (weave) text merge for a given file and file-id.
832
If conflicts are encountered, .THIS and .OTHER files will be emitted,
833
and a conflict will be noted.
835
self._check_file(file_id)
836
lines, conflicts = self._merged_lines(file_id)
838
# Note we're checking whether the OUTPUT is binary in this case,
839
# because we don't want to get into weave merge guts.
840
check_text_lines(lines)
841
self.tt.create_file(lines, trans_id)
843
self._raw_conflicts.append(('text conflict', trans_id))
844
name = self.tt.final_name(trans_id)
845
parent_id = self.tt.final_parent(trans_id)
846
file_group = self._dump_conflicts(name, parent_id, file_id,
848
file_group.append(trans_id)
851
class Diff3Merger(Merge3Merger):
852
"""Three-way merger using external diff3 for text merging"""
854
def dump_file(self, temp_dir, name, tree, file_id):
855
out_path = pathjoin(temp_dir, name)
856
out_file = open(out_path, "wb")
858
in_file = tree.get_file(file_id)
865
def text_merge(self, file_id, trans_id):
866
"""Perform a diff3 merge using a specified file-id and trans-id.
867
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
868
will be dumped, and a will be conflict noted.
871
temp_dir = mkdtemp(prefix="bzr-")
873
new_file = pathjoin(temp_dir, "new")
874
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
875
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
876
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
877
status = bzrlib.patch.diff3(new_file, this, base, other)
878
if status not in (0, 1):
879
raise BzrError("Unhandled diff3 exit code")
880
f = open(new_file, 'rb')
882
self.tt.create_file(f, trans_id)
886
name = self.tt.final_name(trans_id)
887
parent_id = self.tt.final_parent(trans_id)
888
self._dump_conflicts(name, parent_id, file_id)
889
self._raw_conflicts.append(('text conflict', trans_id))
894
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
896
merge_type=Merge3Merger,
897
interesting_ids=None,
901
interesting_files=None,
904
"""Primary interface for merging.
906
typical use is probably
907
'merge_inner(branch, branch.get_revision_tree(other_revision),
908
branch.get_revision_tree(base_revision))'
910
if this_tree is None:
911
warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
912
"bzrlib version 0.8.",
915
this_tree = this_branch.bzrdir.open_workingtree()
916
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
918
merger.backup_files = backup_files
919
merger.merge_type = merge_type
920
merger.interesting_ids = interesting_ids
921
merger.ignore_zero = ignore_zero
922
if interesting_files:
923
assert not interesting_ids, ('Only supply interesting_ids'
924
' or interesting_files')
925
merger._set_interesting_files(interesting_files)
926
merger.show_base = show_base
927
merger.reprocess = reprocess
928
merger.other_rev_id = other_rev_id
929
merger.other_basis = other_rev_id
930
return merger.do_merge()
933
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
934
"diff3": (Diff3Merger, "Merge using external diff3"),
935
'weave': (WeaveMerger, "Weave-based merge")
939
def merge_type_help():
940
templ = '%s%%7s: %%s' % (' '*12)
941
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
942
return '\n'.join(lines)
76
base_tree = branch.working_tree()
78
base_tree = branch.basis_tree()
80
base_tree = branch.revision_tree(branch.lookup_revision(revno))
81
temp_path = os.path.join(temp_root, label)
83
return MergeTree(base_tree, temp_path)
86
def abspath(tree, file_id):
87
path = tree.inventory.id2path(file_id)
92
def file_exists(tree, file_id):
93
return tree.has_filename(tree.id2path(file_id))
95
def inventory_map(tree):
97
for file_id in tree.inventory:
98
if not file_exists(tree, file_id):
100
path = abspath(tree, file_id)
101
inventory[path] = SourceFile(path, file_id)
105
class MergeTree(object):
106
def __init__(self, tree, tempdir):
107
object.__init__(self)
108
if hasattr(tree, "basedir"):
109
self.root = tree.basedir
112
self.inventory = inventory_map(tree)
114
self.tempdir = tempdir
115
os.mkdir(os.path.join(self.tempdir, "texts"))
118
def readonly_path(self, id):
119
if self.root is not None:
120
return self.tree.abspath(self.tree.id2path(id))
122
if self.tree.inventory[id].kind in ("directory", "root_directory"):
124
if not self.cached.has_key(id):
125
path = os.path.join(self.tempdir, "texts", id)
126
outfile = file(path, "wb")
127
outfile.write(self.tree.get_file(id).read())
128
assert(os.path.exists(path))
129
self.cached[id] = path
130
return self.cached[id]
132
def merge(other_revision, base_revision):
133
tempdir = tempfile.mkdtemp(prefix="bzr-")
135
this_branch = Branch('.')
136
other_tree = get_tree(other_revision, tempdir, "other")
137
base_tree = get_tree(base_revision, tempdir, "base")
138
merge_inner(this_branch, other_tree, base_tree, tempdir)
140
shutil.rmtree(tempdir)
143
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
144
"""Generate a changeset, using the text_id to mark really-changed files.
145
This permits blazing comparisons when text_ids are present. It also
146
disables metadata comparison for files with identical texts.
148
for file_id in tree_a.tree.inventory:
149
if file_id not in tree_b.tree.inventory:
151
entry_a = tree_a.tree.inventory[file_id]
152
entry_b = tree_b.tree.inventory[file_id]
153
if (entry_a.kind, entry_b.kind) != ("file", "file"):
155
if None in (entry_a.text_id, entry_b.text_id):
157
if entry_a.text_id != entry_b.text_id:
159
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
160
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
161
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
162
for entry in cset.entries.itervalues():
163
entry.metadata_change = None
167
def merge_inner(this_branch, other_tree, base_tree, tempdir):
168
this_tree = get_tree(('.', None), tempdir, "this")
170
def get_inventory(tree):
171
return tree.inventory
173
inv_changes = merge_flex(this_tree, base_tree, other_tree,
174
generate_cset_optimized, get_inventory,
175
MergeConflictHandler(base_tree.root))
178
for id, path in inv_changes.iteritems():
183
assert path.startswith('./')
185
adjust_ids.append((path, id))
186
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
189
def regen_inventory(this_branch, root, new_entries):
190
old_entries = this_branch.read_working_inventory()
193
for file_id in old_entries:
194
entry = old_entries[file_id]
195
path = old_entries.id2path(file_id)
196
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
197
by_path[path] = file_id
202
for path, file_id in new_entries:
204
del new_inventory[file_id]
207
new_path_list.append((path, file_id))
208
if file_id not in old_entries:
210
# Ensure no file is added before its parent
212
for path, file_id in new_path_list:
216
parent = by_path[os.path.dirname(path)]
217
kind = bzrlib.osutils.file_kind(os.path.join(root, path))
218
new_inventory[file_id] = (path, file_id, parent, kind)
219
by_path[path] = file_id
221
# Get a list in insertion order
222
new_inventory_list = new_inventory.values()
223
mutter ("""Inventory regeneration:
224
old length: %i insertions: %i deletions: %i new_length: %i"""\
225
% (len(old_entries), insertions, deletions, len(new_inventory_list)))
226
assert len(new_inventory_list) == len(old_entries) + insertions - deletions
227
new_inventory_list.sort()
228
return new_inventory_list