Map your pull request

Glance shows which files and functions a branch changed, how those functions call each other, and which lines match your own flag rules.

$ pnpm add -g github:nishilfaldu/glance

Repo map

Shows every folder and file in the repo. Each changed file lists the functions the branch touched. Switch to Changed only to hide everything the branch didn't touch. Click a function to mark it reviewed.

Rotate sessions across checkout #42Live

Call graph

Shows only the changed files and the folders that lead to them, starting from the deepest folder they all share. Gray lines join folders to their files and functions. Arrows show which changed function calls which. Hover a function to highlight its calls.

Rotate sessions across checkout #42Live

Run it

Check out a branch and run glance. The page opens on localhost and nothing is uploaded. Add --working to include changes you haven't committed.

~/storefront
$ git switch feature/session-rotation $ glance feature/session-rotation → main: 11 files, 14 functions, 4 flagged Review page: http://127.0.0.1:53817/r/local

Share it

To share the page, run glance publish --pr. It uploads the brief (described below) to your own Vercel Blob store and adds the link to the pull request's description. Running it again updates the same link. The upload has file and function names, not your code.

The link opens a page on glance.nishilfaldu.site, and that page loads the brief from your store. The part of the link after the # says which store and brief to load. Browsers never send that part to glance.nishilfaldu.site, so the site can't see which briefs you open.

Rotate sessions across checkout #42Open
$ export BLOB_READ_WRITE_TOKEN=vercel_blob_rw_… $ glance publish --pr feature/session-rotation → main: 11 files, 14 functions, 4 flagged https://glance.nishilfaldu.site/r#k3x9d2/9xQ2mV7cLk0aTf3RzH1bWg
Adds refresh tokens and rotates the session on checkout.
Review mapglance.nishilfaldu.site/r#k3x9…you · now
#42 Linked+482−37

Under the hood

How it works

Glance doesn't use AI. It reads git and parses your code with the TypeScript compiler, so the same branch always gives the same result. These are the steps, using the example above.

From branch to map

Where the branch starts

Glance compares your branch with the last commit it shares with main. Git calls that commit the merge base. Comparing with the newest commit on main would also show changes other people merged after you branched.

By default, main is the remote's default branch, or else main or master. Use --base to compare with a different branch.

~/storefront
Fix pagination on ordersmain
Bump eslintmain
Rotate sessions across checkoutyour branch
Initial storefront88fc5edmerge base
$ git merge-base main feature/session-rotation 88fc5ed

What changed

git diff --name-status -M lists the files that were added, changed or renamed. Deleted files are left out, because they no longer exist on the branch.

Glance skips the files you list under skip: in .glance.yml, such as generated code. Skipped files still appear on the map with a dashed border, but Glance doesn't read them. For every other file, git diff -U0 gives the exact line numbers that changed.

~/storefront
$ git diff --name-status -M 88fc5ed HEAD M docs/sessions.md skip: .glance.yml M generated/client.ts skip: .glance.yml M package.json M src/api/checkout.ts M src/api/routes.ts A src/auth/refresh.ts M src/auth/session.ts M src/utils/date.ts M src/web/sessionClient.ts M tests/checkout.test.ts M tests/session.test.ts
$ git diff -U0 88fc5ed HEAD -- src/auth/session.ts @@ -7,0 +9 @@ nothing removed, 1 added at line 9 + rotatedFrom?: string; @@ -10 +12,6 @@ line 10 replaced by 12–17 -const SESSION_TTL_MINUTES = 30; +export interface SessionOptions { …

Finding the changed functions

Glance parses each changed file with the TypeScript compiler. It doesn't build your project or check types, so any TS or JS file works. These count as functions: function declarations, named arrow functions, class methods, route handlers like "POST /session", and test cases like it("…", () => …).

Each changed line belongs to the smallest function that contains it. A removed line belongs to the function now at that spot. Lines outside any function belong to the file. A function is new if main's version of the file has no function with that name, and changed if it does. In package.json, each added or updated dependency gets its own entry.

session.ts src/auth13
1 import { SignJWT } from "jose";
2 import { db } from "../db";
3
4+const SECRET = process.env.SESSION_SECRET ?? "dev-secret";
5+const key = new TextEncoder().encode(SECRET);
6
signToken() · new
7+async function signToken(userId: string) {
8+ return new SignJWT({ sub: userId })
9+ .setExpirationTime("15m").sign(key);
10+}
11
createSession() · changed
12 export async function createSession(userId: string) {
− const token = crypto.randomUUID();
13+ const token = await signToken(userId);
14 await db.sessions.insert({ userId, token });
15 return token;
16 }
17
rotateSession() · new
18+export async function rotateSession(oldToken: string) {
19+ const old = await db.sessions.take(oldToken);
20+ if (!old) return null;
21+ return createSession(old.userId);
22+}

Connecting calls

For each function, Glance records what it calls: foo(), new Foo(), <Foo />, ns.foo() when ns comes from import * as ns, class methods (this.save(), new Cart().total(), or cart.total() after const cart = new Cart()), and functions passed by name, like list.map(build) or onClick={save}. It looks for each name in the same file first, then in the file's imports.

An import can be a relative path, a paths alias from the nearest tsconfig.json (like @/lib), or a package that lives in the same repo. Glance follows export * from and export { … } from to reach the function itself. Any other package points to its entry in the nearest package.json.

If both functions changed, the call graph draws an arrow between them. Glance stops at your own code. It doesn't follow calls into a package from outside the repo, or calls on objects like db.insert(), which usually go into a library or ORM.

checkout.ts src/api
1 import { rotateSession } from "../auth/session";
2
3 export async function handleCheckout(req: Request) {
4+ const token = await rotateSession(req.token);
Imported from ../auth/session
src/auth/sessionnot a file
src/auth/session.tsfound
rotateSession()touched

Flags

Glance only flags what you ask for. Add rules under flags: in .glance.yml at the root of your repo. Each rule has a name and at least one of these:

  • path: a file pattern. On its own, it flags every changed file that matches.
  • added: a regular expression, checked against each added line.
  • removed: a regular expression, checked against each removed line that wasn't added back.

A match on a line flags the function around it. Each rule gets one number on the page; click a numbered item to see the rule's name in the bar at the bottom. With no rules, nothing is flagged.

.glance.yml storefront5 rules
skip: - generated/** - docs/** flags: - name: auth code 1 path: src/auth/** - name: new catch block 2 added: \bcatch\b - name: env var with a fallback 3 added: process\.env\.\w+\s*(\?\?|\|\|) - name: type check bypassed 4 added: as any|@ts-ignore|eslint-disable - name: assertion removed 5 path: tests/** removed: \b(expect|assert)\(

The brief

Glance saves the result as one JSON file called the brief. It has the folder tree, the state of each file, the changed functions and their calls, and the flags. It has names, not code. The local page reads the brief from memory, and glance publish uploads the same file. Run glance json to print it.

brief.json src/auth/session.ts
{ "name": "session.ts", "state": "changed", "flags": ["auth code", "env var with a fallback"], "fns": [ { "name": "signToken()", "state": "new", "calls": ["package.json#jose"] }, { "name": "createSession()", "state": "changed", "calls": ["src/auth/session.ts#signToken()"] }, { "name": "rotateSession()", "state": "new", "calls": ["src/auth/session.ts#createSession()"] } ] }

Drawing the page

The page is a single HTML file without a framework or graph library. The repo map is nested boxes that wrap like text. The call graph puts folders, files and functions in columns. Each column starts right after the widest label in the column before it. Each function gets its own row, and each folder or file is centered next to what it contains.

Lines are SVG curves. Call arrows curve out to the right of the functions, so they don't cross the folder lines.

Laying out the call graph
One column each for folders, files and functionsdepth
Next column starts 34px past the widest labelmeasure
Each function gets a row; folders and files are centeredrows
Call arrows curve out to the rightSVG

Reference

glanceOpen the map on localhost
glance publishUpload it to your Vercel Blob store and print a link
glance jsonPrint the brief as JSON
--base <ref>Compare against this branch
--workingInclude uncommitted changes
--pr [n]Link it on the pull request
glance --help
Usage glance [options] Open the review page for this branch glance publish [options] Upload it to your Blob store and print a link glance json [options] Print the brief as JSON Options --base <ref> Branch to compare against --head <ref> Commit or branch to review (default: HEAD) --working Include uncommitted and untracked changes --title <text> Title shown on the page --pr [number] With publish: link it on the pull request --port <n> Port for the local page --no-open Don't open a browser .glance.yml, in the repo root: skip: - generated/** flags: - name: auth code path: src/auth/** Publishing needs BLOB_READ_WRITE_TOKEN (a public Blob store you own), or "blobToken" in ~/.config/glance/config.json

Limits

Glance only finds functions in TypeScript and JavaScript. It doesn't follow calls on objects it can't trace back to a class in the repo (db.insert()), or into packages from outside the repo. Deleted files don't appear on the map.