A standalone Chrome extension that adds a Graph tab to GitHub repository pages and renders the commit graph. It needs no token, no OAuth, and no extension permissions: same-origin fetches from the content script ride the user's existing github.com session cookie, so any repo you can view in the browser (public, private, org) works without any setup.
WIP, extension store publish pending
Install from the extension store. Just search for "graph tab".
Works on Chrome and Edge, Firefox is WiP.
chrome://extensions→ enable Developer mode- Load unpacked → select this directory
- Open any repository on github.com → click the Graph tab
- Data (
src/data.js): GitHub's undocumented network-graph endpoints (/{owner}/{repo}/network/meta+network/chunk; the chunkendbound is inclusive, checked against the live API), fetched newest-window first, paginated by the "Load older commits" footer. The endpoints return the entire fork network with interleaved commits, so the focused repository is isolated by reachability frommeta.users[0].heads(author/owner/block filtering gives wrong results, see the findings doc). - Freshness (
src/gitproto.js,src/webfresh.js): the network-graph snapshot lags pushes by minutes to hours, so branch heads are re-read live and missing commits spliced in. Public repos: git smart-HTTP v2 on/{owner}/{repo}.git(ls-refs + atree:0-filtered fetch, parsed by an own pack reader and RFC-1951 inflater). The same ls-refs call also returns tags (peelresolves annotated tags to their commits), rendered as dashed chips next to the branch chips. Private repos reject anonymous git, so the graph walks/latest-commit/{ref}and/commit/{oid}(as JSON viaAccept) instead, and resolves tags through/refs?type=tag+/latest-commit/{tag}. That costs one request per missing commit (and per tag, capped). To keep that from blocking, the graph is drawn from the snapshot straight away ("Updating…" on the pill) and redrawn when the fetch settles; the commit list (/commits/{oid}, 35 oids per request, no parents) names the missing commits up front, so their pages are fetched a few at a time instead of one parent after another. A load spends at most 100 commit requests: a branch that moved further is drawn from its live head down to the last commit fetched, with a dashed tail, and "Load older commits" fetches the next 100 below it. Fetched commits are immutable, so they are cached (localStorage, keyed by oid) and never re-fetched. - Branches (
src/branches.js,src/render.js,src/octicon.js): which branches the graph draws is a choice, because a branch outside the snapshot window costs a request to pull in and not everyone wants everydependabot/*line. The default is free: the default branch (HEAD's symref target from ls-refs, or thedefaultBranchthe repo page embeds) plus every branch the window already holds — the old behaviour, at the old cost. The default branch is always drawn and cannot be unticked, so lane 0 is always it. Ticking more branches fetches just their tips (want+ a smalldeepen, with nohavelines: the loaded window is a slice of the network array, not an ancestor-closed set, so promising it as "have" makes the server negotiate everything away and answer with an empty pack). A branch that merely moved past a snapshot head the window holds is the one case wherehaveis right —have <snapshot head>stops the pack exactly where known history begins — so its gap is bridged in full, not cut at the stub depth. A branch that reaches back further than the fetch is drawn as a stub with a dashed tail rather than hidden, the waygit log --graph --allshows a shallow or unrelated history. Choices are remembered per repository. The picker is GitHub's SelectPanel — the control behind its repository and branch dropdowns — with Primer's own rules:prc-Overlay-Overlay,prc-SelectPanel-*,prc-FilteredActionList-*,prc-ActionList-*,prc-SegmentedControl-*and the TextInput wrapper are lifted verbatim out of GitHub's stylesheet (primer-react-css.*.module.css) with only their selectors renamed, so every declaration, custom property and fallback is GitHub's — down to the ActionList's grid areas, itsdata-dividershairlines and the way they hide around the active row, and the accent bar on[data-is-active-descendant]. They are shipped rather than borrowed because GitHub code-splits that stylesheet: on a plain repository page none of it is loaded until one of GitHub's own menus mounts. It is injected fromrender.js, notstyle.css, because the manifest only refreshes that one when the extension is reloaded while the modules refresh on every page load. Refresh the copy by re-running the extraction against the current asset URL. The overlay is fixed-positioned ondocument.body: inside the graph shell it would be part of the toolbar's layout and clipped by the shell'soverflow. A pick applies immediately — no Apply, no close button. The two bulk settings live in the header as a Primer SegmentedControl ("Default | All") rather than as rows: they act on the whole list, and GitHub likewise puts the one switch its branch panel has above the list, never inside it. Two named segments also avoid the lie a select-all checkbox would tell — a graph has to draw at least one branch, so "unchecked" has no honest meaning — and hand-picking simply leaves neither segment active, which is what "custom" looks like. While the pick is in flight the button spins, a progress bar rides the top of the frame and the rows dim, and the rebuilt graph fades up out of that dim instead of cutting. - Ordering (
src/order.js): the network array's absolute position is a date axis, so commits spliced in from git or the commit pages are placed on it by their own date (chronoIndex) rather than stacked on top. Dates alone cannot guarantee a child stays above its parent, so the final row order is decided topologically and uses the date only to choose between commits that are ready — which is also what makes two histories with nothing in common interleave sanely instead of fighting over the lanes. - Layout (
src/layout.js): pure, DOM-free lane assignment following the classicgit log --graph/ vscode-git-graph model: lane reservation and release, merge edges that join already-open lanes, octopus merges, and dashed tails for parents that live below the loaded window. Lane 0 is reserved for the default branch and is never handed to anything else, so the leftmost line always means the same thing — otherwise it goes to whichever commit happens to be newest, which on a busy repository is any random topic branch. - Rendering (
src/render.js): one SVG behind fixed-height HTML rows, cubic-bezier lane transitions, a stable 12-color palette, and GitHub CSS variables for the chrome so light/dark themes both work. - Integration (
src/tab.js,src/main.js): the nav tab is built from scratch (not cloned from GitHub markup) and kept alive across soft navigations by a debounced MutationObserver +turbo:load. The view is URL-driven: the tab pushes#graphand the view opens/closes purely fromlocation, so refresh, back/forward, and pasted links all work (a hash, unlike a real path, never reaches the server, so nothing can 404).
No build step: loader.js dynamic-imports src/main.js as an ES module.
Built with Codex (GPT 5.6), which unblocked the three problems that stopped existing extensions:
- Authentication: asking users for a token or OAuth was never an option
for an extension. Codex, using Playwright and
gh api, found GitHub's undocumented network-graph endpoints, so the extension needs no permissions and no token. - Stale data: fresh commits are fetched live over the git smart protocol.
- Imperfect visuals: it took a lot of iteration and testing to get the graph rendering right.
node --test tests/*.test.mjs
Layout is a pure function, so the graph geometry (lanes, colors, merge joins,
coalesced straight runs, dangling tails) is asserted directly; network code
is tested by stubbing globalThis.fetch.
Pull requests are welcome. The preferred flow:
- Branch off
mainfor each change; do not send your fork'smainas the PR branch. - Keep the branch linear. Rebase onto
mainwhen it moves instead of mergingmaininto your branch; a PR should be a clean series of commits with no merge commits inside it. - One concern per commit, with a message that says why, not just what. Run the tests before pushing.
- PRs are landed with a merge commit (no fast-forward, no squash), so the branch's commits stay as they were reviewed and the merge itself marks where the change came in.
package.sh can be used to create a zip for the browser extension store
