Skip to content

Instantly share code, notes, and snippets.

@jonashaag
Last active August 29, 2015 14:27
Show Gist options
  • Save jonashaag/49550759b3220ecdf8c6 to your computer and use it in GitHub Desktop.
Save jonashaag/49550759b3220ecdf8c6 to your computer and use it in GitHub Desktop.
"""Experimental implementation of a git 'block history' log, that is, the history
of all changes made to a certain _named_ program block (class, def).
Blocks are identified in the source code by their fully qualified names::
# test_module.py
class Foo:
def bar(self):
if ...:
...
else:
...
fqn(bar metod) = ('test_module', 'Foo', 'bar')
In this implementation, only the last item of the fqn tuple is used.
Block history is implemented as a loop between 'git log' and a parser of the
target program's programming language.
Using the parser, we can find the block's start and end line numbers.
The 'bar' block in the example above starts at line 2 and ends at line 6.
The (start, end) information can be used to restrict 'git log' to showing only
changes made to that line range::
git log -L start,end:test_module.py
The first commit of this log is being taken as the next iteration's starting
revision as we can be sure it's the next change to the block we're looking for.
We start over at the beginning of the loop by parsing the program at the new
revision we just found. This is repeated as long as the block exists in history.
"""
import sys
import subprocess
import ast
import collections
def get_file_contents(filename, rev):
"""Get contents of `filename` at git revision `rev`."""
return subprocess.check_output(['git', 'show', '%s:%s' % (rev, filename)])
def get_block_range(block_name, contents):
"""Figure out the start and end line number of the block `block_name`."""
module = ast.parse(contents)
worklist = collections.deque([module])
start = end = None
while worklist:
current = worklist.popleft()
if start is not None:
end = current.lineno
break
if getattr(current, 'name', None) == block_name:
start = current.lineno
else:
worklist.extend(getattr(current, 'body', []))
return start, end
def get_next_block_change(start_rev, block_start, block_end, source):
"""Find next commit that changed a block."""
proc = subprocess.Popen(['git', 'log', '-L', '%d,%d:%s' % (block_start, block_end, source), '--pretty=%H', start_rev],
stdout=subprocess.PIPE)
line = proc.stdout.readline()
proc.terminate()
return line.strip().decode()
def main(source, block_name):
current_rev = "HEAD"
while 1:
start, end = get_block_range(block_name, get_file_contents(source, current_rev))
if start is None:
break
if current_rev != "HEAD":
current_rev += "~1"
current_rev = get_next_block_change(current_rev, start, end, source)
print(current_rev)
main(*sys.argv[1:])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment