diff options
author | Adam Spiers <git@adamspiers.org> | 2013-11-14 11:39:13 -0500 |
---|---|---|
committer | Adam Spiers <git@adamspiers.org> | 2015-01-05 16:54:34 +0000 |
commit | b1967573e81a8100a4cc778936de0ba0a8a8f5cb (patch) | |
tree | 0761f10bed08ce5151ab8f060215cce09027eb26 | |
download | git-deps-b1967573e81a8100a4cc778936de0ba0a8a8f5cb.tar.gz |
first prototype of git-deps
Automatic git commit dependency inference tool.
Originally committed to:
https://github.com/aspiers/git-config/blob/master/bin/git-deps
and then split off into this repository via git filter-branch
and other hackery, preserving history.
-rwxr-xr-x | git-deps | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/git-deps b/git-deps new file mode 100755 index 0000000..0d5a9bb --- /dev/null +++ b/git-deps @@ -0,0 +1,82 @@ +#!/usr/bin/python + +import argparse +import re +import sys +import subprocess +from textwrap import dedent, wrap + +import pygit2 + +def abort(msg, exitcode=1): + print >>sys.stderr, msg + sys.exit(exitcode) + +def parse_args(): + parser = argparse.ArgumentParser(description='Auto-detect inter-commit dependencies.') + parser.add_argument('--recurse', '-r', dest='recurse', action='store_true', + help='Follow dependencies recursively') + + options, args = parser.parse_known_args() + + if len(args) != 1: + abort("usage: git deps rev") + dependent_rev = args[0] + return options, dependent_rev + +def main(): + options, dependent_rev = parse_args() + + try: + repo_path = pygit2.discover_repository('.') + except KeyError: + abort("Couldn't find a repository in the current directory.") + + repo = pygit2.Repository(repo_path) + + try: + dependent = repo.revparse_single(dependent_rev) + except KeyError: + abort("Couldn't parse %s" % dependent_rev) + + for parent in dependent.parents: + find_dependencies(options, repo, dependent, parent) + +def find_dependencies(options, repo, dependent, parent): + dependencies = {} + diff = repo.diff(parent, dependent) + for patch in diff: + path = patch.old_file_path + #print(path) + for hunk in patch.hunks: + blame_hunk(options, parent, path, hunk, dependencies) + for dependency in dependencies: + if options.recurse: + print("%s %s" % (dependent.hex, dependency)) + else: + print(dependency) + # for path in dependencies[dependency]: + # print(" %s" % path) + # print(" %s" % ", ".join(sorted(dependencies[dependency][path].keys()))) + +def blame_hunk(options, commit, path, hunk, dependencies): + line_range = "%d,+%d" % (hunk.old_start, hunk.old_lines) + # for mode, line in hunk.lines: + # print(mode + line.rstrip()) + cmd = [ 'git', 'blame', commit.hex, '--porcelain', '-L', line_range, path ] + blame = subprocess.check_output(cmd) + for line in blame.split('\n'): + m = re.match('^([0-9a-f]{40}) (\d+) (\d+)( \d+)?$', line) + if not m: + continue + dependency, line_num = m.group(1, 2) + if dependency not in dependencies: + dependencies[dependency] = {} + if path not in dependencies[dependency]: + dependencies[dependency][path] = {} + if line_num in dependencies[dependency][path]: + abort("line %d already found when blaming %s:%s" % + (line_num, commit.hex[:8], path)) + dependencies[dependency][path][line_num] = True + +main() |