Created
January 12, 2026 21:06
-
-
Save marcaurele/e45a8fa4b411b8148eece62171c1952e to your computer and use it in GitHub Desktop.
Git statistics over commits across a yearly time window.
This file contains hidden or 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
| # /// script | |
| # requires-python = ">=3.11" | |
| # dependencies = [] | |
| # /// | |
| # uv run git_stats.py 2025 <optional author> | |
| import subprocess | |
| import sys | |
| from collections import defaultdict | |
| def get_detailed_author_stats(year, target_author): | |
| # Format: HASH | DATE | ADDED | REMOVED | |
| cmd = [ | |
| "git", "log", | |
| f"--since={year}-01-01", | |
| f"--until={year}-12-31", | |
| f"--author={target_author}", | |
| "--numstat", | |
| "--pretty=format:COMMIT:%h|%ad", | |
| "--date=short", | |
| "--", | |
| "**/test_*.py", | |
| # "**/tests/*.py", | |
| ] | |
| try: | |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) | |
| commits = [] | |
| current_commit = None | |
| for line in result.stdout.splitlines(): | |
| line = line.strip() | |
| if not line: continue | |
| if line.startswith("COMMIT:"): | |
| details = line.replace("COMMIT:", "").split("|") | |
| current_commit = {"hash": details[0], "date": details[1], "added": 0, "removed": 0} | |
| commits.append(current_commit) | |
| continue | |
| if current_commit: | |
| parts = line.split() | |
| if len(parts) >= 3: | |
| try: | |
| current_commit["added"] += int(parts[0]) | |
| current_commit["removed"] += int(parts[1]) | |
| except ValueError: | |
| continue | |
| # Sort: (Added + Removed) descending | |
| commits.sort(key=lambda x: x["added"] + x["removed"], reverse=True) | |
| print(f"\n--- Commit Detail for '{target_author}' in {year} ---") | |
| print(f"{'Hash':<8} | {'Date':<12} | {'Added':>8} | {'Removed':>8}") | |
| print("-" * 45) | |
| for c in commits: | |
| if c["added"] + c["removed"] > 0: | |
| print(f"{c['hash']:<8} | {c['date']:<12} | {c['added']:>8} | {c['removed']:>8}") | |
| except subprocess.CalledProcessError as e: | |
| print(f"Error executing git: {e}") | |
| def get_summary_stats(year): | |
| # (Existing summary logic from previous step) | |
| cmd = [ | |
| "git", "log", | |
| f"--since={year}-01-01", | |
| f"--until={year}-12-31", | |
| "--numstat", | |
| "--pretty=format:AUTHOR:%aN", | |
| "--", | |
| "**/test_*.py", | |
| # "**/tests/*.py", | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) | |
| stats = defaultdict(lambda: {"added": 0, "removed": 0}) | |
| current_author = None | |
| for line in result.stdout.splitlines(): | |
| if line.startswith("AUTHOR:"): | |
| current_author = line.replace("AUTHOR:", "") | |
| elif current_author and line.strip(): | |
| parts = line.split() | |
| if len(parts) >= 3: | |
| try: | |
| stats[current_author]["added"] += int(parts[0]) | |
| stats[current_author]["removed"] += int(parts[1]) | |
| except ValueError: continue | |
| print(f"\n--- Summary for {year} ---") | |
| print(f"{'Author':<25} | {'Added':>10} | {'Removed':>10}") | |
| print("-" * 51) | |
| for author, counts in sorted(stats.items(), key=lambda x: x[1]['added'], reverse=True): | |
| print(f"{author[:25]:<25} | {counts['added']:>10} | {counts['removed']:>10}") | |
| if __name__ == "__main__": | |
| year = sys.argv[1] if len(sys.argv) > 1 else "2025" | |
| author = sys.argv[2] if len(sys.argv) > 2 else None | |
| if author: | |
| get_detailed_author_stats(year, author) | |
| else: | |
| get_summary_stats(year) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment