add browse command to open branch diff in web browser
This commit is contained in:
parent
e456c95a21
commit
b8f7aea3b0
|
|
@ -16,6 +16,7 @@ import { action as rebaseAction } from "./commands/rebase.ts"
|
||||||
import { action as saveAction } from "./commands/save.ts"
|
import { action as saveAction } from "./commands/save.ts"
|
||||||
import { action as diffAction } from "./commands/diff.ts"
|
import { action as diffAction } from "./commands/diff.ts"
|
||||||
import { action as showAction } from "./commands/show.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 logAction } from "./commands/log.ts"
|
||||||
import { action as dirAction } from "./commands/dir.ts"
|
import { action as dirAction } from "./commands/dir.ts"
|
||||||
import { action as editAction } from "./commands/edit.ts"
|
import { action as editAction } from "./commands/edit.ts"
|
||||||
|
|
@ -102,6 +103,12 @@ program
|
||||||
.description("Show the prompt and full diff for a branch")
|
.description("Show the prompt and full diff for a branch")
|
||||||
.action(showAction)
|
.action(showAction)
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("browse")
|
||||||
|
.argument("<branch>", "branch name")
|
||||||
|
.description("Open the branch diff in a web browser")
|
||||||
|
.action(browseAction)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("save")
|
.command("save")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
|
|
|
||||||
96
src/commands/browse.ts
Normal file
96
src/commands/browse.ts
Normal file
|
|
@ -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 = `<!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 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, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
}
|
||||||
|
|
@ -218,6 +218,14 @@ export async function hasNewCommits(worktreePath: string): Promise<boolean> {
|
||||||
return parseInt(result.text().trim(), 10) > 0
|
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<string> {
|
||||||
|
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). */
|
/** Detect the main branch name (main or master). */
|
||||||
export async function mainBranch(cwd?: string): Promise<string> {
|
export async function mainBranch(cwd?: string): Promise<string> {
|
||||||
const dir = cwd ?? "."
|
const dir = cwd ?? "."
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user