From b8f7aea3b07f19ff494ce4b5dc5e27e491da4074 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 23 Feb 2026 20:17:25 -0800 Subject: [PATCH 1/2] add `browse` command to open branch diff in web browser --- src/cli.ts | 7 +++ src/commands/browse.ts | 96 ++++++++++++++++++++++++++++++++++++++++++ src/git.ts | 8 ++++ 3 files changed, 111 insertions(+) create mode 100644 src/commands/browse.ts diff --git a/src/cli.ts b/src/cli.ts index 45e0ccb..b191e59 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,6 +16,7 @@ import { action as rebaseAction } from "./commands/rebase.ts" import { action as saveAction } from "./commands/save.ts" import { action as diffAction } from "./commands/diff.ts" import { action as showAction } from "./commands/show.ts" +import { action as browseAction } from "./commands/browse.ts" import { action as logAction } from "./commands/log.ts" import { action as dirAction } from "./commands/dir.ts" import { action as editAction } from "./commands/edit.ts" @@ -102,6 +103,12 @@ program .description("Show the prompt and full diff for a branch") .action(showAction) +program + .command("browse") + .argument("", "branch name") + .description("Open the branch diff in a web browser") + .action(browseAction) + program .command("save") .argument("", "branch name") diff --git a/src/commands/browse.ts b/src/commands/browse.ts new file mode 100644 index 0000000..eb474e8 --- /dev/null +++ b/src/commands/browse.ts @@ -0,0 +1,96 @@ +import { $ } from "bun" +import * as git from "../git.ts" +import { die } from "../fmt.ts" +import { requireSession } from "./helpers.ts" + +export async function action(branch: string) { + const { session } = await requireSession(branch) + const worktree = session.worktree + const main = await git.mainBranch(worktree) + + const [diff, log, stat] = await Promise.all([ + git.branchDiff(branch, worktree), + git.commitLog(`${main}..${branch}`, worktree), + git.diffStat(`${main}...${branch}`, worktree), + ]) + + if (!diff.trim()) { + die(`No changes on branch "${branch}" compared to ${main}.`) + } + + const prompt = session.prompt ? escapeHtml(session.prompt) : "" + const diffJson = JSON.stringify(diff) + + const html = ` + + + +${escapeHtml(branch)} — sandlot diff + + + + +
+

${escapeHtml(branch)}

+ ${prompt ? `

${prompt}

` : ""} +
+ ${log ? `

Commits

${escapeHtml(log)}
` : ""} + ${stat ? `

Stats

${escapeHtml(stat)}
` : ""} +
+
+
+ + + +` + + const tmpPath = `/tmp/sandlot-browse-${branch}.html` + await Bun.write(tmpPath, html) + await $`open ${tmpPath}`.nothrow().quiet() +} + +function escapeHtml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) +} diff --git a/src/git.ts b/src/git.ts index 379e139..21cf5d3 100644 --- a/src/git.ts +++ b/src/git.ts @@ -218,6 +218,14 @@ export async function hasNewCommits(worktreePath: string): Promise { return parseInt(result.text().trim(), 10) > 0 } +/** Get the full unified diff of a branch vs main as a string. */ +export async function branchDiff(branch: string, cwd: string): Promise { + const main = await mainBranch(cwd) + const result = await $`git diff ${main}...${branch}`.cwd(cwd).nothrow().quiet() + if (result.exitCode !== 0) return "" + return result.text() +} + /** Detect the main branch name (main or master). */ export async function mainBranch(cwd?: string): Promise { const dir = cwd ?? "." From 909189b74594ec6fb2215037209d0b8be4f0c05a Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 23 Feb 2026 20:33:10 -0800 Subject: [PATCH 2/2] Extract browse HTML into external template The HTML for the browse view was inlined as a template literal in browse.ts, making it hard to edit and losing syntax highlighting. Move it to browse.html and use placeholder replacement instead. Also update branchDiff to accept the main branch as a parameter so the caller resolves it once and reuses it for diffStat too. --- src/commands/browse.html | 60 +++++++++++++++++++++++++++++++++ src/commands/browse.ts | 73 +++++----------------------------------- src/git.ts | 3 +- 3 files changed, 70 insertions(+), 66 deletions(-) create mode 100644 src/commands/browse.html diff --git a/src/commands/browse.html b/src/commands/browse.html new file mode 100644 index 0000000..c2fbef9 --- /dev/null +++ b/src/commands/browse.html @@ -0,0 +1,60 @@ + + + + +{{BRANCH}} — sandlot diff + + + + +
+

{{BRANCH}}

+ {{PROMPT_SECTION}} +
+ {{LOG_SECTION}} + {{STAT_SECTION}} +
+
+
+ + + + diff --git a/src/commands/browse.ts b/src/commands/browse.ts index eb474e8..ebf69ac 100644 --- a/src/commands/browse.ts +++ b/src/commands/browse.ts @@ -3,13 +3,15 @@ import * as git from "../git.ts" import { die } from "../fmt.ts" import { requireSession } from "./helpers.ts" +const template = await Bun.file(new URL("browse.html", import.meta.url).pathname).text() + export async function action(branch: string) { const { session } = await requireSession(branch) const worktree = session.worktree const main = await git.mainBranch(worktree) const [diff, log, stat] = await Promise.all([ - git.branchDiff(branch, worktree), + git.branchDiff(branch, main, worktree), git.commitLog(`${main}..${branch}`, worktree), git.diffStat(`${main}...${branch}`, worktree), ]) @@ -18,69 +20,12 @@ export async function action(branch: string) { die(`No changes on branch "${branch}" compared to ${main}.`) } - const prompt = session.prompt ? escapeHtml(session.prompt) : "" - const diffJson = JSON.stringify(diff) - - const html = ` - - - -${escapeHtml(branch)} — sandlot diff - - - - -
-

${escapeHtml(branch)}

- ${prompt ? `

${prompt}

` : ""} -
- ${log ? `

Commits

${escapeHtml(log)}
` : ""} - ${stat ? `

Stats

${escapeHtml(stat)}
` : ""} -
-
-
- - - -` + const html = template + .replaceAll("{{BRANCH}}", escapeHtml(branch)) + .replace("{{PROMPT_SECTION}}", session.prompt ? `

${escapeHtml(session.prompt)}

` : "") + .replace("{{LOG_SECTION}}", log ? `

Commits

${escapeHtml(log)}
` : "") + .replace("{{STAT_SECTION}}", stat ? `

Stats

${escapeHtml(stat)}
` : "") + .replace("{{DIFF_JSON}}", JSON.stringify(diff)) const tmpPath = `/tmp/sandlot-browse-${branch}.html` await Bun.write(tmpPath, html) diff --git a/src/git.ts b/src/git.ts index 21cf5d3..417d86c 100644 --- a/src/git.ts +++ b/src/git.ts @@ -219,8 +219,7 @@ export async function hasNewCommits(worktreePath: string): Promise { } /** Get the full unified diff of a branch vs main as a string. */ -export async function branchDiff(branch: string, cwd: string): Promise { - const main = await mainBranch(cwd) +export async function branchDiff(branch: string, main: string, cwd: string): Promise { const result = await $`git diff ${main}...${branch}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) return "" return result.text()