Analyst git repo for loc
2026-11-13
from collections import defaultdict
import subprocess
import sys
def run_git(args, repo_path):
cmd = ["git", "-C", repo_path] + args
result = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True
)
return result.stdout.strip()
def analyze_branches(repo_path):
print("Discovering branches and analyzing code volume per branch...")
raw_output = run_git(["branch", "-a", "--no-color"], repo_path)
branches = set()
for line in raw_output.splitlines():
line = line.strip().replace("* ", "")
if not line or "->" in line:
continue
if line.startswith("remotes/"):
line = line[8:]
branches.add(line)
branch_stats = {}
for branch in sorted(branches):
try:
files_out = run_git(["ls-tree", "-r", "--name-only", branch], repo_path)
files = [f for f in files_out.splitlines() if f.strip()]
except subprocess.CalledProcessError:
continue
total_lines = 0
file_count = 0
for file_path in files:
show_cmd = ["git", "-C", repo_path, "show", f"{branch}:{file_path}"]
try:
raw_content = subprocess.run(
show_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=False,
check=True,
).stdout
# Skip binary files containing null bytes
if b"\x00" in raw_content:
continue
lines = raw_content.count(b"\n")
if raw_content and not raw_content.endswith(b"\n"):
lines += 1
total_lines += lines
file_count += 1
except subprocess.CalledProcessError:
continue
branch_stats[branch] = {"lines": total_lines, "files": file_count}
return branch_stats
def analyze_developers(repo_path):
print("Analyzing developer contribution history across all branches...")
cmd = [
"git",
"-C",
repo_path,
"log",
"--all",
"--no-merges",
"--numstat",
"--format=AUTHOR:%an",
]
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
except subprocess.CalledProcessError as e:
print(f"Error executing git log: {e.stderr.strip()}")
return {}
developer_stats = defaultdict(lambda: {"added": 0, "deleted": 0, "net": 0})
current_author = "Unknown"
for line in result.stdout.splitlines():
line = line.strip()
if not line:
continue
if line.startswith("AUTHOR:"):
current_author = line[7:]
else:
parts = line.split("\t")
if len(parts) >= 2:
added, deleted = parts[0], parts[1]
if added.isdigit() and deleted.isdigit():
developer_stats[current_author]["added"] += int(added)
developer_stats[current_author]["deleted"] += int(deleted)
for author, stats in developer_stats.items():
stats["net"] = stats["added"] - stats["deleted"]
return developer_stats
if __name__ == "__main__":
target_path = sys.argv[1] if len(sys.argv) > 1 else "."
# --- Part 1: Branch Analysis ---
branch_stats = analyze_branches(target_path)
if branch_stats:
print("\n" + "=" * 80)
print("PART 1: CODE VOLUME BY BRANCH (Current HEAD of each branch)")
print("=" * 80)
print(f"{'Branch Name':<45} | {'Files':<8} | {'Total Lines':<12}")
print("-" * 80)
grand_total_lines = sum(data["lines"] for data in branch_stats.values())
for branch, data in sorted(
branch_stats.items(), key=lambda x: x[1]["lines"], reverse=True
):
print(f"{branch:<45} | {data['files']:<8,} | {data['lines']:<12,}")
print("=" * 80)
print(f"Combined total lines across all branch HEADs: {grand_total_lines:,}")
print("=" * 80)
# --- Part 2: Developer History Analysis ---
dev_stats = analyze_developers(target_path)
if dev_stats:
print("\n" + "=" * 95)
print(
"PART 2: DEVELOPER CONTRIBUTIONS ACROSS ALL HISTORY (All Branches,"
" No Merges)"
)
print("=" * 95)
print(
f"{'Developer':<30} | {'Lines Added':<14} | {'Lines Deleted':<14} |"
f" {'Net Lines':<12}"
)
print("-" * 95)
sorted_devs = sorted(
dev_stats.items(), key=lambda x: x[1]["added"], reverse=True
)
for author, data in sorted_devs:
print(
f"{author:<30} | {data['added']:<14,} | {data['deleted']:<14,}"
f" | {data['net']:<12,}"
)
print("=" * 95)