-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-commit-map
More file actions
executable file
·88 lines (71 loc) · 2.84 KB
/
Copy pathgit-commit-map
File metadata and controls
executable file
·88 lines (71 loc) · 2.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env python3
"""Show commits in a range: file -> function -> lines.
Usage:
git-commit-map # merge-base with main/master
git-commit-map <branch> # merge-base with <branch>
git-commit-map <rev>..<rev> # explicit range
git-commit-map --from-init # from first commit
"""
import re, subprocess, sys
from collections import defaultdict
def run(cmd, check=True):
return subprocess.run(cmd, capture_output=True, text=True, check=check)
def resolve_range(arg):
if arg == '--from-init':
root = run(['git', 'rev-list', '--max-parents=0', 'HEAD']).stdout.strip()
return f'{root}..HEAD'
if '..' in arg or arg.startswith('^'):
return arg
r = run(['git', 'merge-base', 'HEAD', arg], check=False)
return f'{r.stdout.strip()}..HEAD' if r.returncode == 0 else arg
def default_range():
for b in ['main', 'master', 'origin/main', 'origin/master']:
r = run(['git', 'merge-base', 'HEAD', b], check=False)
if r.returncode == 0:
return f'{r.stdout.strip()}..HEAD'
return 'HEAD~10..HEAD'
def parse_diff(diff_text):
"""Return {file: {func: [(start, end)]}}."""
files = {}
cur = None
for line in diff_text.splitlines():
if line.startswith('+++ b/'):
cur = line[6:]
files.setdefault(cur, defaultdict(list))
elif line.startswith('+++ /dev/null'):
cur = None
elif line.startswith('@@') and cur is not None:
m = re.match(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@(.*)', line)
if m:
start = int(m.group(1))
count = int(m.group(2)) if m.group(2) is not None else 1
end = start + max(count - 1, 0)
func = m.group(3).strip()
files[cur][func].append((start, end))
return files
def fmt(start, end):
if end == 0:
return f'after {start}'
return f'{start}' if start == end else f'{start}-{end}'
def main():
range_spec = resolve_range(sys.argv[1]) if len(sys.argv) > 1 else default_range()
shas = run(['git', 'log', '--format=%H', range_spec]).stdout.strip().splitlines()
if not shas:
print('No commits found.')
return
for sha in reversed(shas):
subject = run(['git', 'show', '--no-patch', '--format=%s', sha]).stdout.strip()
diff = parse_diff(run(['git', 'show', '--unified=0', '--format=', sha]).stdout)
print(f'{sha[:12]} {subject}')
for filepath, funcs in diff.items():
print(f' {filepath}')
for func, ranges in funcs.items():
locs = ', '.join(fmt(s, e) for s, e in ranges)
if func:
print(f' {func}')
print(f' {locs}')
else:
print(f' {locs}')
print()
if __name__ == '__main__':
main()