41
44
from bzrlib.merge3 import Merge3
42
45
from bzrlib.osutils import rename, pathjoin
43
46
from progress import DummyProgress, ProgressPhase
44
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
47
from bzrlib.revision import (is_ancestor, NULL_REVISION, ensure_null)
45
48
from bzrlib.textfile import check_text_lines
46
49
from bzrlib.trace import mutter, warning, note
47
50
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
48
FinalPaths, create_by_entry, unique_add,
50
from bzrlib.versionedfile import WeaveMerge
51
conflict_pass, FinalPaths, create_by_entry,
52
unique_add, ROOT_PARENT)
53
from bzrlib.versionedfile import PlanWeaveMerge
51
54
from bzrlib import ui
53
56
# 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
59
def transform_tree(from_tree, to_tree, interesting_ids=None):
95
60
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
114
80
self.ignore_zero = False
115
81
self.backup_files = False
116
82
self.interesting_ids = None
83
self.interesting_files = None
117
84
self.show_base = False
118
85
self.reprocess = False
121
88
self.recurse = recurse
122
89
self.change_reporter = change_reporter
124
def revision_tree(self, revision_id):
125
return self.this_branch.repository.revision_tree(revision_id)
90
self._cached_trees = {}
92
def revision_tree(self, revision_id, branch=None):
93
if revision_id not in self._cached_trees:
95
branch = self.this_branch
97
tree = self.this_tree.revision_tree(revision_id)
98
except errors.NoSuchRevisionInTree:
99
tree = branch.repository.revision_tree(revision_id)
100
self._cached_trees[revision_id] = tree
101
return self._cached_trees[revision_id]
103
def _get_tree(self, treespec, possible_transports=None):
104
from bzrlib import workingtree
105
location, revno = treespec
107
tree = workingtree.WorkingTree.open_containing(location)[0]
108
return tree.branch, tree
109
branch = Branch.open_containing(location, possible_transports)[0]
111
revision_id = branch.last_revision()
113
revision_id = branch.get_rev_id(revno)
114
revision_id = ensure_null(revision_id)
115
return branch, self.revision_tree(revision_id, branch)
127
117
def ensure_revision_trees(self):
128
118
if self.this_revision_tree is None:
129
self.this_basis_tree = self.this_branch.repository.revision_tree(
119
self.this_basis_tree = self.revision_tree(self.this_basis)
131
120
if self.this_basis == self.this_rev_id:
132
121
self.this_revision_tree = self.this_basis_tree
163
152
raise BzrCommandError("Working tree has uncommitted changes.")
165
154
def compare_basis(self):
166
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
156
basis_tree = self.revision_tree(self.this_tree.last_revision())
157
except errors.RevisionNotPresent:
158
basis_tree = self.this_tree.basis_tree()
159
changes = self.this_tree.changes_from(basis_tree)
167
160
if not changes.has_changed():
168
161
self.this_rev_id = self.this_basis
170
163
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
164
self.interesting_files = file_list
199
166
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):
167
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
171
def _add_parent(self):
172
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
173
new_parent_trees = []
174
for revision_id in new_parents:
176
tree = self.revision_tree(revision_id)
177
except errors.RevisionNotPresent:
181
new_parent_trees.append((revision_id, tree))
183
self.this_tree.set_parent_trees(new_parent_trees,
184
allow_leftmost_as_ghost=True)
186
for _revision_id, tree in new_parent_trees:
190
def set_other(self, other_revision, possible_transports=None):
210
191
"""Set the revision and tree to merge from.
212
193
This sets the other_tree, other_rev_id, other_basis attributes.
214
195
:param other_revision: The [path, revision] list to merge from.
216
self.other_branch, self.other_tree = _get_tree(other_revision,
197
self.other_branch, self.other_tree = self._get_tree(other_revision,
218
199
if other_revision[1] == -1:
219
self.other_rev_id = self.other_branch.last_revision()
220
if self.other_rev_id is None:
200
self.other_rev_id = _mod_revision.ensure_null(
201
self.other_branch.last_revision())
202
if _mod_revision.is_null(self.other_rev_id):
221
203
raise NoCommits(self.other_branch)
222
204
self.other_basis = self.other_rev_id
223
205
elif other_revision[1] is not None:
228
210
self.other_basis = self.other_branch.last_revision()
229
211
if self.other_basis is None:
230
212
raise NoCommits(self.other_branch)
231
if self.other_branch.base != self.this_branch.base:
232
self.this_branch.fetch(self.other_branch,
233
last_revision=self.other_basis)
213
if self.other_rev_id is not None:
214
self._cached_trees[self.other_rev_id] = self.other_tree
215
self._maybe_fetch(self.other_branch,self.this_branch, self.other_basis)
235
217
def set_other_revision(self, revision_id, other_branch):
236
218
"""Set 'other' based on a branch and revision id
241
223
self.other_rev_id = revision_id
242
224
self.other_branch = other_branch
243
self.this_branch.fetch(other_branch, self.other_rev_id)
225
self._maybe_fetch(other_branch, self.this_branch, self.other_rev_id)
244
226
self.other_tree = self.revision_tree(revision_id)
245
227
self.other_basis = revision_id
229
def set_base_revision(self, revision_id, branch):
230
"""Set 'base' based on a branch and revision id
232
:param revision_id: The revision to use for a tree
233
:param branch: The branch containing this tree
235
self.base_rev_id = revision_id
236
self.base_branch = branch
237
self._maybe_fetch(branch, self.this_branch, revision_id)
238
self.base_tree = self.revision_tree(revision_id)
239
self.base_is_ancestor = is_ancestor(self.this_basis,
242
self.base_is_other_ancestor = is_ancestor(self.other_basis,
246
def _maybe_fetch(self, source, target, revision_id):
247
if (source.repository.bzrdir.root_transport.base !=
248
target.repository.bzrdir.root_transport.base):
249
target.fetch(source, revision_id)
247
251
def find_base(self):
248
self.set_base([None, None])
252
this_repo = self.this_branch.repository
253
graph = this_repo.get_graph()
254
revisions = [ensure_null(self.this_basis),
255
ensure_null(self.other_basis)]
256
if NULL_REVISION in revisions:
257
self.base_rev_id = NULL_REVISION
259
self.base_rev_id = graph.find_unique_lca(*revisions)
260
if self.base_rev_id == NULL_REVISION:
261
raise UnrelatedBranches()
262
self.base_tree = self.revision_tree(self.base_rev_id)
263
self.base_is_ancestor = True
264
self.base_is_other_ancestor = True
250
266
def set_base(self, base_revision):
251
267
"""Set the base revision to use for the merge.
255
271
mutter("doing merge() with no base_revision specified")
256
272
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)
275
base_branch, self.base_tree = self._get_tree(base_revision)
274
276
if base_revision[1] == -1:
275
277
self.base_rev_id = base_branch.last_revision()
276
278
elif base_revision[1] is None:
277
self.base_rev_id = None
279
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)
281
self.base_rev_id = _mod_revision.ensure_null(
282
base_branch.get_rev_id(base_revision[1]))
283
self._maybe_fetch(base_branch, self.this_branch, self.base_rev_id)
282
284
self.base_is_ancestor = is_ancestor(self.this_basis,
283
285
self.base_rev_id,
284
286
self.this_branch)
287
self.base_is_other_ancestor = is_ancestor(self.other_basis,
286
291
def do_merge(self):
287
292
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
288
293
'other_tree': self.other_tree,
289
294
'interesting_ids': self.interesting_ids,
295
'interesting_files': self.interesting_files,
291
297
if self.merge_type.requires_base:
292
298
kwargs['base_tree'] = self.base_tree
341
347
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
350
class Merge3Merger(object):
414
351
"""Three-way merger that uses the merge3 text merger"""
416
353
supports_reprocess = True
417
354
supports_show_base = True
418
355
history_based = False
356
winner_idx = {"this": 2, "other": 1, "conflict": 1}
420
358
def __init__(self, working_tree, this_tree, base_tree, other_tree,
421
359
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."""
360
pb=DummyProgress(), pp=None, change_reporter=None,
361
interesting_files=None):
362
"""Initialize the merger object and perform the merge.
364
:param working_tree: The working tree to apply the merge to
365
:param this_tree: The local tree in the merge operation
366
:param base_tree: The common tree in the merge operation
367
:param other_tree: The other other tree to merge changes from
368
:param interesting_ids: The file_ids of files that should be
369
participate in the merge. May not be combined with
371
:param: reprocess If True, perform conflict-reduction processing.
372
:param show_base: If True, show the base revision in text conflicts.
373
(incompatible with reprocess)
374
:param pb: A Progress bar
375
:param pp: A ProgressPhase object
376
:param change_reporter: An object that should report changes made
377
:param interesting_files: The tree-relative paths of files that should
378
participate in the merge. If these paths refer to directories,
379
the contents of those directories will also be included. May not
380
be combined with interesting_ids. If neither interesting_files nor
381
interesting_ids is specified, all files may participate in the
424
384
object.__init__(self)
385
if interesting_files is not None:
386
assert interesting_ids is None
387
self.interesting_ids = interesting_ids
388
self.interesting_files = interesting_files
425
389
self.this_tree = working_tree
426
390
self.this_tree.lock_tree_write()
427
391
self.base_tree = base_tree
438
402
if self.pp is None:
439
403
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
405
self.tt = TreeTransform(working_tree, self.pb)
448
407
self.pp.next_phase()
408
entries = self._entries3()
449
409
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)
411
for num, (file_id, changed, parents3, names3,
412
executable3) in enumerate(entries):
413
child_pb.update('Preparing file merge', num, len(entries))
414
self._merge_names(file_id, parents3, names3)
416
file_status = self.merge_contents(file_id)
418
file_status = 'unmodified'
419
self._merge_executable(file_id,
420
executable3, file_status)
457
422
child_pb.finished()
459
424
self.pp.next_phase()
460
425
child_pb = ui.ui_factory.nested_progress_bar()
462
fs_conflicts = resolve_conflicts(self.tt, child_pb)
427
fs_conflicts = resolve_conflicts(self.tt, child_pb,
428
lambda t, c: conflict_pass(t, c, self.other_tree))
464
430
child_pb.finished()
465
431
if change_reporter is not None:
482
448
self.this_tree.unlock()
452
"""Gather data about files modified between three trees.
454
Return a list of tuples of file_id, changed, parents3, names3,
455
executable3. changed is a boolean indicating whether the file contents
456
or kind were changed. parents3 is a tuple of parent ids for base,
457
other and this. names3 is a tuple of names for base, other and this.
458
executable3 is a tuple of execute-bit values for base, other and this.
461
iterator = self.other_tree._iter_changes(self.base_tree,
462
include_unchanged=True, specific_files=self.interesting_files,
463
extra_trees=[self.this_tree])
464
for (file_id, paths, changed, versioned, parents, names, kind,
465
executable) in iterator:
466
if (self.interesting_ids is not None and
467
file_id not in self.interesting_ids):
469
if file_id in self.this_tree.inventory:
470
entry = self.this_tree.inventory[file_id]
471
this_name = entry.name
472
this_parent = entry.parent_id
473
this_executable = entry.executable
477
this_executable = None
478
parents3 = parents + (this_parent,)
479
names3 = names + (this_name,)
480
executable3 = executable + (this_executable,)
481
result.append((file_id, changed, parents3, names3, executable3))
485
484
def fix_root(self):
487
486
self.tt.final_kind(self.tt.root)
559
558
return tree.kind(file_id)
561
def _three_way(base, other, this):
562
#if base == other, either they all agree, or only THIS has changed.
565
elif this not in (base, other):
567
# "Ambiguous clean merge" -- both sides have made the same change.
570
# this == base: only other has changed.
562
575
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
563
576
"""Do a three-way test on a scalar.
564
577
Return "this", "other" or "conflict", depending whether a value wins.
588
600
this_entry = get_entry(self.this_tree)
589
601
other_entry = get_entry(self.other_tree)
590
602
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:
603
entries = (base_entry, other_entry, this_entry)
606
for entry in entries:
611
names.append(entry.name)
612
parents.append(entry.parent_id)
613
return self._merge_names(file_id, parents, names)
615
def _merge_names(self, file_id, parents, names):
616
"""Perform a merge on file_id names and parents"""
617
base_name, other_name, this_name = names
618
base_parent, other_parent, this_parent = parents
620
name_winner = self._three_way(*names)
622
parent_id_winner = self._three_way(*parents)
623
if this_name is None:
597
624
if name_winner == "this":
598
625
name_winner = "other"
599
626
if parent_id_winner == "this":
603
630
if name_winner == "conflict":
604
631
trans_id = self.tt.trans_id_file_id(file_id)
605
632
self._raw_conflicts.append(('name conflict', trans_id,
606
self.name(this_entry, file_id),
607
self.name(other_entry, file_id)))
633
this_name, other_name))
608
634
if parent_id_winner == "conflict":
609
635
trans_id = self.tt.trans_id_file_id(file_id)
610
636
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:
637
this_parent, other_parent))
638
if other_name is None:
614
639
# it doesn't matter whether the result was 'other' or
615
640
# 'conflict'-- if there's no 'other', we leave it alone.
617
642
# 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
643
trans_id = self.tt.trans_id_file_id(file_id)
621
parent_id = winner_entry[parent_id_winner].parent_id
644
parent_id = parents[self.winner_idx[parent_id_winner]]
622
645
if parent_id is not None:
623
646
parent_trans_id = self.tt.trans_id_file_id(parent_id)
624
self.tt.adjust_path(winner_entry[name_winner].name,
647
self.tt.adjust_path(names[self.winner_idx[name_winner]],
625
648
parent_trans_id, trans_id)
627
650
def merge_contents(self, file_id):
788
811
def merge_executable(self, file_id, file_status):
789
812
"""Perform a merge on the execute bit."""
813
executable = [self.executable(t, file_id) for t in (self.base_tree,
814
self.other_tree, self.this_tree)]
815
self._merge_executable(file_id, executable, file_status)
817
def _merge_executable(self, file_id, executable, file_status):
818
"""Perform a merge on the execute bit."""
819
base_executable, other_executable, this_executable = executable
790
820
if file_status == "deleted":
792
822
trans_id = self.tt.trans_id_file_id(file_id)
808
836
if winner == "this":
809
837
if file_status == "modified":
810
executability = self.this_tree.is_executable(file_id)
838
executability = this_executable
811
839
if executability is not None:
812
840
trans_id = self.tt.trans_id_file_id(file_id)
813
841
self.tt.set_executability(executability, trans_id)
815
843
assert winner == "other"
816
844
if file_id in self.other_tree:
817
executability = self.other_tree.is_executable(file_id)
845
executability = other_executable
818
846
elif file_id in self.this_tree:
819
executability = self.this_tree.is_executable(file_id)
847
executability = this_executable
820
848
elif file_id in self.base_tree:
821
executability = self.base_tree.is_executable(file_id)
849
executability = base_executable
822
850
if executability is not None:
823
851
trans_id = self.tt.trans_id_file_id(file_id)
824
852
self.tt.set_executability(executability, trans_id)
891
919
def __init__(self, working_tree, this_tree, base_tree, other_tree,
892
920
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)
921
reprocess=False, change_reporter=None,
922
interesting_files=None):
896
923
super(WeaveMerger, self).__init__(working_tree, this_tree,
897
924
base_tree, other_tree,
898
925
interesting_ids=interesting_ids,
899
926
pb=pb, pp=pp, reprocess=reprocess,
900
927
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
929
def _merged_lines(self, file_id):
922
930
"""Generate the merged lines.
923
931
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)
934
plan = self.this_tree.plan_file_merge(file_id, self.other_tree)
935
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
936
'>>>>>>> MERGE-SOURCE\n')
937
return textmerge.merge_lines(self.reprocess)
934
939
def text_merge(self, file_id, trans_id):
935
940
"""Perform a (weave) text merge for a given file and file-id.
936
941
If conflicts are encountered, .THIS and .OTHER files will be emitted,
937
942
and a conflict will be noted.
939
self._check_file(file_id)
940
944
lines, conflicts = self._merged_lines(file_id)
941
945
lines = list(lines)
942
946
# Note we're checking whether the OUTPUT is binary in this case,
1039
1043
from bzrlib import option
1040
1044
return option._merge_type_registry
1047
def _plan_annotate_merge(annotated_a, annotated_b, ancestors_a, ancestors_b):
1048
def status_a(revision, text):
1049
if revision in ancestors_b:
1050
return 'killed-b', text
1052
return 'new-a', text
1054
def status_b(revision, text):
1055
if revision in ancestors_a:
1056
return 'killed-a', text
1058
return 'new-b', text
1060
plain_a = [t for (a, t) in annotated_a]
1061
plain_b = [t for (a, t) in annotated_b]
1062
matcher = patiencediff.PatienceSequenceMatcher(None, plain_a, plain_b)
1063
blocks = matcher.get_matching_blocks()
1066
for ai, bi, l in blocks:
1067
# process all mismatched sections
1068
# (last mismatched section is handled because blocks always
1069
# includes a 0-length last block)
1070
for revision, text in annotated_a[a_cur:ai]:
1071
yield status_a(revision, text)
1072
for revision, text in annotated_b[b_cur:bi]:
1073
yield status_b(revision, text)
1075
# and now the matched section
1078
for text_a, text_b in zip(plain_a[ai:a_cur], plain_b[bi:b_cur]):
1079
assert text_a == text_b
1080
yield "unchanged", text_a