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
|