xil_effort.py — estimate effort from git history + xil pipeline logs.
Lives in tools/; run from anywhere: python3 tools/xil_effort.py [--log PATH ...]
Git repo: auto-detected from this script's location.
Data root: $XIL_PROJECTROOT — logs read from $XIL_PROJECTROOT/logs/*.log,
outputs written to $XIL_PROJECTROOT/reports/. Falls back to the repo root
for both if $XIL_PROJECTROOT is unset.
DATA = Path(os.environ.get('XIL_PROJECTROOT') or REPO)
REPORTS = DATA / 'reports'
ap = argparse.ArgumentParser()
commits = git('log', '--date=short', '--pretty=%ad')
merges = git('log', '--merges', '--date=short', '--pretty=%ad')
day_commits = defaultdict(int)
day_merges = defaultdict(int)
day_ins = defaultdict(int)
day_del = defaultdict(int)
day_pipe = defaultdict(int)
cmd_counts = defaultdict(int)
FNAME_DATE = re.compile('xil_(?:v\\d+_)?(\\d{4}-\\d{2}-\\d{2})(?:_[^/]*)?\\.log$')
HEADER = re.compile('^([A-Z][A-Z0-9_/-]+(?: [A-Z0-9_/-]+)+)')
LEVEL = re.compile('^[A-Z]+$')
m = FNAME_DATE.search(path)
parts = [(p.strip()) for p in (line.split('|'))]
day = dt.date.fromisoformat(parts[0][:10]).isoformat()
msg = 'no log files matched' if matched_files == 0 else f'{matched_files} log file(s) matched but no activity was parsed'
ins = re.search('(\\d+) insertion', line)
dele = re.search('(\\d+) deletion', line)
weeks = defaultdict(lambda: {'commits': 0, 'merges': 0, 'days': set(), 'ins': 0, 'del': 0, 'pipe': 0})
csv_out = open(REPORTS / 'effort_weekly.csv', 'w', newline='')
t = tier(n, w['ins'] + w['del'])
tot_c = sum((w['commits']) for w in (weeks.values()))
tot_m = sum((w['merges']) for w in (weeks.values()))
tot_p = sum((w['pipe']) for w in (weeks.values()))
tot_d = sum((len(w['days'])) for w in (weeks.values()))
tot_i = sum((w['ins']) for w in (weeks.values()))
tot_x = sum((w['del']) for w in (weeks.values()))
all_days = set(day_commits) | set(day_pipe)
first = min((dt.date.fromisoformat(d)) for d in all_days)
last = max((dt.date.fromisoformat(d)) for d in all_days)
start = first - dt.timedelta(days=first.weekday())
peak = max(day_commits.values()) if day_commits else 1
cells = defaultdict(dict)
n = day_commits.get(d.isoformat(), 0)
tds = ''.join((f'<td title="{c[0]} — {c[1]} commits, {day_pipe.get(c[0].isoformat(), 0)} pipe runs, +{day_ins.get(c[0].isoformat(), 0)}/−{day_del.get(c[0].isoformat(), 0)} lines" style="{cell_style(c[0], c[1])}"></td>' if (c := (cells[wd].get(i))) else '<td></td>') for i in (range(wi + 1)))
html = f'<html><body style='font-family:sans-serif'><h3>xil-pipeline activity ({first} → {last})</h3><table style='border-spacing:2px'>{''.join(rows)}</table><p>{tot_c} commits, {tot_m} merges, {tot_p} pipeline runs, {tot_d} active days, +{tot_i}/−{tot_x} lines. Green = commits (peak {peak}/day); orange outline = pipeline activity.</p></body></html>'
Source code in tools/xil_effort.py
| def find_repo():
here = Path(__file__).resolve().parent
try:
top = subprocess.run(["git", "-C", str(here), "rev-parse", "--show-toplevel"],
capture_output=True, text=True, check=True).stdout.strip()
return Path(top)
except subprocess.CalledProcessError:
raise SystemExit(f"No git repo found at or above {here}")
|
Source code in tools/xil_effort.py
| def git(*a):
return subprocess.run(["git", "-C", str(REPO)] + list(a),
capture_output=True, text=True, check=True).stdout.splitlines()
|
Source code in tools/xil_effort.py
| def iso_week(datestr):
y, w, _ = dt.date.fromisoformat(datestr).isocalendar()
return f"{y}-W{w:02d}"
|
Source code in tools/xil_effort.py
| def tier(active_days, churn):
base = 2 if active_days >= 4 else 1 if active_days >= 2 else 0
if churn >= 1000: base = min(base + 1, 2) # big churn bumps a tier
return ["Low", "Medium", "High"][base]
|
Source code in tools/xil_effort.py
| def color(n):
if n == 0: return "#ebedf0"
steps = ["#9be9a8", "#40c463", "#30a14e", "#216e39"]
return steps[min(int(n / peak * 4), 3)]
|
Source code in tools/xil_effort.py
| def cell_style(day, n):
s = f"width:12px;height:12px;background:{color(n)};border-radius:2px"
if day_pipe.get(day.isoformat(), 0):
s += ";outline:2px solid #f66a0a;outline-offset:-2px" # pipeline marker
return s
|