48
45
from bzrlib.textfile import check_text_lines
49
46
from bzrlib.trace import mutter, warning, note
50
47
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
51
conflict_pass, FinalPaths, create_by_entry,
52
unique_add, ROOT_PARENT)
53
from bzrlib.versionedfile import PlanWeaveMerge
48
FinalPaths, create_by_entry, unique_add,
50
from bzrlib.versionedfile import WeaveMerge
54
51
from bzrlib import ui
56
53
# 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)
59
94
def transform_tree(from_tree, to_tree, interesting_ids=None):
60
95
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
80
114
self.ignore_zero = False
81
115
self.backup_files = False
82
116
self.interesting_ids = None
83
self.interesting_files = None
84
117
self.show_base = False
85
118
self.reprocess = False
88
121
self.recurse = recurse
89
122
self.change_reporter = change_reporter
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)
124
def revision_tree(self, revision_id):
125
return self.this_branch.repository.revision_tree(revision_id)
117
127
def ensure_revision_trees(self):
118
128
if self.this_revision_tree is None:
119
self.this_basis_tree = self.revision_tree(self.this_basis)
129
self.this_basis_tree = self.this_branch.repository.revision_tree(
120
131
if self.this_basis == self.this_rev_id:
121
132
self.this_revision_tree = self.this_basis_tree
152
163
raise BzrCommandError("Working tree has uncommitted changes.")
154
165
def compare_basis(self):
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)
166
changes = self.this_tree.changes_from(self.this_tree.basis_tree())
160
167
if not changes.has_changed():
161
168
self.this_rev_id = self.this_basis
163
170
def set_interesting_files(self, file_list):
164
self.interesting_files = 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
166
199
def set_pending(self):
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):
200
if not self.base_is_ancestor:
202
if self.other_rev_id is None:
204
ancestry = set(self.this_branch.repository.get_ancestry(
205
self.this_basis, topo_sorted=False))
206
if self.other_rev_id in ancestry:
208
self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
210
def set_other(self, other_revision):
191
211
"""Set the revision and tree to merge from.
193
213
This sets the other_tree, other_rev_id, other_basis attributes.
195
215
:param other_revision: The [path, revision] list to merge from.
197
self.other_branch, self.other_tree = self._get_tree(other_revision,
217
self.other_branch, self.other_tree = _get_tree(other_revision,
199
219
if other_revision[1] == -1:
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):
220
self.other_rev_id = self.other_branch.last_revision()
221
if self.other_rev_id is None:
203
222
raise NoCommits(self.other_branch)
204
223
self.other_basis = self.other_rev_id
205
224
elif other_revision[1] is not None:
210
229
self.other_basis = self.other_branch.last_revision()
211
230
if self.other_basis is None:
212
231
raise NoCommits(self.other_branch)
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)
232
if self.other_branch.base != self.this_branch.base:
233
self.this_branch.fetch(self.other_branch,
234
last_revision=self.other_basis)
217
236
def set_other_revision(self, revision_id, other_branch):
218
237
"""Set 'other' based on a branch and revision id
223
242
self.other_rev_id = revision_id
224
243
self.other_branch = other_branch
225
self._maybe_fetch(other_branch, self.this_branch, self.other_rev_id)
244
self.this_branch.fetch(other_branch, self.other_rev_id)
226
245
self.other_tree = self.revision_tree(revision_id)
227
246
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)
251
248
def find_base(self):
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
249
self.set_base([None, None])
266
251
def set_base(self, base_revision):
267
252
"""Set the base revision to use for the merge.
271
256
mutter("doing merge() with no base_revision specified")
272
257
if base_revision == [None, None]:
259
pb = ui.ui_factory.nested_progress_bar()
261
this_repo = self.this_branch.repository
262
graph = this_repo.get_graph()
263
revisions = [ensure_null(self.this_basis),
264
ensure_null(self.other_basis)]
265
if NULL_REVISION in revisions:
266
self.base_rev_id = NULL_REVISION
268
self.base_rev_id = graph.find_unique_lca(*revisions)
269
if self.base_rev_id == NULL_REVISION:
270
raise UnrelatedBranches()
273
except NoCommonAncestor:
274
raise UnrelatedBranches()
275
self.base_tree = _get_revid_tree_from_tree(self.this_tree,
278
self.base_is_ancestor = True
275
base_branch, self.base_tree = self._get_tree(base_revision)
280
base_branch, self.base_tree = _get_tree(base_revision)
276
281
if base_revision[1] == -1:
277
282
self.base_rev_id = base_branch.last_revision()
278
283
elif base_revision[1] is None:
279
self.base_rev_id = _mod_revision.NULL_REVISION
284
self.base_rev_id = None
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)
286
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
287
if self.this_branch.base != base_branch.base:
288
self.this_branch.fetch(base_branch)
284
289
self.base_is_ancestor = is_ancestor(self.this_basis,
285
290
self.base_rev_id,
286
291
self.this_branch)
287
self.base_is_other_ancestor = is_ancestor(self.other_basis,
291
293
def do_merge(self):
292
294
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
293
295
'other_tree': self.other_tree,
294
296
'interesting_ids': self.interesting_ids,
295
'interesting_files': self.interesting_files,
297
298
if self.merge_type.requires_base:
298
299
kwargs['base_tree'] = self.base_tree
347
348
return len(merge.cooked_conflicts)
350
def regen_inventory(self, new_entries):
351
old_entries = self.this_tree.read_working_inventory()
355
for path, file_id in new_entries:
358
new_entries_map[file_id] = path
360
def id2path(file_id):
361
path = new_entries_map.get(file_id)
364
entry = old_entries[file_id]
365
if entry.parent_id is None:
367
return pathjoin(id2path(entry.parent_id), entry.name)
369
for file_id in old_entries:
370
entry = old_entries[file_id]
371
path = id2path(file_id)
372
if file_id in self.base_tree.inventory:
373
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
375
executable = getattr(entry, 'executable', False)
376
new_inventory[file_id] = (path, file_id, entry.parent_id,
377
entry.kind, executable)
379
by_path[path] = file_id
384
for path, file_id in new_entries:
386
del new_inventory[file_id]
389
new_path_list.append((path, file_id))
390
if file_id not in old_entries:
392
# Ensure no file is added before its parent
394
for path, file_id in new_path_list:
398
parent = by_path[os.path.dirname(path)]
399
abspath = pathjoin(self.this_tree.basedir, path)
400
kind = osutils.file_kind(abspath)
401
if file_id in self.base_tree.inventory:
402
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
405
new_inventory[file_id] = (path, file_id, parent, kind, executable)
406
by_path[path] = file_id
408
# Get a list in insertion order
409
new_inventory_list = new_inventory.values()
410
mutter ("""Inventory regeneration:
411
old length: %i insertions: %i deletions: %i new_length: %i"""\
412
% (len(old_entries), insertions, deletions,
413
len(new_inventory_list)))
414
assert len(new_inventory_list) == len(old_entries) + insertions\
416
new_inventory_list.sort()
417
return new_inventory_list
350
420
class Merge3Merger(object):
351
421
"""Three-way merger that uses the merge3 text merger"""
353
423
supports_reprocess = True
354
424
supports_show_base = True
355
425
history_based = False
356
winner_idx = {"this": 2, "other": 1, "conflict": 1}
358
427
def __init__(self, working_tree, this_tree, base_tree, other_tree,
359
428
interesting_ids=None, reprocess=False, show_base=False,
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
429
pb=DummyProgress(), pp=None, change_reporter=None):
430
"""Initialize the merger object and perform the merge."""
384
431
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
389
432
self.this_tree = working_tree
390
433
self.this_tree.lock_tree_write()
391
434
self.base_tree = base_tree
402
445
if self.pp is None:
403
446
self.pp = ProgressPhase("Merge phase", 3, self.pb)
448
if interesting_ids is not None:
449
all_ids = interesting_ids
451
all_ids = set(base_tree)
452
all_ids.update(other_tree)
405
453
self.tt = TreeTransform(working_tree, self.pb)
407
455
self.pp.next_phase()
408
entries = self._entries3()
409
456
child_pb = ui.ui_factory.nested_progress_bar()
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)
458
for num, file_id in enumerate(all_ids):
459
child_pb.update('Preparing file merge', num, len(all_ids))
460
self.merge_names(file_id)
461
file_status = self.merge_contents(file_id)
462
self.merge_executable(file_id, file_status)
422
464
child_pb.finished()
424
466
self.pp.next_phase()
425
467
child_pb = ui.ui_factory.nested_progress_bar()
427
fs_conflicts = resolve_conflicts(self.tt, child_pb,
428
lambda t, c: conflict_pass(t, c, self.other_tree))
469
fs_conflicts = resolve_conflicts(self.tt, child_pb)
430
471
child_pb.finished()
431
472
if change_reporter is not None:
448
489
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))
484
492
def fix_root(self):
486
494
self.tt.final_kind(self.tt.root)
558
566
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.
575
569
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
576
570
"""Do a three-way test on a scalar.
577
571
Return "this", "other" or "conflict", depending whether a value wins.
600
595
this_entry = get_entry(self.this_tree)
601
596
other_entry = get_entry(self.other_tree)
602
597
base_entry = get_entry(self.base_tree)
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:
598
name_winner = self.scalar_three_way(this_entry, base_entry,
599
other_entry, file_id, self.name)
600
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
601
other_entry, file_id,
603
if this_entry is None:
624
604
if name_winner == "this":
625
605
name_winner = "other"
626
606
if parent_id_winner == "this":
630
610
if name_winner == "conflict":
631
611
trans_id = self.tt.trans_id_file_id(file_id)
632
612
self._raw_conflicts.append(('name conflict', trans_id,
633
this_name, other_name))
613
self.name(this_entry, file_id),
614
self.name(other_entry, file_id)))
634
615
if parent_id_winner == "conflict":
635
616
trans_id = self.tt.trans_id_file_id(file_id)
636
617
self._raw_conflicts.append(('parent conflict', trans_id,
637
this_parent, other_parent))
638
if other_name is None:
618
self.parent(this_entry, file_id),
619
self.parent(other_entry, file_id)))
620
if other_entry is None:
639
621
# it doesn't matter whether the result was 'other' or
640
622
# 'conflict'-- if there's no 'other', we leave it alone.
642
624
# if we get here, name_winner and parent_winner are set to safe values.
625
winner_entry = {"this": this_entry, "other": other_entry,
626
"conflict": other_entry}
643
627
trans_id = self.tt.trans_id_file_id(file_id)
644
parent_id = parents[self.winner_idx[parent_id_winner]]
628
parent_id = winner_entry[parent_id_winner].parent_id
645
629
if parent_id is not None:
646
630
parent_trans_id = self.tt.trans_id_file_id(parent_id)
647
self.tt.adjust_path(names[self.winner_idx[name_winner]],
631
self.tt.adjust_path(winner_entry[name_winner].name,
648
632
parent_trans_id, trans_id)
650
634
def merge_contents(self, file_id):
811
795
def merge_executable(self, file_id, file_status):
812
796
"""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
820
797
if file_status == "deleted":
822
799
trans_id = self.tt.trans_id_file_id(file_id)
836
815
if winner == "this":
837
816
if file_status == "modified":
838
executability = this_executable
817
executability = self.this_tree.is_executable(file_id)
839
818
if executability is not None:
840
819
trans_id = self.tt.trans_id_file_id(file_id)
841
820
self.tt.set_executability(executability, trans_id)
843
822
assert winner == "other"
844
823
if file_id in self.other_tree:
845
executability = other_executable
824
executability = self.other_tree.is_executable(file_id)
846
825
elif file_id in self.this_tree:
847
executability = this_executable
826
executability = self.this_tree.is_executable(file_id)
848
827
elif file_id in self.base_tree:
849
executability = base_executable
828
executability = self.base_tree.is_executable(file_id)
850
829
if executability is not None:
851
830
trans_id = self.tt.trans_id_file_id(file_id)
852
831
self.tt.set_executability(executability, trans_id)
919
898
def __init__(self, working_tree, this_tree, base_tree, other_tree,
920
899
interesting_ids=None, pb=DummyProgress(), pp=None,
921
reprocess=False, change_reporter=None,
922
interesting_files=None):
900
reprocess=False, change_reporter=None):
901
self.this_revision_tree = self._get_revision_tree(this_tree)
902
self.other_revision_tree = self._get_revision_tree(other_tree)
923
903
super(WeaveMerger, self).__init__(working_tree, this_tree,
924
904
base_tree, other_tree,
925
905
interesting_ids=interesting_ids,
926
906
pb=pb, pp=pp, reprocess=reprocess,
927
907
change_reporter=change_reporter)
909
def _get_revision_tree(self, tree):
910
"""Return a revision tree related to this tree.
911
If the tree is a WorkingTree, the basis will be returned.
913
if getattr(tree, 'get_weave', False) is False:
914
# If we have a WorkingTree, try using the basis
915
return tree.branch.basis_tree()
919
def _check_file(self, file_id):
920
"""Check that the revision tree's version of the file matches."""
921
for tree, rt in ((self.this_tree, self.this_revision_tree),
922
(self.other_tree, self.other_revision_tree)):
925
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
926
raise WorkingTreeNotRevision(self.this_tree)
929
928
def _merged_lines(self, file_id):
930
929
"""Generate the merged lines.
931
930
There is no distinction between lines that are meant to contain <<<<<<<
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)
933
weave = self.this_revision_tree.get_weave(file_id)
934
this_revision_id = self.this_revision_tree.inventory[file_id].revision
935
other_revision_id = \
936
self.other_revision_tree.inventory[file_id].revision
937
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
938
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
939
return wm.merge_lines(self.reprocess)
939
941
def text_merge(self, file_id, trans_id):
940
942
"""Perform a (weave) text merge for a given file and file-id.
941
943
If conflicts are encountered, .THIS and .OTHER files will be emitted,
942
944
and a conflict will be noted.
946
self._check_file(file_id)
944
947
lines, conflicts = self._merged_lines(file_id)
945
948
lines = list(lines)
946
949
# Note we're checking whether the OUTPUT is binary in this case,
1043
1046
from bzrlib import option
1044
1047
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