$ cat node-template.py
B
Branch compare
// Compare two branches of a GitHub repository: commits ahead and behind, the files changed, and the pull requests open from the branch or into it. No model — read straight from GitHub, so it is cheap to run again after a merge. Wire `repo` from "Working branch".repo to compare the working branch with its base. Reads only.
Process
Integration
#github#branch#compare#repo-read#vibe
template.py
1"""Branch compare — what one branch holds that another does not.23The plan's working branch collects every Lane's merged work before it ships to4the base as one pull request. Before that pull request is opened — and after5each merge, and whenever a reused branch is found holding work — the question6is the same: what is on it? This node answers it from GitHub, with no model:7commits ahead and behind, the files changed, and the pull requests open from8it or into it.910The Codebase map states the same branch facts at the start of a plan; this11node is the cheap re-read for later, when the branch has moved and the map's12facts are stale — re-running the map would re-read the code as well.1314Reads only. GitHub is reached through `gais.github_repo`, which keeps the15token out of every error and names the permission a 403 is missing.16"""1718from gais import github_repo as gh19from gais._errors import RemoteError2021COMMITS_SHOWN = 3022FILES_SHOWN = 30023PRS_SHOWN = 20242526def open_prs(client: gh.Client, owner: str, repo: str, branch: str) -> list[dict]:27 """Pull requests open FROM `branch` and INTO it."""28 rows = []29 for direction, query in (("out", f"head={gh.enc(owner + ':' + branch)}"),30 ("into", f"base={gh.enc(branch)}")):31 try:32 found = client.get(f"/repos/{owner}/{repo}/pulls?state=open&{query}&per_page={PRS_SHOWN}") or []33 except RemoteError as e:34 raise gh.explain(e, f"to list the pull requests of '{branch}'", gh.NEED["pulls_read"]) from None35 for pr in found:36 rows.append({37 "number": pr.get("number"),38 "title": pr.get("title") or "",39 "url": pr.get("html_url") or "",40 "head": (pr.get("head") or {}).get("ref") or "",41 "base": (pr.get("base") or {}).get("ref") or "",42 "draft": bool(pr.get("draft")),43 "direction": direction,44 })45 return rows464748def compare(client: gh.Client, owner: str, repo: str, base: str, head: str) -> dict:49 path = f"/repos/{owner}/{repo}/compare/{gh.enc(base)}...{gh.enc(head)}"50 try:51 return client.get(path) or {}52 except RemoteError as e:53 if e.http_status == 404:54 raise gh.RepoError(55 f"'{base}' or '{head}' was not found in {owner}/{repo}, or your GitHub connection "56 "cannot see the repository.") from None57 raise gh.explain(e, f"to compare '{head}' with '{base}'", gh.NEED["contents_read"]) from None585960def render(base: str, head: str, out: dict) -> str:61 ahead, behind = out["ahead_by"], out["behind_by"]62 if out["status"] == "identical":63 lines = [f"**{head}** and **{base}** are identical."]64 else:65 lines = [f"**{head}** is {ahead} commit{'s' * (ahead != 1)} ahead of **{base}** and "66 f"{behind} behind; {len(out['files'])} file{'s' * (len(out['files']) != 1)} changed."]67 if out["commits"]:68 listed = out["commits_listed"]69 lines += ["", "Commits (newest first):" if listed >= out["commits_total"]70 else f"Commits (GitHub listed {listed} of {out['commits_total']}; newest of those first):"]71 lines += [f"- `{c['sha'][:7]}` {c['message']}" for c in out["commits"]]72 if out["commits_total"] > len(out["commits"]):73 lines.append(f"- … {out['commits_total'] - len(out['commits'])} older")74 if out["files"]:75 lines += ["", "Files:"]76 lines += [f"- {f['status']} `{f['path']}` (+{f['additions']} −{f['deletions']})" for f in out["files"][:40]]77 if len(out["files"]) > 40:78 lines.append(f"- … {len(out['files']) - 40} more")79 if out["open_prs"]:80 lines += ["", "Open pull requests:"]81 lines += [f"- #{p['number']} {p['title']} ({p['head']} → {p['base']}{', draft' if p['draft'] else ''})"82 for p in out["open_prs"]]83 return "\n".join(lines)848586def run(inputs: dict, client: gh.Client) -> dict:87 owner, repo, repo_branch = gh.parse_repo(inputs.get("repo"))88 head = str(inputs.get("head") or "").strip() or repo_branch89 base = str(inputs.get("base") or "").strip()90 if not head:91 raise gh.RepoError("Set the branch to compare (`head`), or wire `repo` from \"Working branch\".repo.")92 if not base:93 base = gh.repository(client, owner, repo).get("default_branch") or ""94 for name in (base, head):95 problem = gh.branch_problem(name)96 if problem:97 raise gh.RepoError(problem)98 if base == head:99 raise gh.RepoError(f"'{head}' is compared with itself — set `base` to the branch it came from.")100101 cmp = compare(client, owner, repo, base, head)102 commits = cmp.get("commits") or []103 newest = list(reversed(commits))[:COMMITS_SHOWN]104 out = {105 "status": cmp.get("status") or "unknown",106 "ahead_by": int(cmp.get("ahead_by") or 0),107 "behind_by": int(cmp.get("behind_by") or 0),108 "head": head,109 "base": base,110 # From the ref, not the last listed commit: GitHub lists at most 250.111 "head_sha": gh.branch_sha(client, owner, repo, head) or "",112 "merge_base_sha": (cmp.get("merge_base_commit") or {}).get("sha") or "",113 "commits_total": int(cmp.get("total_commits") or len(commits)),114 "commits_listed": len(commits),115 "commits": [{116 "sha": c.get("sha") or "",117 "message": ((c.get("commit") or {}).get("message") or "").split("\n", 1)[0][:200],118 "author": (((c.get("commit") or {}).get("author") or {}).get("name") or ""),119 "date": (((c.get("commit") or {}).get("author") or {}).get("date") or ""),120 } for c in newest],121 "files": [{122 "path": f.get("filename") or "",123 "status": f.get("status") or "",124 "additions": int(f.get("additions") or 0),125 "deletions": int(f.get("deletions") or 0),126 } for f in (cmp.get("files") or [])[:FILES_SHOWN]],127 "open_prs": open_prs(client, owner, repo, head),128 "url": cmp.get("html_url") or f"https://github.com/{owner}/{repo}/compare/{base}...{head}",129 }130 out["summary"] = render(base, head, out)131 out["summary_line"] = (f"{owner}/{repo} {head} vs {base}: {out['status']}, +{out['ahead_by']} "132 f"−{out['behind_by']}, {len(out['files'])} files, {len(out['open_prs'])} open PRs")133 return out134135136def main() -> None:137 gh.run_node("branch-compare", run)138139140if __name__ == "__main__":141 main()142$ git log --oneline
v1.0.0
HEAD
2026-10-14First marketplace publish: vibe-coding lanes and the gstack plan flow (PR #1313).