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 { 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 = `<!DOCTYPE html>
<html lang="en">
<head>
<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 html = template
.replaceAll("{{BRANCH}}", escapeHtml(branch))
.replace("{{PROMPT_SECTION}}", session.prompt ? `<p class="prompt">${escapeHtml(session.prompt)}</p>` : "")
.replace("{{LOG_SECTION}}", log ? `<div class="meta-section"><h3>Commits</h3><pre>${escapeHtml(log)}</pre></div>` : "")
.replace("{{STAT_SECTION}}", stat ? `<div class="meta-section"><h3>Stats</h3><pre>${escapeHtml(stat)}</pre></div>` : "")
.replace("{{DIFF_JSON}}", JSON.stringify(diff))
const tmpPath = `/tmp/sandlot-browse-${branch}.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. */
export async function branchDiff(branch: string, cwd: string): Promise<string> {
const main = await mainBranch(cwd)
export async function branchDiff(branch: string, main: string, cwd: string): Promise<string> {
const result = await $`git diff ${main}...${branch}`.cwd(cwd).nothrow().quiet()
if (result.exitCode !== 0) return ""
return result.text()