[RFC] Changing annotate to show merge points
Henri Wiechers
hwiechers at gmail.com
Sun Nov 19 19:19:52 GMT 2006
The attached patch changes annotate to display the revision in which a
line was merged instead of just saying 'merge'. I'm suspect there's a
better way of doing this, so I'm asking for feedback rather than
submitting it for merging.
It works by finding the oldest decedent of the line's revision that is
a member of the branch. Because Repository.get_revision_graph()
returns a dict that maps a revision to its parents, it does a breadth
first search from the branch until it finds the revision.
Searching from the branch seems really inefficient because you have to
run down the entire branch, but to go the other way round you need a
revision graph that maps a revision to its children. I found that
Repository.get_revision_graph_with_ghosts() returns a Graph that can
give that mapping. The problem is that it seems very slow. I coded an
alternate version using that function instead, but it took three times
longer to run on builtins.py. I'd really like to hear someone else's
view on this problem.
Also, take a look at the output and let know what you think. In
appends an 'm' in front of merge points. ('Merge points' is my name
for the revisions where the merging happens.) A verbose option showing
which revision was merged might also be cool.
Thanks,
Henri
-------------- next part --------------
=== modified file 'bzrlib/annotate.py'
--- bzrlib/annotate.py 2006-10-16 01:25:46 +0000
+++ bzrlib/annotate.py 2006-11-19 11:01:13 +0000
@@ -20,8 +20,6 @@
#
# interposed: show more details between blocks of modified lines
-# TODO: Show which revision caused a line to merge into the parent
-
# TODO: perhaps abbreviate timescales depending on how recent they are
# e.g. "3:12 Tue", "13 Oct", "Oct 2005", etc.
@@ -59,6 +57,7 @@
w = branch.repository.weave_store.get_weave(file_id,
branch.repository.get_transaction())
last_origin = None
+ merge_point_finder = None
for origin, text in w.annotate_iter(rev_id):
text = text.rstrip('\r\n')
if origin == last_origin:
@@ -71,7 +70,13 @@
if origin in rh:
revno_str = str(rh.index(origin) + 1)
else:
- revno_str = 'merge'
+ if merge_point_finder is None:
+ merge_point_finder = _MergePointFinder(branch)
+ merge_point = merge_point_finder._find_merge_point(origin)
+ if merge_point is not None:
+ revno_str = 'm' + str(rh.index(merge_point) + 1)
+ else:
+ revno_str = 'merge'
rev = branch.repository.get_revision(origin)
tz = rev.timezone or 0
date_str = time.strftime('%Y%m%d',
@@ -84,3 +89,49 @@
except NoEmailInUsername:
pass # use the whole name
yield (revno_str, author, date_str, origin, text)
+
+
+class _MergePointFinder(object):
+ def __init__(self, branch):
+ self.branch_revid = branch.last_revision()
+ self.graph = branch.repository.get_revision_graph(self.branch_revid)
+ self.queue = []
+ self.shortest_paths = {}
+ self._load_branch_parents()
+
+ def _load_branch_parents(self):
+ revid = self.branch_revid
+ parents = self.graph[revid]
+ while parents:
+ self.shortest_paths[revid] = None
+ for parent in parents[1:]:
+ self.shortest_paths[parent] = revid
+ self.queue.extend(parents[1:])
+ revid = parents[0]
+ parents = self.graph[revid]
+ #Add the last rev which has no parents.
+ self.shortest_paths[revid] = None
+ #Reverse the queue so that older revs get preference in the
+ #case of ties
+ self.queue.reverse()
+
+ def _get_merge_point(self, merged_revid):
+ current, next = None, merged_revid
+ while next is not None:
+ current, next = next, self.shortest_paths[next]
+ return current
+
+ def _find_merge_point(self, merged_revid):
+ if merged_revid in self.shortest_paths:
+ return self._get_merge_point(merged_revid)
+
+ while self.queue:
+ revid = self.queue.pop(0)
+ for parent in self.graph.get(revid,()):
+ if parent not in self.shortest_paths:
+ self.shortest_paths[parent] = revid
+ self.queue.append(parent)
+ if parent == merged_revid:
+ return self._get_merge_point(merged_revid)
+
+ return None
=== modified file 'bzrlib/tests/blackbox/test_annotate.py'
--- bzrlib/tests/blackbox/test_annotate.py 2006-10-11 23:08:27 +0000
+++ bzrlib/tests/blackbox/test_annotate.py 2006-11-19 17:15:31 +0000
@@ -40,23 +40,25 @@
def setUp(self):
super(TestAnnotate, self).setUp()
- wt = self.make_branch_and_tree('.')
- b = wt.branch
- self.build_tree_contents([('hello.txt', 'my helicopter\n'),
- ('nomail.txt', 'nomail\n')])
- wt.add(['hello.txt'])
- wt.commit('add hello', committer='test at user')
- wt.add(['nomail.txt'])
- wt.commit('add nomail', committer='no mail')
- file('hello.txt', 'ab').write('your helicopter')
- wt.commit('mod hello', committer='user at test')
+ self.main_working_tree = self.make_branch_and_tree('main_branch')
+ self.build_tree_contents([('main_branch/hello.txt', 'my helicopter\n'),
+ ('main_branch/nomail.txt', 'nomail\n')])
+ self.main_working_tree.add(['hello.txt'])
+ self.main_working_tree.commit('add hello', committer='test at user')
+ self.main_working_tree.add(['nomail.txt'])
+ self.main_working_tree.commit('add nomail', committer='no mail')
+ file('main_branch/hello.txt', 'ab').write('your helicopter\n')
+ self.main_working_tree.commit('mod hello', committer='user at test')
def test_help_annotate(self):
"""Annotate command exists"""
- out, err = self.run_bzr_captured(['--no-plugins', 'annotate', '--help'])
+ out, err = self.run_bzr_captured(
+ ['--no-plugins', 'annotate', '--help'],
+ working_dir='main_branch')
def test_annotate_cmd(self):
- out, err = self.run_bzr_captured(['annotate', 'hello.txt'])
+ out, err = self.run_bzr_captured(['annotate', 'hello.txt'],
+ working_dir='main_branch')
self.assertEquals(err, '')
self.assertEqualDiff(out, '''\
1 test at us | my helicopter
@@ -64,21 +66,24 @@
''')
def test_no_mail(self):
- out, err = self.run_bzr_captured(['annotate', 'nomail.txt'])
+ out, err = self.run_bzr_captured(['annotate', 'nomail.txt'],
+ working_dir='main_branch')
self.assertEquals(err, '')
self.assertEqualDiff(out, '''\
2 no mail | nomail
''')
def test_annotate_cmd_revision(self):
- out, err = self.run_bzr_captured(['annotate', 'hello.txt', '-r1'])
+ out, err = self.run_bzr_captured(['annotate', 'hello.txt', '-r1'],
+ working_dir='main_branch')
self.assertEquals(err, '')
self.assertEqualDiff(out, '''\
1 test at us | my helicopter
''')
def test_annotate_cmd_revision3(self):
- out, err = self.run_bzr_captured(['annotate', 'hello.txt', '-r3'])
+ out, err = self.run_bzr_captured(['annotate', 'hello.txt', '-r3'],
+ working_dir='main_branch')
self.assertEquals(err, '')
self.assertEqualDiff(out, '''\
1 test at us | my helicopter
@@ -87,13 +92,13 @@
def test_annotate_cmd_unknown_revision(self):
out, err = self.run_bzr_captured(['annotate', 'hello.txt', '-r', '10'],
- retcode=3)
+ working_dir='main_branch', retcode=3)
self.assertEquals(out, '')
self.assertContainsRe(err, 'Requested revision: \'10\' does not exist')
def test_annotate_cmd_two_revisions(self):
out, err = self.run_bzr_captured(['annotate', 'hello.txt', '-r1..2'],
- retcode=3)
+ working_dir='main_branch', retcode=3)
self.assertEquals(out, '')
self.assertEquals(err, 'bzr: ERROR: bzr annotate --revision takes'
' exactly 1 argument\n')
@@ -107,3 +112,50 @@
os.chdir('tree')
out, err = self.run_bzr('annotate', 'empty')
self.assertEqual('', out)
+
+ def test_annotate_merged_file(self):
+ second_working_tree = self.make_branch_and_tree('second_branch')
+ second_working_tree.pull(self.main_working_tree.branch)
+ file('second_branch/hello.txt', 'ab').write('no helicopter\n')
+ second_working_tree.commit('mod hello 2', committer='user2 at test')
+ self.main_working_tree.merge_from_branch(second_working_tree.branch)
+ self.main_working_tree.commit('merged mod hello 2',
+ committer='user at test')
+ out, err = self.run_bzr_captured(['annotate', 'hello.txt'],
+ working_dir='main_branch')
+ self.assertEquals(err, '')
+ self.assertEquals(out, '''\
+ 1 test at us | my helicopter
+ 3 user at te | your helicopter
+ m4 user2 at t | no helicopter
+''')
+
+ def test_annotate_twice_merged_file(self):
+ second_working_tree = self.make_branch_and_tree('second_branch')
+ second_working_tree.pull(self.main_working_tree.branch)
+ third_working_tree = self.make_branch_and_tree('third_branch')
+ third_working_tree.pull(self.main_working_tree.branch)
+
+ file('third_branch/hello.txt', 'ab').write('no helicopter\n')
+ third_working_tree.commit('mod hello 2', committer='user3 at test')
+ second_working_tree.merge_from_branch(third_working_tree.branch)
+ second_working_tree.commit('merged mod hello 2',
+ committer='user2 at test')
+ self.main_working_tree.merge_from_branch(second_working_tree.branch)
+ self.main_working_tree.commit('merged mod hello 2',
+ committer='user at test')
+
+ file('third_branch/hello.txt', 'ab').write('no helicopter\n')
+ third_working_tree.commit('mod hello 3', committer='user3 at test')
+ self.main_working_tree.merge_from_branch(third_working_tree.branch)
+ self.main_working_tree.commit('merged mod hello 3',
+ committer='user at test')
+ out, err = self.run_bzr_captured(['annotate', 'hello.txt'],
+ working_dir='main_branch')
+ self.assertEquals(err, '')
+ self.assertEquals(out, '''\
+ 1 test at us | my helicopter
+ 3 user at te | your helicopter
+ m4 user3 at t | no helicopter
+ m5 user3 at t | no helicopter
+''')
More information about the bazaar
mailing list