Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

compare.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

compare.tsxBlame182 lines · 1 contributor
79136bbClaude1/**
2 * Compare view — diff between two branches or commits.
3 * URL: /:owner/:repo/compare/:base...:head
4 */
5
6import { Hono } from "hono";
7import { join } from "path";
8import { Layout } from "../views/layout";
9import { RepoHeader, DiffView } from "../views/components";
10import { IssueNav } from "./issues";
11import {
12 listBranches,
13 listCommits,
14 repoExists,
15 getRepoPath,
16} from "../git/repository";
17import { softAuth } from "../middleware/auth";
18import type { AuthEnv } from "../middleware/auth";
19import type { GitDiffFile } from "../git/repository";
bb0f894Claude20import {
21 EmptyState,
22 Flex,
23 Text,
24} from "../views/ui";
79136bbClaude25
26const compare = new Hono<AuthEnv>();
27
28compare.use("*", softAuth);
29
30compare.get("/:owner/:repo/compare/:spec?", async (c) => {
31 const { owner, repo } = c.req.param();
32 const user = c.get("user");
33 const spec = c.req.param("spec");
34
35 if (!(await repoExists(owner, repo))) {
36 return c.html(
37 <Layout title="Not Found" user={user}>
bb0f894Claude38 <EmptyState title="Repository not found" />
79136bbClaude39 </Layout>,
40 404
41 );
42 }
43
44 const branches = await listBranches(owner, repo);
45
46 if (!spec || !spec.includes("...")) {
47 // Show compare picker
48 const defaultBase = branches.includes("main") ? "main" : branches[0] || "";
49 return c.html(
50 <Layout title={`Compare — ${owner}/${repo}`} user={user}>
51 <RepoHeader owner={owner} repo={repo} />
52 <IssueNav owner={owner} repo={repo} active="code" />
53 <h2 style="margin-bottom: 16px">Compare changes</h2>
54 <form
45e31d0Claude55 method="get"
79136bbClaude56 action={`/${owner}/${repo}/compare`}
57 >
bb0f894Claude58 <Flex gap={12} align="center" style="margin-bottom: 20px">
2e9136dClaude59 <select name="base" class="branch-selector" style="cursor: pointer">
bb0f894Claude60 {branches.map((b) => (
61 <option value={b} selected={b === defaultBase}>
62 {b}
63 </option>
64 ))}
2e9136dClaude65 </select>
bb0f894Claude66 <Text muted>...</Text>
2e9136dClaude67 <select name="head" class="branch-selector" style="cursor: pointer">
bb0f894Claude68 {branches.map((b) => (
69 <option value={b} selected={b !== defaultBase}>
70 {b}
71 </option>
72 ))}
2e9136dClaude73 </select>
74 <button
bb0f894Claude75 type="submit"
2e9136dClaude76 class="btn btn-primary"
77 onclick={`this.form.action='/${owner}/${repo}/compare/'+this.form.base.value+'...'+this.form.head.value; return true;`}
bb0f894Claude78 >
79 Compare
2e9136dClaude80 </button>
bb0f894Claude81 </Flex>
79136bbClaude82 </form>
83 </Layout>
84 );
85 }
86
87 const [base, head] = spec.split("...");
88
89 // Get diff
90 const repoDir = getRepoPath(owner, repo);
91 const proc = Bun.spawn(
92 ["git", "diff", `${base}...${head}`],
93 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
94 );
95 const raw = await new Response(proc.stdout).text();
96 await proc.exited;
97
98 // Get numstat
99 const statProc = Bun.spawn(
100 ["git", "diff", "--numstat", `${base}...${head}`],
101 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
102 );
103 const stat = await new Response(statProc.stdout).text();
104 await statProc.exited;
105
106 const files: GitDiffFile[] = stat
107 .trim()
108 .split("\n")
109 .filter(Boolean)
110 .map((line) => {
111 const [add, del, filePath] = line.split("\t");
112 return {
113 path: filePath,
114 status: "modified",
115 additions: add === "-" ? 0 : parseInt(add, 10),
116 deletions: del === "-" ? 0 : parseInt(del, 10),
117 patch: "",
118 };
119 });
120
121 // Get commits between
122 const logProc = Bun.spawn(
123 [
124 "git",
125 "log",
126 "--format=%H%x00%s%x00%an%x00%aI",
127 `${base}...${head}`,
128 ],
129 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
130 );
131 const logOutput = await new Response(logProc.stdout).text();
132 await logProc.exited;
133
134 const commitsBetween = logOutput
135 .trim()
136 .split("\n")
137 .filter(Boolean)
138 .map((line) => {
139 const [sha, msg, author, date] = line.split("\0");
140 return { sha, message: msg, author, date };
141 });
142
143 return c.html(
144 <Layout title={`${base}...${head} — ${owner}/${repo}`} user={user}>
145 <RepoHeader owner={owner} repo={repo} />
146 <IssueNav owner={owner} repo={repo} active="code" />
147 <h2 style="margin-bottom: 8px">
148 Comparing {base}...{head}
149 </h2>
bb0f894Claude150 <Text size={14} muted style="display:block;margin-bottom:20px">
79136bbClaude151 {commitsBetween.length} commit{commitsBetween.length !== 1 ? "s" : ""}
bb0f894Claude152 </Text>
79136bbClaude153
154 {commitsBetween.length > 0 && (
155 <div class="commit-list" style="margin-bottom: 24px">
156 {commitsBetween.map((cm) => (
157 <div class="commit-item">
158 <div>
159 <div class="commit-message">
160 <a href={`/${owner}/${repo}/commit/${cm.sha}`}>
161 {cm.message}
162 </a>
163 </div>
164 <div class="commit-meta">{cm.author}</div>
165 </div>
166 <a
167 href={`/${owner}/${repo}/commit/${cm.sha}`}
168 class="commit-sha"
169 >
170 {cm.sha.slice(0, 7)}
171 </a>
172 </div>
173 ))}
174 </div>
175 )}
176
177 <DiffView raw={raw} files={files} />
178 </Layout>
179 );
180});
181
182export default compare;