15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from itertools import chain
23
18
from bzrlib import (
19
branch as _mod_branch,
20
conflicts as _mod_conflicts,
26
23
graph as _mod_graph,
30
29
revision as _mod_revision,
34
from bzrlib.branch import Branch
35
from bzrlib.conflicts import ConflictList, Conflict
36
from bzrlib.errors import (BzrCommandError,
46
WorkingTreeNotRevision,
49
from bzrlib.graph import Graph
50
from bzrlib.merge3 import Merge3
51
from bzrlib.osutils import rename, pathjoin
52
from progress import DummyProgress, ProgressPhase
53
from bzrlib.revision import (NULL_REVISION, ensure_null)
54
from bzrlib.textfile import check_text_lines
55
from bzrlib.trace import mutter, warning, note, is_quiet
56
from bzrlib.transform import (TransformPreview, TreeTransform,
57
resolve_conflicts, cook_conflicts,
58
conflict_pass, FinalPaths, create_from_tree,
59
unique_add, ROOT_PARENT)
60
from bzrlib.versionedfile import PlanWeaveMerge
38
from bzrlib.symbol_versioning import (
63
42
# TODO: Report back as changes are merged in
66
45
def transform_tree(from_tree, to_tree, interesting_ids=None):
67
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
68
interesting_ids=interesting_ids, this_tree=from_tree)
46
from_tree.lock_tree_write()
48
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
49
interesting_ids=interesting_ids, this_tree=from_tree)
71
54
class Merger(object):
102
85
self._is_criss_cross = None
103
86
self._lca_trees = None
88
def cache_trees_with_revision_ids(self, trees):
89
"""Cache any tree in trees if it has a revision_id."""
90
for maybe_tree in trees:
91
if maybe_tree is None:
94
rev_id = maybe_tree.get_revision_id()
95
except AttributeError:
97
self._cached_trees[rev_id] = maybe_tree
106
100
def revision_graph(self):
107
101
if self._revision_graph is None:
169
163
base_revision_id, tree.branch.last_revision())):
170
164
base_revision_id = None
172
warning('Performing cherrypick')
166
trace.warning('Performing cherrypick')
173
167
merger = klass.from_revision_ids(pb, tree, other_revision_id,
174
168
base_revision_id, revision_graph=
227
221
if revno is None:
228
222
tree = workingtree.WorkingTree.open_containing(location)[0]
229
223
return tree.branch, tree
230
branch = Branch.open_containing(location, possible_transports)[0]
224
branch = _mod_branch.Branch.open_containing(
225
location, possible_transports)[0]
232
227
revision_id = branch.last_revision()
234
229
revision_id = branch.get_rev_id(revno)
235
revision_id = ensure_null(revision_id)
230
revision_id = _mod_revision.ensure_null(revision_id)
236
231
return branch, self.revision_tree(revision_id, branch)
233
@deprecated_method(deprecated_in((2, 1, 0)))
238
234
def ensure_revision_trees(self):
239
235
if self.this_revision_tree is None:
240
236
self.this_basis_tree = self.revision_tree(self.this_basis)
244
240
if self.other_rev_id is None:
245
241
other_basis_tree = self.revision_tree(self.other_basis)
246
changes = other_basis_tree.changes_from(self.other_tree)
247
if changes.has_changed():
242
if other_basis_tree.has_changes(self.other_tree):
248
243
raise WorkingTreeNotRevision(self.this_tree)
249
244
other_rev_id = self.other_basis
250
245
self.other_tree = other_basis_tree
247
@deprecated_method(deprecated_in((2, 1, 0)))
252
248
def file_revisions(self, file_id):
253
249
self.ensure_revision_trees()
254
250
def get_id(tree, file_id):
257
253
if self.this_rev_id is None:
258
254
if self.this_basis_tree.get_file_sha1(file_id) != \
259
255
self.this_tree.get_file_sha1(file_id):
260
raise WorkingTreeNotRevision(self.this_tree)
256
raise errors.WorkingTreeNotRevision(self.this_tree)
262
258
trees = (self.this_basis_tree, self.other_tree)
263
259
return [get_id(tree, file_id) for tree in trees]
261
@deprecated_method(deprecated_in((2, 1, 0)))
265
262
def check_basis(self, check_clean, require_commits=True):
266
263
if self.this_basis is None and require_commits is True:
267
raise BzrCommandError("This branch has no commits."
268
" (perhaps you would prefer 'bzr pull')")
264
raise errors.BzrCommandError(
265
"This branch has no commits."
266
" (perhaps you would prefer 'bzr pull')")
270
268
self.compare_basis()
271
269
if self.this_basis != self.this_rev_id:
272
270
raise errors.UncommittedChanges(self.this_tree)
272
@deprecated_method(deprecated_in((2, 1, 0)))
274
273
def compare_basis(self):
276
275
basis_tree = self.revision_tree(self.this_tree.last_revision())
277
276
except errors.NoSuchRevision:
278
277
basis_tree = self.this_tree.basis_tree()
279
changes = self.this_tree.changes_from(basis_tree)
280
if not changes.has_changed():
278
if not self.this_tree.has_changes(basis_tree):
281
279
self.this_rev_id = self.this_basis
283
281
def set_interesting_files(self, file_list):
284
282
self.interesting_files = file_list
286
284
def set_pending(self):
287
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
285
if (not self.base_is_ancestor or not self.base_is_other_ancestor
286
or self.other_rev_id is None):
289
288
self._add_parent()
320
319
self.other_rev_id = _mod_revision.ensure_null(
321
320
self.other_branch.last_revision())
322
321
if _mod_revision.is_null(self.other_rev_id):
323
raise NoCommits(self.other_branch)
322
raise errors.NoCommits(self.other_branch)
324
323
self.other_basis = self.other_rev_id
325
324
elif other_revision[1] is not None:
326
325
self.other_rev_id = self.other_branch.get_rev_id(other_revision[1])
329
328
self.other_rev_id = None
330
329
self.other_basis = self.other_branch.last_revision()
331
330
if self.other_basis is None:
332
raise NoCommits(self.other_branch)
331
raise errors.NoCommits(self.other_branch)
333
332
if self.other_rev_id is not None:
334
333
self._cached_trees[self.other_rev_id] = self.other_tree
335
334
self._maybe_fetch(self.other_branch,self.this_branch, self.other_basis)
362
361
target.fetch(source, revision_id)
364
363
def find_base(self):
365
revisions = [ensure_null(self.this_basis),
366
ensure_null(self.other_basis)]
367
if NULL_REVISION in revisions:
368
self.base_rev_id = NULL_REVISION
364
revisions = [_mod_revision.ensure_null(self.this_basis),
365
_mod_revision.ensure_null(self.other_basis)]
366
if _mod_revision.NULL_REVISION in revisions:
367
self.base_rev_id = _mod_revision.NULL_REVISION
369
368
self.base_tree = self.revision_tree(self.base_rev_id)
370
369
self._is_criss_cross = False
372
371
lcas = self.revision_graph.find_lca(revisions[0], revisions[1])
373
372
self._is_criss_cross = False
374
373
if len(lcas) == 0:
375
self.base_rev_id = NULL_REVISION
374
self.base_rev_id = _mod_revision.NULL_REVISION
376
375
elif len(lcas) == 1:
377
376
self.base_rev_id = list(lcas)[0]
378
377
else: # len(lcas) > 1
387
386
self.base_rev_id = self.revision_graph.find_unique_lca(
389
388
self._is_criss_cross = True
390
if self.base_rev_id == NULL_REVISION:
391
raise UnrelatedBranches()
389
if self.base_rev_id == _mod_revision.NULL_REVISION:
390
raise errors.UnrelatedBranches()
392
391
if self._is_criss_cross:
393
warning('Warning: criss-cross merge encountered. See bzr'
394
' help criss-cross.')
395
mutter('Criss-cross lcas: %r' % lcas)
392
trace.warning('Warning: criss-cross merge encountered. See bzr'
393
' help criss-cross.')
394
trace.mutter('Criss-cross lcas: %r' % lcas)
396
395
interesting_revision_ids = [self.base_rev_id]
397
396
interesting_revision_ids.extend(lcas)
398
397
interesting_trees = dict((t.get_revision_id(), t)
408
407
self.base_tree = self.revision_tree(self.base_rev_id)
409
408
self.base_is_ancestor = True
410
409
self.base_is_other_ancestor = True
411
mutter('Base revid: %r' % self.base_rev_id)
410
trace.mutter('Base revid: %r' % self.base_rev_id)
413
412
def set_base(self, base_revision):
414
413
"""Set the base revision to use for the merge.
416
415
:param base_revision: A 2-list containing a path and revision number.
418
mutter("doing merge() with no base_revision specified")
417
trace.mutter("doing merge() with no base_revision specified")
419
418
if base_revision == [None, None]:
441
440
if self.merge_type.supports_reprocess:
442
441
kwargs['reprocess'] = self.reprocess
443
442
elif self.reprocess:
444
raise BzrError("Conflict reduction is not supported for merge"
445
" type %s." % self.merge_type)
443
raise errors.BzrError(
444
"Conflict reduction is not supported for merge"
445
" type %s." % self.merge_type)
446
446
if self.merge_type.supports_show_base:
447
447
kwargs['show_base'] = self.show_base
448
448
elif self.show_base:
449
raise BzrError("Showing base is not supported for this"
450
" merge type. %s" % self.merge_type)
449
raise errors.BzrError("Showing base is not supported for this"
450
" merge type. %s" % self.merge_type)
451
451
if (not getattr(self.merge_type, 'supports_reverse_cherrypick', True)
452
452
and not self.base_is_other_ancestor):
453
453
raise errors.CannotReverseCherrypick()
503
503
self.this_tree.unlock()
504
504
if len(merge.cooked_conflicts) == 0:
505
if not self.ignore_zero and not is_quiet():
506
note("All changes applied successfully.")
505
if not self.ignore_zero and not trace.is_quiet():
506
trace.note("All changes applied successfully.")
508
note("%d conflicts encountered." % len(merge.cooked_conflicts))
508
trace.note("%d conflicts encountered."
509
% len(merge.cooked_conflicts))
510
511
return len(merge.cooked_conflicts)
541
542
def __init__(self, working_tree, this_tree, base_tree, other_tree,
542
543
interesting_ids=None, reprocess=False, show_base=False,
543
pb=DummyProgress(), pp=None, change_reporter=None,
544
pb=progress.DummyProgress(), pp=None, change_reporter=None,
544
545
interesting_files=None, do_merge=True,
545
546
cherrypick=False, lca_trees=None):
546
547
"""Initialize the merger object and perform the merge.
592
593
self.change_reporter = change_reporter
593
594
self.cherrypick = cherrypick
594
595
if self.pp is None:
595
self.pp = ProgressPhase("Merge phase", 3, self.pb)
596
self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
600
601
self.this_tree.lock_tree_write()
601
602
self.base_tree.lock_read()
602
603
self.other_tree.lock_read()
603
self.tt = TreeTransform(self.this_tree, self.pb)
606
self._compute_transform()
608
results = self.tt.apply(no_conflicts=True)
609
self.write_modified(results)
605
self.tt = transform.TreeTransform(self.this_tree, self.pb)
611
self.this_tree.add_conflicts(self.cooked_conflicts)
612
except UnsupportedOperation:
608
self._compute_transform()
610
results = self.tt.apply(no_conflicts=True)
611
self.write_modified(results)
613
self.this_tree.add_conflicts(self.cooked_conflicts)
614
except errors.UnsupportedOperation:
616
619
self.other_tree.unlock()
617
620
self.base_tree.unlock()
618
621
self.this_tree.unlock()
621
624
def make_preview_transform(self):
622
625
self.base_tree.lock_read()
623
626
self.other_tree.lock_read()
624
self.tt = TransformPreview(self.this_tree)
627
self.tt = transform.TransformPreview(self.this_tree)
626
629
self.pp.next_phase()
627
630
self._compute_transform()
657
660
self.pp.next_phase()
658
661
child_pb = ui.ui_factory.nested_progress_bar()
660
fs_conflicts = resolve_conflicts(self.tt, child_pb,
661
lambda t, c: conflict_pass(t, c, self.other_tree))
663
fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
664
lambda t, c: transform.conflict_pass(t, c, self.other_tree))
663
666
child_pb.finished()
664
667
if self.change_reporter is not None:
875
878
def fix_root(self):
877
880
self.tt.final_kind(self.tt.root)
881
except errors.NoSuchFile:
879
882
self.tt.cancel_deletion(self.tt.root)
880
883
if self.tt.final_file_id(self.tt.root) is None:
881
884
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
1147
1150
self.tt.delete_contents(trans_id)
1148
1151
if file_id in self.other_tree:
1149
1152
# OTHER changed the file
1150
create_from_tree(self.tt, trans_id,
1151
self.other_tree, file_id)
1154
if wt.supports_content_filtering():
1155
# We get the path from the working tree if it exists.
1156
# That fails though when OTHER is adding a file, so
1157
# we fall back to the other tree to find the path if
1158
# it doesn't exist locally.
1160
filter_tree_path = wt.id2path(file_id)
1161
except errors.NoSuchId:
1162
filter_tree_path = self.other_tree.id2path(file_id)
1164
# Skip the id2path lookup for older formats
1165
filter_tree_path = None
1166
transform.create_from_tree(self.tt, trans_id,
1167
self.other_tree, file_id,
1168
filter_tree_path=filter_tree_path)
1152
1169
if not file_in_this:
1153
1170
self.tt.version_file(file_id, trans_id)
1154
1171
return "modified"
1165
1182
# have agreement that output should be a file.
1167
1184
self.text_merge(file_id, trans_id)
1185
except errors.BinaryFile:
1169
1186
return contents_conflict()
1170
1187
if file_id not in self.this_tree:
1171
1188
self.tt.version_file(file_id, trans_id)
1173
1190
self.tt.tree_kind(trans_id)
1174
1191
self.tt.delete_contents(trans_id)
1192
except errors.NoSuchFile:
1177
1194
return "modified"
1196
1213
base_lines = []
1197
1214
other_lines = self.get_lines(self.other_tree, file_id)
1198
1215
this_lines = self.get_lines(self.this_tree, file_id)
1199
m3 = Merge3(base_lines, this_lines, other_lines,
1200
is_cherrypick=self.cherrypick)
1216
m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1217
is_cherrypick=self.cherrypick)
1201
1218
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1202
1219
if self.show_base is True:
1203
1220
base_marker = '|' * 7
1241
1258
('THIS', self.this_tree, this_lines)]
1242
1259
if not no_base:
1243
1260
data.append(('BASE', self.base_tree, base_lines))
1262
# We need to use the actual path in the working tree of the file here,
1263
# ignoring the conflict suffixes
1265
if wt.supports_content_filtering():
1267
filter_tree_path = wt.id2path(file_id)
1268
except errors.NoSuchId:
1269
# file has been deleted
1270
filter_tree_path = None
1272
# Skip the id2path lookup for older formats
1273
filter_tree_path = None
1244
1275
versioned = False
1245
1276
file_group = []
1246
1277
for suffix, tree, lines in data:
1247
1278
if file_id in tree:
1248
1279
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1280
suffix, lines, filter_tree_path)
1250
1281
file_group.append(trans_id)
1251
1282
if set_version and not versioned:
1252
1283
self.tt.version_file(file_id, trans_id)
1254
1285
return file_group
1256
1287
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1288
lines=None, filter_tree_path=None):
1258
1289
"""Emit a single conflict file."""
1259
1290
name = name + '.' + suffix
1260
1291
trans_id = self.tt.create_path(name, parent_id)
1261
create_from_tree(self.tt, trans_id, tree, file_id, lines)
1292
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1262
1294
return trans_id
1264
1296
def merge_executable(self, file_id, file_status):
1306
1338
def cook_conflicts(self, fs_conflicts):
1307
1339
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1308
from conflicts import Conflict
1309
1340
name_conflicts = {}
1310
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
1311
fp = FinalPaths(self.tt)
1341
self.cooked_conflicts.extend(transform.cook_conflicts(
1342
fs_conflicts, self.tt))
1343
fp = transform.FinalPaths(self.tt)
1312
1344
for conflict in self._raw_conflicts:
1313
1345
conflict_type = conflict[0]
1314
1346
if conflict_type in ('name conflict', 'parent conflict'):
1316
1348
conflict_args = conflict[2:]
1317
1349
if trans_id not in name_conflicts:
1318
1350
name_conflicts[trans_id] = {}
1319
unique_add(name_conflicts[trans_id], conflict_type,
1351
transform.unique_add(name_conflicts[trans_id], conflict_type,
1321
1353
if conflict_type == 'contents conflict':
1322
1354
for trans_id in conflict[1]:
1323
1355
file_id = self.tt.final_file_id(trans_id)
1328
1360
if path.endswith(suffix):
1329
1361
path = path[:-len(suffix)]
1331
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1363
c = _mod_conflicts.Conflict.factory(conflict_type,
1364
path=path, file_id=file_id)
1332
1365
self.cooked_conflicts.append(c)
1333
1366
if conflict_type == 'text conflict':
1334
1367
trans_id = conflict[1]
1335
1368
path = fp.get_path(trans_id)
1336
1369
file_id = self.tt.final_file_id(trans_id)
1337
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1370
c = _mod_conflicts.Conflict.factory(conflict_type,
1371
path=path, file_id=file_id)
1338
1372
self.cooked_conflicts.append(c)
1340
1374
for trans_id, conflicts in name_conflicts.iteritems():
1355
1389
if this_parent is not None and this_name is not None:
1356
1390
this_parent_path = \
1357
1391
fp.get_path(self.tt.trans_id_file_id(this_parent))
1358
this_path = pathjoin(this_parent_path, this_name)
1392
this_path = osutils.pathjoin(this_parent_path, this_name)
1360
1394
this_path = "<deleted>"
1361
1395
file_id = self.tt.final_file_id(trans_id)
1362
c = Conflict.factory('path conflict', path=this_path,
1363
conflict_path=other_path, file_id=file_id)
1396
c = _mod_conflicts.Conflict.factory('path conflict', path=this_path,
1397
conflict_path=other_path,
1364
1399
self.cooked_conflicts.append(c)
1365
self.cooked_conflicts.sort(key=Conflict.sort_key)
1400
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1368
1403
class WeaveMerger(Merge3Merger):
1389
1424
name = self.tt.final_name(trans_id) + '.plan'
1390
1425
contents = ('%10s|%s' % l for l in plan)
1391
1426
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1392
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1393
'>>>>>>> MERGE-SOURCE\n')
1427
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1428
'>>>>>>> MERGE-SOURCE\n')
1394
1429
return textmerge.merge_lines(self.reprocess)
1396
1431
def text_merge(self, file_id, trans_id):
1402
1437
lines = list(lines)
1403
1438
# Note we're checking whether the OUTPUT is binary in this case,
1404
1439
# because we don't want to get into weave merge guts.
1405
check_text_lines(lines)
1440
textfile.check_text_lines(lines)
1406
1441
self.tt.create_file(lines, trans_id)
1408
1443
self._raw_conflicts.append(('text conflict', trans_id))
1432
1467
name = self.tt.final_name(trans_id) + '.plan'
1433
1468
contents = ('%10s|%s' % l for l in plan)
1434
1469
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1435
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1436
'>>>>>>> MERGE-SOURCE\n')
1470
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1471
'>>>>>>> MERGE-SOURCE\n')
1437
1472
return textmerge.merge_lines(self.reprocess)
1441
1476
"""Three-way merger using external diff3 for text merging"""
1443
1478
def dump_file(self, temp_dir, name, tree, file_id):
1444
out_path = pathjoin(temp_dir, name)
1479
out_path = osutils.pathjoin(temp_dir, name)
1445
1480
out_file = open(out_path, "wb")
1447
1482
in_file = tree.get_file(file_id)
1459
1494
import bzrlib.patch
1460
1495
temp_dir = osutils.mkdtemp(prefix="bzr-")
1462
new_file = pathjoin(temp_dir, "new")
1497
new_file = osutils.pathjoin(temp_dir, "new")
1463
1498
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1464
1499
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1465
1500
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1466
1501
status = bzrlib.patch.diff3(new_file, this, base, other)
1467
1502
if status not in (0, 1):
1468
raise BzrError("Unhandled diff3 exit code")
1503
raise errors.BzrError("Unhandled diff3 exit code")
1469
1504
f = open(new_file, 'rb')
1471
1506
self.tt.create_file(f, trans_id)
1498
1533
branch.get_revision_tree(base_revision))'
1500
1535
if this_tree is None:
1501
raise BzrError("bzrlib.merge.merge_inner requires a this_tree "
1502
"parameter as of bzrlib version 0.8.")
1536
raise errors.BzrError("bzrlib.merge.merge_inner requires a this_tree "
1537
"parameter as of bzrlib version 0.8.")
1503
1538
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
1504
1539
pb=pb, change_reporter=change_reporter)
1505
1540
merger.backup_files = backup_files
1518
1553
get_revision_id = getattr(base_tree, 'get_revision_id', None)
1519
1554
if get_revision_id is None:
1520
1555
get_revision_id = base_tree.last_revision
1556
merger.cache_trees_with_revision_ids([other_tree, base_tree, this_tree])
1521
1557
merger.set_base_revision(get_revision_id(), this_branch)
1522
1558
return merger.do_merge()
1722
1758
super(_PlanMerge, self).__init__(a_rev, b_rev, vf, key_prefix)
1723
1759
self.a_key = self._key_prefix + (self.a_rev,)
1724
1760
self.b_key = self._key_prefix + (self.b_rev,)
1725
self.graph = Graph(self.vf)
1761
self.graph = _mod_graph.Graph(self.vf)
1726
1762
heads = self.graph.heads((self.a_key, self.b_key))
1727
1763
if len(heads) == 1:
1728
1764
# one side dominates, so we can just return its values, yay for
1736
mutter('found dominating revision for %s\n%s > %s', self.vf,
1737
self._head_key[-1], other)
1772
trace.mutter('found dominating revision for %s\n%s > %s', self.vf,
1773
self._head_key[-1], other)
1738
1774
self._weave = None
1740
1776
self._head_key = None
1755
1791
next_lcas = self.graph.find_lca(*cur_ancestors)
1756
1792
# Map a plain NULL_REVISION to a simple no-ancestors
1757
if next_lcas == set([NULL_REVISION]):
1793
if next_lcas == set([_mod_revision.NULL_REVISION]):
1759
1795
# Order the lca's based on when they were merged into the tip
1760
1796
# While the actual merge portion of weave merge uses a set() of
1772
1808
elif len(next_lcas) > 2:
1773
1809
# More than 2 lca's, fall back to grabbing all nodes between
1774
1810
# this and the unique lca.
1775
mutter('More than 2 LCAs, falling back to all nodes for:'
1776
' %s, %s\n=> %s', self.a_key, self.b_key, cur_ancestors)
1811
trace.mutter('More than 2 LCAs, falling back to all nodes for:'
1813
self.a_key, self.b_key, cur_ancestors)
1777
1814
cur_lcas = next_lcas
1778
1815
while len(cur_lcas) > 1:
1779
1816
cur_lcas = self.graph.find_lca(*cur_lcas)
1782
1819
unique_lca = None
1784
1821
unique_lca = list(cur_lcas)[0]
1785
if unique_lca == NULL_REVISION:
1822
if unique_lca == _mod_revision.NULL_REVISION:
1786
1823
# find_lca will return a plain 'NULL_REVISION' rather
1787
1824
# than a key tuple when there is no common ancestor, we
1788
1825
# prefer to just use None, because it doesn't confuse
1811
1848
# We remove NULL_REVISION because it isn't a proper tuple key, and
1812
1849
# thus confuses things like _get_interesting_texts, and our logic
1813
1850
# to add the texts into the memory weave.
1814
if NULL_REVISION in parent_map:
1815
parent_map.pop(NULL_REVISION)
1851
if _mod_revision.NULL_REVISION in parent_map:
1852
parent_map.pop(_mod_revision.NULL_REVISION)
1817
1854
interesting = set()
1818
1855
for tip in tip_keys: