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.
This commit is contained in:
Chris Wanstrath 2026-02-23 20:33:10 -08:00
parent b8f7aea3b0
commit 909189b745
3 changed files with 70 additions and 66 deletions

60
src/commands/browse.html Normal file
View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{BRANCH}} — sandlot diff</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; margin: 0; padding: 0; background: #0d1117; color: #e6edf3; }
.header { padding: 24px 32px; border-bottom: 1px solid #30363d; }
.header h1 { margin: 0 0 8px; font-size: 24px; font-weight: 600; }
.header h1 code { background: #1f2937; padding: 2px 8px; border-radius: 6px; font-size: 22px; }
.prompt { color: #8b949e; margin: 0 0 16px; font-style: italic; }
.meta { display: flex; gap: 32px; }
.meta-section h3 { margin: 0 0 6px; font-size: 13px; text-transform: uppercase; color: #8b949e; letter-spacing: 0.05em; }
.meta-section pre { margin: 0; font-size: 13px; line-height: 1.5; color: #c9d1d9; white-space: pre-wrap; }
.diff-container { padding: 16px 32px; }
/* Override diff2html for dark theme */
.d2h-wrapper { background: #0d1117; }
.d2h-file-header { background: #161b22; border-color: #30363d; color: #e6edf3; }
.d2h-file-wrapper { border-color: #30363d; margin-bottom: 16px; border-radius: 6px; overflow: hidden; }
.d2h-code-linenumber { background: #161b22; color: #8b949e; border-color: #30363d; }
.d2h-code-line { background: #0d1117; color: #e6edf3; }
.d2h-code-side-line { background: #0d1117; }
.d2h-del { background: rgba(248,81,73,0.1); }
.d2h-ins { background: rgba(63,185,80,0.1); }
.d2h-del .d2h-code-line-ctn { background: rgba(248,81,73,0.15); }
.d2h-ins .d2h-code-line-ctn { background: rgba(63,185,80,0.15); }
.d2h-code-line-ctn { color: #e6edf3; }
.d2h-info { background: #161b22; color: #8b949e; border-color: #30363d; }
.d2h-tag { background: #1f6feb; color: #fff; }
.d2h-file-stats-wrapper { display: flex; }
</style>
</head>
<body>
<div class="header">
<h1><code>{{BRANCH}}</code></h1>
{{PROMPT_SECTION}}
<div class="meta">
{{LOG_SECTION}}
{{STAT_SECTION}}
</div>
</div>
<div class="diff-container" id="diff"></div>
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
<script>
const diffString = {{DIFF_JSON}};
const targetElement = document.getElementById("diff");
const configuration = {
drawFileList: true,
matching: "lines",
outputFormat: "side-by-side",
highlight: true,
colorScheme: "dark",
};
const diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
diff2htmlUi.draw();
diff2htmlUi.highlightCode();
</script>
</body>
</html>

View File

@ -3,13 +3,15 @@ import * as git from "../git.ts"
import { die } from "../fmt.ts" import { die } from "../fmt.ts"
import { requireSession } from "./helpers.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) { export async function action(branch: string) {
const { session } = await requireSession(branch) const { session } = await requireSession(branch)
const worktree = session.worktree const worktree = session.worktree
const main = await git.mainBranch(worktree) const main = await git.mainBranch(worktree)
const [diff, log, stat] = await Promise.all([ const [diff, log, stat] = await Promise.all([
git.branchDiff(branch, worktree), git.branchDiff(branch, main, worktree),
git.commitLog(`${main}..${branch}`, worktree), git.commitLog(`${main}..${branch}`, worktree),
git.diffStat(`${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}.`) die(`No changes on branch "${branch}" compared to ${main}.`)
} }
const prompt = session.prompt ? escapeHtml(session.prompt) : "" const html = template
const diffJson = JSON.stringify(diff) .replaceAll("{{BRANCH}}", escapeHtml(branch))
.replace("{{PROMPT_SECTION}}", session.prompt ? `<p class="prompt">${escapeHtml(session.prompt)}</p>` : "")
const html = `<!DOCTYPE html> .replace("{{LOG_SECTION}}", log ? `<div class="meta-section"><h3>Commits</h3><pre>${escapeHtml(log)}</pre></div>` : "")
<html lang="en"> .replace("{{STAT_SECTION}}", stat ? `<div class="meta-section"><h3>Stats</h3><pre>${escapeHtml(stat)}</pre></div>` : "")
<head> .replace("{{DIFF_JSON}}", JSON.stringify(diff))
<meta charset="utf-8">
<title>${escapeHtml(branch)} sandlot diff</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; margin: 0; padding: 0; background: #0d1117; color: #e6edf3; }
.header { padding: 24px 32px; border-bottom: 1px solid #30363d; }
.header h1 { margin: 0 0 8px; font-size: 24px; font-weight: 600; }
.header h1 code { background: #1f2937; padding: 2px 8px; border-radius: 6px; font-size: 22px; }
.prompt { color: #8b949e; margin: 0 0 16px; font-style: italic; }
.meta { display: flex; gap: 32px; }
.meta-section h3 { margin: 0 0 6px; font-size: 13px; text-transform: uppercase; color: #8b949e; letter-spacing: 0.05em; }
.meta-section pre { margin: 0; font-size: 13px; line-height: 1.5; color: #c9d1d9; white-space: pre-wrap; }
.diff-container { padding: 16px 32px; }
/* Override diff2html for dark theme */
.d2h-wrapper { background: #0d1117; }
.d2h-file-header { background: #161b22; border-color: #30363d; color: #e6edf3; }
.d2h-file-wrapper { border-color: #30363d; margin-bottom: 16px; border-radius: 6px; overflow: hidden; }
.d2h-code-linenumber { background: #161b22; color: #8b949e; border-color: #30363d; }
.d2h-code-line { background: #0d1117; color: #e6edf3; }
.d2h-code-side-line { background: #0d1117; }
.d2h-del { background: rgba(248,81,73,0.1); }
.d2h-ins { background: rgba(63,185,80,0.1); }
.d2h-del .d2h-code-line-ctn { background: rgba(248,81,73,0.15); }
.d2h-ins .d2h-code-line-ctn { background: rgba(63,185,80,0.15); }
.d2h-code-line-ctn { color: #e6edf3; }
.d2h-info { background: #161b22; color: #8b949e; border-color: #30363d; }
.d2h-tag { background: #1f6feb; color: #fff; }
.d2h-file-stats-wrapper { display: flex; }
</style>
</head>
<body>
<div class="header">
<h1><code>${escapeHtml(branch)}</code></h1>
${prompt ? `<p class="prompt">${prompt}</p>` : ""}
<div class="meta">
${log ? `<div class="meta-section"><h3>Commits</h3><pre>${escapeHtml(log)}</pre></div>` : ""}
${stat ? `<div class="meta-section"><h3>Stats</h3><pre>${escapeHtml(stat)}</pre></div>` : ""}
</div>
</div>
<div class="diff-container" id="diff"></div>
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
<script>
const diffString = ${diffJson};
const targetElement = document.getElementById("diff");
const configuration = {
drawFileList: true,
matching: "lines",
outputFormat: "side-by-side",
highlight: true,
colorScheme: "dark",
};
const diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
diff2htmlUi.draw();
diff2htmlUi.highlightCode();
</script>
</body>
</html>`
const tmpPath = `/tmp/sandlot-browse-${branch}.html` const tmpPath = `/tmp/sandlot-browse-${branch}.html`
await Bun.write(tmpPath, html) await Bun.write(tmpPath, html)

View File

@ -219,8 +219,7 @@ export async function hasNewCommits(worktreePath: string): Promise<boolean> {
} }
/** Get the full unified diff of a branch vs main as a string. */ /** Get the full unified diff of a branch vs main as a string. */
export async function branchDiff(branch: string, cwd: string): Promise<string> { export async function branchDiff(branch: string, main: string, cwd: string): Promise<string> {
const main = await mainBranch(cwd)
const result = await $`git diff ${main}...${branch}`.cwd(cwd).nothrow().quiet() const result = await $`git diff ${main}...${branch}`.cwd(cwd).nothrow().quiet()
if (result.exitCode !== 0) return "" if (result.exitCode !== 0) return ""
return result.text() return result.text()