41
45
from bzrlib.merge3 import Merge3
42
46
from bzrlib.osutils import rename, pathjoin
43
47
from progress import DummyProgress, ProgressPhase
44
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
48
from bzrlib.revision import (NULL_REVISION, ensure_null)
45
49
from bzrlib.textfile import check_text_lines
46
50
from bzrlib.trace import mutter, warning, note
47
51
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
48
FinalPaths, create_by_entry, unique_add,
50
from bzrlib.versionedfile import WeaveMerge
52
conflict_pass, FinalPaths, create_by_entry,
53
unique_add, ROOT_PARENT)
54
from bzrlib.versionedfile import PlanWeaveMerge
51
55
from bzrlib import ui
53
57
# TODO: Report back as changes are merged in
55
def _get_tree(treespec, local_branch=None):
56
from bzrlib import workingtree
57
location, revno = treespec
59
tree = workingtree.WorkingTree.open_containing(location)[0]
60
return tree.branch, tree
61
branch = Branch.open_containing(location)[0]
63
revision_id = branch.last_revision()
65
revision_id = branch.get_rev_id(revno)
66
if revision_id is None:
67
revision_id = NULL_REVISION
68
return branch, _get_revid_tree(branch, revision_id, local_branch)
71
def _get_revid_tree(branch, revision_id, local_branch):
72
if revision_id is None:
73
base_tree = branch.bzrdir.open_workingtree()
75
if local_branch is not None:
76
if local_branch.base != branch.base:
77
local_branch.fetch(branch, revision_id)
78
base_tree = local_branch.repository.revision_tree(revision_id)
80
base_tree = branch.repository.revision_tree(revision_id)
84
def _get_revid_tree_from_tree(tree, revision_id, local_branch):
85
if revision_id is None:
87
if local_branch is not None:
88
if local_branch.base != tree.branch.base:
89
local_branch.fetch(tree.branch, revision_id)
90
return local_branch.repository.revision_tree(revision_id)
91
return tree.branch.repository.revision_tree(revision_id)
94
60
def transform_tree(from_tree, to_tree, interesting_ids=None):
95
61
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
114
81
self.ignore_zero = False
115
82
self.backup_files = False
116
83
self.interesting_ids = None
84
self.interesting_files = None
117
85
self.show_base = False
118
86
self.reprocess = False
121
89
self.recurse = recurse
122
90
self.change_reporter = change_reporter
124
def revision_tree(self, revision_id):
125
return self.this_branch.repository.revision_tree(revision_id)
91
self._cached_trees = {}
94
def from_uncommitted(tree, other_tree, pb):
95
"""Return a Merger for uncommitted changes in other_tree.
97
:param tree: The tree to merge into
98
:param other_tree: The tree to get uncommitted changes from
99
:param pb: A progress indicator
101
merger = Merger(tree.branch, other_tree, other_tree.basis_tree(), tree,
103
merger.base_rev_id = merger.base_tree.get_revision_id()
104
merger.other_rev_id = None
108
def from_mergeable(klass, tree, mergeable, pb):
109
"""Return a Merger for a bundle or merge directive.
111
:param tree: The tree to merge changes into
112
:param mergeable: A merge directive or bundle
113
:param pb: A progress indicator
115
mergeable.install_revisions(tree.branch.repository)
116
base_revision_id, other_revision_id, verified =\
117
mergeable.get_merge_request(tree.branch.repository)
118
if (base_revision_id != _mod_revision.NULL_REVISION and
119
tree.branch.repository.get_graph().is_ancestor(
120
base_revision_id, tree.branch.last_revision())):
121
base_revision_id = None
123
warning('Performing cherrypick')
124
merger = klass.from_revision_ids(pb, tree, other_revision_id,
126
return merger, verified
129
def from_revision_ids(pb, this, other, base=None, other_branch=None,
131
"""Return a Merger for revision-ids.
133
:param tree: The tree to merge changes into
134
:param other: The revision-id to use as OTHER
135
:param base: The revision-id to use as BASE. If not specified, will
137
:param other_branch: A branch containing the other revision-id. If
138
not supplied, this.branch is used.
139
:param base_branch: A branch containing the base revision-id. If
140
not supplied, other_branch or this.branch will be used.
141
:param pb: A progress indicator
143
merger = Merger(this.branch, this_tree=this, pb=pb)
144
if other_branch is None:
145
other_branch = this.branch
146
merger.set_other_revision(other, other_branch)
150
if base_branch is None:
151
base_branch = other_branch
152
merger.set_base_revision(base, base_branch)
155
def revision_tree(self, revision_id, branch=None):
156
if revision_id not in self._cached_trees:
158
branch = self.this_branch
160
tree = self.this_tree.revision_tree(revision_id)
161
except errors.NoSuchRevisionInTree:
162
tree = branch.repository.revision_tree(revision_id)
163
self._cached_trees[revision_id] = tree
164
return self._cached_trees[revision_id]
166
def _get_tree(self, treespec, possible_transports=None):
167
from bzrlib import workingtree
168
location, revno = treespec
170
tree = workingtree.WorkingTree.open_containing(location)[0]
171
return tree.branch, tree
172
branch = Branch.open_containing(location, possible_transports)[0]
174
revision_id = branch.last_revision()
176
revision_id = branch.get_rev_id(revno)
177
revision_id = ensure_null(revision_id)
178
return branch, self.revision_tree(revision_id, branch)
127
180
def ensure_revision_trees(self):
128
181
if self.this_revision_tree is None:
129
self.this_basis_tree = self.this_branch.repository.revision_tree(
182
self.this_basis_tree = self.revision_tree(self.this_basis)
131
183
if self.this_basis == self.this_rev_id:
132
184
self.this_revision_tree = self.this_basis_tree
161
213
self.compare_basis()
162
214
if self.this_basis != self.this_rev_id:
163
raise BzrCommandError("Working tree has uncommitted changes.")
215
raise errors.UncommittedChanges(self.this_tree)
165
217
def compare_basis(self):
166
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
219
basis_tree = self.revision_tree(self.this_tree.last_revision())
220
except errors.RevisionNotPresent:
221
basis_tree = self.this_tree.basis_tree()
222
changes = self.this_tree.changes_from(basis_tree)
167
223
if not changes.has_changed():
168
224
self.this_rev_id = self.this_basis
170
226
def set_interesting_files(self, file_list):
172
self._set_interesting_files(file_list)
173
except NotVersionedError, e:
174
raise BzrCommandError("%s is not a source file in any"
177
def _set_interesting_files(self, file_list):
178
"""Set the list of interesting ids from a list of files."""
179
if file_list is None:
180
self.interesting_ids = None
183
interesting_ids = set()
184
for path in file_list:
186
# TODO: jam 20070226 The trees are not locked at this time,
187
# wouldn't it make merge faster if it locks everything in the
188
# beginning? It locks at do_merge time, but this happens
190
for tree in (self.this_tree, self.base_tree, self.other_tree):
191
file_id = tree.path2id(path)
192
if file_id is not None:
193
interesting_ids.add(file_id)
196
raise NotVersionedError(path=path)
197
self.interesting_ids = interesting_ids
227
self.interesting_files = file_list
199
229
def set_pending(self):
200
if not self.base_is_ancestor:
202
if self.other_rev_id is None:
204
ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
205
if self.other_rev_id in ancestry:
207
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
209
def set_other(self, other_revision):
230
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
234
def _add_parent(self):
235
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
236
new_parent_trees = []
237
for revision_id in new_parents:
239
tree = self.revision_tree(revision_id)
240
except errors.RevisionNotPresent:
244
new_parent_trees.append((revision_id, tree))
246
self.this_tree.set_parent_trees(new_parent_trees,
247
allow_leftmost_as_ghost=True)
249
for _revision_id, tree in new_parent_trees:
253
def set_other(self, other_revision, possible_transports=None):
210
254
"""Set the revision and tree to merge from.
212
256
This sets the other_tree, other_rev_id, other_basis attributes.
214
258
:param other_revision: The [path, revision] list to merge from.
216
self.other_branch, self.other_tree = _get_tree(other_revision,
260
self.other_branch, self.other_tree = self._get_tree(other_revision,
218
262
if other_revision[1] == -1:
219
self.other_rev_id = self.other_branch.last_revision()
220
if self.other_rev_id is None:
263
self.other_rev_id = _mod_revision.ensure_null(
264
self.other_branch.last_revision())
265
if _mod_revision.is_null(self.other_rev_id):
221
266
raise NoCommits(self.other_branch)
222
267
self.other_basis = self.other_rev_id
223
268
elif other_revision[1] is not None:
241
286
self.other_rev_id = revision_id
242
287
self.other_branch = other_branch
243
self.this_branch.fetch(other_branch, self.other_rev_id)
288
self._maybe_fetch(other_branch, self.this_branch, self.other_rev_id)
244
289
self.other_tree = self.revision_tree(revision_id)
245
290
self.other_basis = revision_id
292
def set_base_revision(self, revision_id, branch):
293
"""Set 'base' based on a branch and revision id
295
:param revision_id: The revision to use for a tree
296
:param branch: The branch containing this tree
298
self.base_rev_id = revision_id
299
self.base_branch = branch
300
self._maybe_fetch(branch, self.this_branch, revision_id)
301
self.base_tree = self.revision_tree(revision_id)
302
graph = self.this_branch.repository.get_graph()
303
self.base_is_ancestor = graph.is_ancestor(self.base_rev_id,
305
self.base_is_other_ancestor = graph.is_ancestor(self.base_rev_id,
308
def _maybe_fetch(self, source, target, revision_id):
309
if not source.repository.has_same_location(target.repository):
310
target.fetch(source, revision_id)
247
312
def find_base(self):
248
self.set_base([None, None])
313
this_repo = self.this_branch.repository
314
graph = this_repo.get_graph()
315
revisions = [ensure_null(self.this_basis),
316
ensure_null(self.other_basis)]
317
if NULL_REVISION in revisions:
318
self.base_rev_id = NULL_REVISION
320
self.base_rev_id, steps = graph.find_unique_lca(revisions[0],
321
revisions[1], count_steps=True)
322
if self.base_rev_id == NULL_REVISION:
323
raise UnrelatedBranches()
325
warning('Warning: criss-cross merge encountered. See bzr'
326
' help criss-cross.')
327
self.base_tree = self.revision_tree(self.base_rev_id)
328
self.base_is_ancestor = True
329
self.base_is_other_ancestor = True
250
331
def set_base(self, base_revision):
251
332
"""Set the base revision to use for the merge.
255
336
mutter("doing merge() with no base_revision specified")
256
337
if base_revision == [None, None]:
258
pb = ui.ui_factory.nested_progress_bar()
260
this_repo = self.this_branch.repository
261
self.base_rev_id = common_ancestor(self.this_basis,
266
except NoCommonAncestor:
267
raise UnrelatedBranches()
268
self.base_tree = _get_revid_tree_from_tree(self.this_tree,
271
self.base_is_ancestor = True
273
base_branch, self.base_tree = _get_tree(base_revision)
340
base_branch, self.base_tree = self._get_tree(base_revision)
274
341
if base_revision[1] == -1:
275
342
self.base_rev_id = base_branch.last_revision()
276
343
elif base_revision[1] is None:
277
self.base_rev_id = None
344
self.base_rev_id = _mod_revision.NULL_REVISION
279
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
280
if self.this_branch.base != base_branch.base:
281
self.this_branch.fetch(base_branch)
282
self.base_is_ancestor = is_ancestor(self.this_basis,
346
self.base_rev_id = _mod_revision.ensure_null(
347
base_branch.get_rev_id(base_revision[1]))
348
self._maybe_fetch(base_branch, self.this_branch, self.base_rev_id)
349
graph = self.this_branch.repository.get_graph()
350
self.base_is_ancestor = graph.is_ancestor(self.base_rev_id,
352
self.base_is_other_ancestor = graph.is_ancestor(self.base_rev_id,
286
355
def do_merge(self):
287
356
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
288
357
'other_tree': self.other_tree,
289
358
'interesting_ids': self.interesting_ids,
359
'interesting_files': self.interesting_files,
291
361
if self.merge_type.requires_base:
292
362
kwargs['base_tree'] = self.base_tree
341
417
return len(merge.cooked_conflicts)
343
def regen_inventory(self, new_entries):
344
old_entries = self.this_tree.read_working_inventory()
348
for path, file_id in new_entries:
351
new_entries_map[file_id] = path
353
def id2path(file_id):
354
path = new_entries_map.get(file_id)
357
entry = old_entries[file_id]
358
if entry.parent_id is None:
360
return pathjoin(id2path(entry.parent_id), entry.name)
362
for file_id in old_entries:
363
entry = old_entries[file_id]
364
path = id2path(file_id)
365
if file_id in self.base_tree.inventory:
366
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
368
executable = getattr(entry, 'executable', False)
369
new_inventory[file_id] = (path, file_id, entry.parent_id,
370
entry.kind, executable)
372
by_path[path] = file_id
377
for path, file_id in new_entries:
379
del new_inventory[file_id]
382
new_path_list.append((path, file_id))
383
if file_id not in old_entries:
385
# Ensure no file is added before its parent
387
for path, file_id in new_path_list:
391
parent = by_path[os.path.dirname(path)]
392
abspath = pathjoin(self.this_tree.basedir, path)
393
kind = osutils.file_kind(abspath)
394
if file_id in self.base_tree.inventory:
395
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
398
new_inventory[file_id] = (path, file_id, parent, kind, executable)
399
by_path[path] = file_id
401
# Get a list in insertion order
402
new_inventory_list = new_inventory.values()
403
mutter ("""Inventory regeneration:
404
old length: %i insertions: %i deletions: %i new_length: %i"""\
405
% (len(old_entries), insertions, deletions,
406
len(new_inventory_list)))
407
assert len(new_inventory_list) == len(old_entries) + insertions\
409
new_inventory_list.sort()
410
return new_inventory_list
413
420
class Merge3Merger(object):
414
421
"""Three-way merger that uses the merge3 text merger"""
416
423
supports_reprocess = True
417
424
supports_show_base = True
418
425
history_based = False
426
supports_reverse_cherrypick = True
427
winner_idx = {"this": 2, "other": 1, "conflict": 1}
420
429
def __init__(self, working_tree, this_tree, base_tree, other_tree,
421
430
interesting_ids=None, reprocess=False, show_base=False,
422
pb=DummyProgress(), pp=None, change_reporter=None):
423
"""Initialize the merger object and perform the merge."""
431
pb=DummyProgress(), pp=None, change_reporter=None,
432
interesting_files=None):
433
"""Initialize the merger object and perform the merge.
435
:param working_tree: The working tree to apply the merge to
436
:param this_tree: The local tree in the merge operation
437
:param base_tree: The common tree in the merge operation
438
:param other_tree: The other other tree to merge changes from
439
:param interesting_ids: The file_ids of files that should be
440
participate in the merge. May not be combined with
442
:param: reprocess If True, perform conflict-reduction processing.
443
:param show_base: If True, show the base revision in text conflicts.
444
(incompatible with reprocess)
445
:param pb: A Progress bar
446
:param pp: A ProgressPhase object
447
:param change_reporter: An object that should report changes made
448
:param interesting_files: The tree-relative paths of files that should
449
participate in the merge. If these paths refer to directories,
450
the contents of those directories will also be included. May not
451
be combined with interesting_ids. If neither interesting_files nor
452
interesting_ids is specified, all files may participate in the
424
455
object.__init__(self)
456
if interesting_files is not None:
457
assert interesting_ids is None
458
self.interesting_ids = interesting_ids
459
self.interesting_files = interesting_files
425
460
self.this_tree = working_tree
426
461
self.this_tree.lock_tree_write()
427
462
self.base_tree = base_tree
438
473
if self.pp is None:
439
474
self.pp = ProgressPhase("Merge phase", 3, self.pb)
441
if interesting_ids is not None:
442
all_ids = interesting_ids
444
all_ids = set(base_tree)
445
all_ids.update(other_tree)
446
476
self.tt = TreeTransform(working_tree, self.pb)
448
478
self.pp.next_phase()
479
entries = self._entries3()
449
480
child_pb = ui.ui_factory.nested_progress_bar()
451
for num, file_id in enumerate(all_ids):
452
child_pb.update('Preparing file merge', num, len(all_ids))
453
self.merge_names(file_id)
454
file_status = self.merge_contents(file_id)
455
self.merge_executable(file_id, file_status)
482
for num, (file_id, changed, parents3, names3,
483
executable3) in enumerate(entries):
484
child_pb.update('Preparing file merge', num, len(entries))
485
self._merge_names(file_id, parents3, names3)
487
file_status = self.merge_contents(file_id)
489
file_status = 'unmodified'
490
self._merge_executable(file_id,
491
executable3, file_status)
457
493
child_pb.finished()
459
495
self.pp.next_phase()
460
496
child_pb = ui.ui_factory.nested_progress_bar()
462
fs_conflicts = resolve_conflicts(self.tt, child_pb)
498
fs_conflicts = resolve_conflicts(self.tt, child_pb,
499
lambda t, c: conflict_pass(t, c, self.other_tree))
464
501
child_pb.finished()
465
502
if change_reporter is not None:
482
519
self.this_tree.unlock()
523
"""Gather data about files modified between three trees.
525
Return a list of tuples of file_id, changed, parents3, names3,
526
executable3. changed is a boolean indicating whether the file contents
527
or kind were changed. parents3 is a tuple of parent ids for base,
528
other and this. names3 is a tuple of names for base, other and this.
529
executable3 is a tuple of execute-bit values for base, other and this.
532
iterator = self.other_tree._iter_changes(self.base_tree,
533
include_unchanged=True, specific_files=self.interesting_files,
534
extra_trees=[self.this_tree])
535
for (file_id, paths, changed, versioned, parents, names, kind,
536
executable) in iterator:
537
if (self.interesting_ids is not None and
538
file_id not in self.interesting_ids):
540
if file_id in self.this_tree.inventory:
541
entry = self.this_tree.inventory[file_id]
542
this_name = entry.name
543
this_parent = entry.parent_id
544
this_executable = entry.executable
548
this_executable = None
549
parents3 = parents + (this_parent,)
550
names3 = names + (this_name,)
551
executable3 = executable + (this_executable,)
552
result.append((file_id, changed, parents3, names3, executable3))
485
555
def fix_root(self):
487
557
self.tt.final_kind(self.tt.root)
588
671
this_entry = get_entry(self.this_tree)
589
672
other_entry = get_entry(self.other_tree)
590
673
base_entry = get_entry(self.base_tree)
591
name_winner = self.scalar_three_way(this_entry, base_entry,
592
other_entry, file_id, self.name)
593
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
594
other_entry, file_id,
596
if this_entry is None:
674
entries = (base_entry, other_entry, this_entry)
677
for entry in entries:
682
names.append(entry.name)
683
parents.append(entry.parent_id)
684
return self._merge_names(file_id, parents, names)
686
def _merge_names(self, file_id, parents, names):
687
"""Perform a merge on file_id names and parents"""
688
base_name, other_name, this_name = names
689
base_parent, other_parent, this_parent = parents
691
name_winner = self._three_way(*names)
693
parent_id_winner = self._three_way(*parents)
694
if this_name is None:
597
695
if name_winner == "this":
598
696
name_winner = "other"
599
697
if parent_id_winner == "this":
603
701
if name_winner == "conflict":
604
702
trans_id = self.tt.trans_id_file_id(file_id)
605
703
self._raw_conflicts.append(('name conflict', trans_id,
606
self.name(this_entry, file_id),
607
self.name(other_entry, file_id)))
704
this_name, other_name))
608
705
if parent_id_winner == "conflict":
609
706
trans_id = self.tt.trans_id_file_id(file_id)
610
707
self._raw_conflicts.append(('parent conflict', trans_id,
611
self.parent(this_entry, file_id),
612
self.parent(other_entry, file_id)))
613
if other_entry is None:
708
this_parent, other_parent))
709
if other_name is None:
614
710
# it doesn't matter whether the result was 'other' or
615
711
# 'conflict'-- if there's no 'other', we leave it alone.
617
713
# if we get here, name_winner and parent_winner are set to safe values.
618
winner_entry = {"this": this_entry, "other": other_entry,
619
"conflict": other_entry}
620
714
trans_id = self.tt.trans_id_file_id(file_id)
621
parent_id = winner_entry[parent_id_winner].parent_id
715
parent_id = parents[self.winner_idx[parent_id_winner]]
622
716
if parent_id is not None:
623
717
parent_trans_id = self.tt.trans_id_file_id(parent_id)
624
self.tt.adjust_path(winner_entry[name_winner].name,
718
self.tt.adjust_path(names[self.winner_idx[name_winner]],
625
719
parent_trans_id, trans_id)
627
721
def merge_contents(self, file_id):
887
986
"""Three-way tree merger, text weave merger."""
888
987
supports_reprocess = True
889
988
supports_show_base = False
989
supports_reverse_cherrypick = False
891
992
def __init__(self, working_tree, this_tree, base_tree, other_tree,
892
993
interesting_ids=None, pb=DummyProgress(), pp=None,
893
reprocess=False, change_reporter=None):
894
self.this_revision_tree = self._get_revision_tree(this_tree)
895
self.other_revision_tree = self._get_revision_tree(other_tree)
994
reprocess=False, change_reporter=None,
995
interesting_files=None, cherrypick=False):
996
self.cherrypick = cherrypick
896
997
super(WeaveMerger, self).__init__(working_tree, this_tree,
897
998
base_tree, other_tree,
898
999
interesting_ids=interesting_ids,
899
1000
pb=pb, pp=pp, reprocess=reprocess,
900
1001
change_reporter=change_reporter)
902
def _get_revision_tree(self, tree):
903
"""Return a revision tree related to this tree.
904
If the tree is a WorkingTree, the basis will be returned.
906
if getattr(tree, 'get_weave', False) is False:
907
# If we have a WorkingTree, try using the basis
908
return tree.branch.basis_tree()
912
def _check_file(self, file_id):
913
"""Check that the revision tree's version of the file matches."""
914
for tree, rt in ((self.this_tree, self.this_revision_tree),
915
(self.other_tree, self.other_revision_tree)):
918
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
919
raise WorkingTreeNotRevision(self.this_tree)
921
1003
def _merged_lines(self, file_id):
922
1004
"""Generate the merged lines.
923
1005
There is no distinction between lines that are meant to contain <<<<<<<
926
weave = self.this_revision_tree.get_weave(file_id)
927
this_revision_id = self.this_revision_tree.inventory[file_id].revision
928
other_revision_id = \
929
self.other_revision_tree.inventory[file_id].revision
930
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
931
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
932
return wm.merge_lines(self.reprocess)
1009
base = self.base_tree
1012
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1014
if 'merge' in debug.debug_flags:
1016
trans_id = self.tt.trans_id_file_id(file_id)
1017
name = self.tt.final_name(trans_id) + '.plan'
1018
contents = ('%10s|%s' % l for l in plan)
1019
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1020
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1021
'>>>>>>> MERGE-SOURCE\n')
1022
return textmerge.merge_lines(self.reprocess)
934
1024
def text_merge(self, file_id, trans_id):
935
1025
"""Perform a (weave) text merge for a given file and file-id.
936
1026
If conflicts are encountered, .THIS and .OTHER files will be emitted,
937
1027
and a conflict will be noted.
939
self._check_file(file_id)
940
1029
lines, conflicts = self._merged_lines(file_id)
941
1030
lines = list(lines)
942
1031
# Note we're checking whether the OUTPUT is binary in this case,
1039
1128
from bzrlib import option
1040
1129
return option._merge_type_registry
1132
def _plan_annotate_merge(annotated_a, annotated_b, ancestors_a, ancestors_b):
1133
def status_a(revision, text):
1134
if revision in ancestors_b:
1135
return 'killed-b', text
1137
return 'new-a', text
1139
def status_b(revision, text):
1140
if revision in ancestors_a:
1141
return 'killed-a', text
1143
return 'new-b', text
1145
plain_a = [t for (a, t) in annotated_a]
1146
plain_b = [t for (a, t) in annotated_b]
1147
matcher = patiencediff.PatienceSequenceMatcher(None, plain_a, plain_b)
1148
blocks = matcher.get_matching_blocks()
1151
for ai, bi, l in blocks:
1152
# process all mismatched sections
1153
# (last mismatched section is handled because blocks always
1154
# includes a 0-length last block)
1155
for revision, text in annotated_a[a_cur:ai]:
1156
yield status_a(revision, text)
1157
for revision, text in annotated_b[b_cur:bi]:
1158
yield status_b(revision, text)
1160
# and now the matched section
1163
for text_a, text_b in zip(plain_a[ai:a_cur], plain_b[bi:b_cur]):
1164
assert text_a == text_b
1165
yield "unchanged", text_a
1168
class _PlanMerge(object):
1169
"""Plan an annotate merge using on-the-fly annotation"""
1171
def __init__(self, a_rev, b_rev, vf):
1174
:param a_rev: Revision-id of one revision to merge
1175
:param b_rev: Revision-id of the other revision to merge
1176
:param vf: A versionedfile containing both revisions
1180
self.lines_a = vf.get_lines(a_rev)
1181
self.lines_b = vf.get_lines(b_rev)
1183
a_ancestry = set(vf.get_ancestry(a_rev, topo_sorted=False))
1184
b_ancestry = set(vf.get_ancestry(b_rev, topo_sorted=False))
1185
self.uncommon = a_ancestry.symmetric_difference(b_ancestry)
1186
self._last_lines = None
1187
self._last_lines_revision_id = None
1189
def plan_merge(self):
1190
"""Generate a 'plan' for merging the two revisions.
1192
This involves comparing their texts and determining the cause of
1193
differences. If text A has a line and text B does not, then either the
1194
line was added to text A, or it was deleted from B. Once the causes
1195
are combined, they are written out in the format described in
1196
VersionedFile.plan_merge
1198
blocks = self._get_matching_blocks(self.a_rev, self.b_rev)
1199
new_a = self._find_new(self.a_rev)
1200
new_b = self._find_new(self.b_rev)
1203
a_lines = self.vf.get_lines(self.a_rev)
1204
b_lines = self.vf.get_lines(self.b_rev)
1205
for i, j, n in blocks:
1206
# determine why lines aren't common
1207
for a_index in range(last_i, i):
1208
if a_index in new_a:
1212
yield cause, a_lines[a_index]
1213
for b_index in range(last_j, j):
1214
if b_index in new_b:
1218
yield cause, b_lines[b_index]
1219
# handle common lines
1220
for a_index in range(i, i+n):
1221
yield 'unchanged', a_lines[a_index]
1225
def _get_matching_blocks(self, left_revision, right_revision):
1226
"""Return a description of which sections of two revisions match.
1228
See SequenceMatcher.get_matching_blocks
1230
if self._last_lines_revision_id == left_revision:
1231
left_lines = self._last_lines
1233
left_lines = self.vf.get_lines(left_revision)
1234
right_lines = self.vf.get_lines(right_revision)
1235
self._last_lines = right_lines
1236
self._last_lines_revision_id = right_revision
1237
matcher = patiencediff.PatienceSequenceMatcher(None, left_lines,
1239
return matcher.get_matching_blocks()
1241
def _unique_lines(self, matching_blocks):
1242
"""Analyse matching_blocks to determine which lines are unique
1244
:return: a tuple of (unique_left, unique_right), where the values are
1245
sets of line numbers of unique lines.
1251
for i, j, n in matching_blocks:
1252
unique_left.extend(range(last_i, i))
1253
unique_right.extend(range(last_j, j))
1256
return unique_left, unique_right
1258
def _find_new(self, version_id):
1259
"""Determine which lines are new in the ancestry of this version.
1261
If a lines is present in this version, and not present in any
1262
common ancestor, it is considered new.
1264
if version_id not in self.uncommon:
1266
parents = self.vf.get_parents(version_id)
1267
if len(parents) == 0:
1268
return set(range(len(self.vf.get_lines(version_id))))
1270
for parent in parents:
1271
blocks = self._get_matching_blocks(version_id, parent)
1272
result, unused = self._unique_lines(blocks)
1273
parent_new = self._find_new(parent)
1274
for i, j, n in blocks:
1275
for ii, jj in [(i+r, j+r) for r in range(n)]:
1276
if jj in parent_new:
1281
new.intersection_update(result)
1285
def _subtract_plans(old_plan, new_plan):
1286
matcher = patiencediff.PatienceSequenceMatcher(None, old_plan,
1289
for i, j, n in matcher.get_matching_blocks():
1290
for jj in range(last_j, j):
1292
for jj in range(j, j+n):
1293
plan_line = new_plan[jj]
1294
if plan_line[0] == 'new-b':
1296
elif plan_line[0] == 'killed-b':
1297
yield 'unchanged', plan_line[1]