Last active
August 29, 2015 14:27
-
-
Save jonashaag/49550759b3220ecdf8c6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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