Compare commits
6 Commits
c6ef48384e
...
be277b7623
| Author | SHA1 | Date | |
|---|---|---|---|
| be277b7623 | |||
| 33f820f124 | |||
| 1d7f60b50c | |||
| 4c1530760c | |||
| 909189b745 | |||
| b8f7aea3b0 |
|
|
@ -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 webAction } from "./commands/web.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("web")
|
||||
.argument("<branch>", "branch name")
|
||||
.description("Open the branch diff in a web browser")
|
||||
.action(webAction)
|
||||
|
||||
program
|
||||
.command("save")
|
||||
.argument("<branch>", "branch name")
|
||||
|
|
|
|||
49
src/commands/diff.html
Normal file
49
src/commands/diff.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!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">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github-dark.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; }
|
||||
.d2h-file-wrapper { margin-bottom: 16px; border-radius: 6px; overflow: hidden; }
|
||||
.d2h-code-line-ctn { background: transparent; }
|
||||
</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/gh/highlightjs/cdn-release/build/highlight.min.js"></script>
|
||||
<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>
|
||||
|
|
@ -97,10 +97,10 @@ async function squashCommit(branch: string, cwd: string): Promise<void> {
|
|||
if (diff.trim()) {
|
||||
const gen = await vm.claudePipe(
|
||||
diff,
|
||||
"write a commit message for these changes following the 50/72 rule: subject line ≤50 chars, blank line, body wrapped at 72 chars. the body should briefly explain what changed and why. output only the message, no quotes or extra text.",
|
||||
"write a single-line commit message for these changes, max 50 characters. no body, no blank line, just the subject. output only the message, no quotes or extra text.",
|
||||
)
|
||||
if (gen.exitCode === 0 && gen.stdout.trim()) {
|
||||
await git.commit(gen.stdout.trim(), cwd)
|
||||
await git.commit(gen.stdout.trim().split("\n")[0], cwd)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ export async function saveChanges(worktree: string, branch: string, message?: st
|
|||
|
||||
const gen = await vm.claudePipe(
|
||||
diff,
|
||||
"write a commit message for these changes following the 50/72 rule: subject line ≤50 chars, blank line, body wrapped at 72 chars. the body should briefly explain what changed and why. output only the message, no quotes or extra text.",
|
||||
"write a single-line commit message for these changes, max 50 characters. no body, no blank line, just the subject. output only the message, no quotes or extra text.",
|
||||
)
|
||||
|
||||
if (gen.exitCode !== 0) {
|
||||
|
|
|
|||
57
src/commands/web.ts
Normal file
57
src/commands/web.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { $ } from "bun"
|
||||
import * as git from "../git.ts"
|
||||
import { die } from "../fmt.ts"
|
||||
import { requireSession } from "./helpers.ts"
|
||||
|
||||
const template = await Bun.file(new URL("diff.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, main, worktree),
|
||||
git.commitLog(`${main}..${branch}`, worktree),
|
||||
git.diffStat(`${main}...${branch}`, worktree),
|
||||
])
|
||||
|
||||
if (!diff.trim()) {
|
||||
die(`No changes on branch "${branch}" compared to ${main}.`)
|
||||
}
|
||||
|
||||
const diffJson = JSON.stringify(diff).replaceAll("<", "\\u003c")
|
||||
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>${formatStat(stat)}</pre></div>` : "")
|
||||
.replace("{{DIFF_JSON}}", () => diffJson)
|
||||
|
||||
const tmpPath = `/tmp/sandlot-${branch}.html`
|
||||
await Bun.write(tmpPath, html)
|
||||
await $`open ${tmpPath}`.nothrow().quiet()
|
||||
}
|
||||
|
||||
function formatStat(raw: string): string {
|
||||
// strip leading space from lines to match the unindented first line
|
||||
const trimmed = raw.split("\n").map(l => l.startsWith(" ") ? l.slice(1) : l).join("\n")
|
||||
const escaped = escapeHtml(trimmed)
|
||||
// colorize + and - in the bar portion (after the |)
|
||||
return escaped.split("\n").map(line => {
|
||||
const pipe = line.indexOf("|")
|
||||
if (pipe === -1) return line
|
||||
return line.slice(0, pipe + 1) +
|
||||
line.slice(pipe + 1)
|
||||
.replace(/\+/g, '<span style="color:#3fb950">+</span>')
|
||||
.replace(/-/g, '<span style="color:#f85149">-</span>')
|
||||
}).join("\n")
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ export async function commitLog(range: string, cwd: string): Promise<string> {
|
|||
|
||||
/** Get a diff stat summary for a revision range. Returns empty string on failure. */
|
||||
export async function diffStat(range: string, cwd: string): Promise<string> {
|
||||
const result = await $`git diff --stat ${range}`.cwd(cwd).nothrow().quiet()
|
||||
const result = await $`git diff --stat --stat-width=68 ${range}`.cwd(cwd).nothrow().quiet()
|
||||
if (result.exitCode !== 0) return ""
|
||||
return result.text().trim()
|
||||
}
|
||||
|
|
@ -218,6 +218,13 @@ export async function hasNewCommits(worktreePath: string): Promise<boolean> {
|
|||
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, main: string, cwd: string): Promise<string> {
|
||||
const result = await $`git diff --no-ext-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<string> {
|
||||
const dir = cwd ?? "."
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user