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.tsxBlame749 lines · 1 contributor
79136bbClaude1/**
2 * Compare view — diff between two branches or commits.
3 * URL: /:owner/:repo/compare/:base...:head
23b3ccaClaude4 *
5 * The picker view and the comparison view both carry the 2026 polish:
6 * hero card with gradient hairline strip + display title, polished
7 * branch-selector cards with a prominent arrow between them, +/- diff
8 * stats with tabular numerals, modernized commit rows (SHA pill +
9 * monospace message + author + age), and a gradient "Create pull
10 * request" CTA. All styling is scoped via `.compare-*` class prefixes
11 * inside an inline <style> block so no other surface is touched.
12 *
13 * No business logic was changed in this polish pass — branch listing,
14 * diff fetching, commit enumeration, and the PR-creation redirect URL
15 * (`/:owner/:repo/pulls/new?base=…&head=…`) are preserved exactly.
79136bbClaude16 */
17
18import { Hono } from "hono";
19import { Layout } from "../views/layout";
ea9ed4cClaude20import { RepoHeader } from "../views/components";
21import { DiffView } from "../views/diff-view";
79136bbClaude22import { IssueNav } from "./issues";
23import {
24 listBranches,
25 repoExists,
26 getRepoPath,
27} from "../git/repository";
28import { softAuth } from "../middleware/auth";
29import type { AuthEnv } from "../middleware/auth";
30import type { GitDiffFile } from "../git/repository";
23b3ccaClaude31import { EmptyState } from "../views/ui";
79136bbClaude32
33const compare = new Hono<AuthEnv>();
34
35compare.use("*", softAuth);
36
23b3ccaClaude37/* ──────────────────────────────────────────────────────────────────────
38 * Inline CSS scoped via `.compare-*` so other surfaces remain untouched.
39 * Tokens come from layout.tsx `:root` for light/dark consistency.
40 * ──────────────────────────────────────────────────────────────────── */
41const COMPARE_STYLES = `
42 .compare-hero {
43 position: relative;
44 margin: 0 0 var(--space-5);
45 padding: 22px 26px 24px;
46 background: var(--bg-elevated);
47 border: 1px solid var(--border);
48 border-radius: 16px;
49 overflow: hidden;
50 }
51 .compare-hero::before {
52 content: '';
53 position: absolute; top: 0; left: 0; right: 0;
54 height: 2px;
55 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
56 opacity: 0.7;
57 pointer-events: none;
58 }
59 .compare-hero-inner { position: relative; z-index: 1; }
60 .compare-eyebrow {
61 font-size: 12px;
62 font-family: var(--font-mono);
63 color: var(--text-muted);
64 letter-spacing: 0.1em;
65 text-transform: uppercase;
66 font-weight: 600;
67 margin-bottom: 8px;
68 }
69 .compare-eyebrow strong { color: var(--accent); font-weight: 600; }
70 .compare-title {
71 font-family: var(--font-display);
72 font-size: clamp(26px, 3.4vw, 34px);
73 font-weight: 800;
74 letter-spacing: -0.025em;
75 line-height: 1.06;
76 margin: 0 0 8px;
77 color: var(--text-strong);
78 }
79 .compare-title .gradient-text {
80 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
81 -webkit-background-clip: text;
82 background-clip: text;
83 -webkit-text-fill-color: transparent;
84 color: transparent;
85 }
86 .compare-sub {
87 font-size: 14.5px;
88 color: var(--text-muted);
89 line-height: 1.5;
90 margin: 0;
91 max-width: 640px;
92 }
93
94 /* Branch selector — base ← head */
95 .compare-branches {
96 display: flex;
97 align-items: stretch;
98 gap: 12px;
99 flex-wrap: wrap;
100 margin: 0 0 var(--space-4);
101 }
102 .compare-branch-card {
103 flex: 1 1 240px;
104 min-width: 220px;
105 display: flex;
106 flex-direction: column;
107 gap: 6px;
108 padding: 12px 14px;
109 background: var(--bg-elevated);
110 border: 1px solid var(--border);
111 border-radius: 12px;
112 transition: border-color 140ms ease, box-shadow 160ms ease;
113 }
114 .compare-branch-card:focus-within {
115 border-color: rgba(140,109,255,0.55);
116 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
117 }
118 .compare-branch-label {
119 display: flex;
120 align-items: center;
121 gap: 6px;
122 font-size: 11px;
123 font-family: var(--font-mono);
124 letter-spacing: 0.12em;
125 text-transform: uppercase;
126 color: var(--text-muted);
127 font-weight: 600;
128 }
129 .compare-branch-label .compare-branch-icon {
130 color: var(--text-faint);
131 font-size: 12px;
132 }
133 .compare-branch-card.is-base .compare-branch-label strong { color: #ff9d76; }
134 .compare-branch-card.is-head .compare-branch-label strong { color: #b69dff; }
135 .compare-branch-select {
136 appearance: none;
137 -webkit-appearance: none;
138 width: 100%;
139 padding: 8px 32px 8px 10px;
140 background: var(--bg-secondary);
141 border: 1px solid var(--border);
142 border-radius: 8px;
143 color: var(--text-strong);
144 font-size: 13.5px;
145 font-family: var(--font-mono);
146 line-height: 1.4;
147 cursor: pointer;
148 background-image: linear-gradient(45deg, transparent 50%, var(--text-muted) 50%), linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
149 background-position: calc(100% - 16px) 14px, calc(100% - 11px) 14px;
150 background-size: 5px 5px;
151 background-repeat: no-repeat;
152 transition: border-color 140ms ease, background-color 140ms ease;
153 }
154 .compare-branch-select:hover { border-color: var(--border-strong); }
155 .compare-branch-select:focus { outline: none; border-color: rgba(140,109,255,0.55); }
156 .compare-branch-arrow {
157 flex: 0 0 auto;
158 align-self: center;
159 display: inline-flex; align-items: center; justify-content: center;
160 width: 36px; height: 36px;
161 border-radius: 9999px;
162 background: linear-gradient(135deg, rgba(140,109,255,0.18), rgba(54,197,214,0.16));
163 border: 1px solid rgba(140,109,255,0.30);
164 color: var(--text-strong);
165 font-size: 16px;
166 line-height: 1;
167 font-weight: 700;
168 box-shadow: 0 0 18px -8px rgba(140,109,255,0.45);
169 }
170 @media (max-width: 720px) {
171 .compare-branch-arrow { align-self: flex-start; transform: rotate(90deg); }
172 }
173 .compare-form-actions {
174 display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
175 }
176 .compare-cta {
177 display: inline-flex; align-items: center; gap: 8px;
178 padding: 10px 18px;
179 border-radius: 10px;
180 font-size: 13.5px;
181 font-weight: 600;
182 color: #fff;
183 background: linear-gradient(135deg, #8c6dff 0%, #6f5be8 60%, #36c5d6 140%);
184 border: 1px solid rgba(140,109,255,0.55);
185 box-shadow: 0 6px 18px -8px rgba(140,109,255,0.55);
186 text-decoration: none;
187 cursor: pointer;
188 transition: transform 120ms ease, box-shadow 160ms ease, filter 160ms ease;
189 }
190 .compare-cta:hover {
191 transform: translateY(-1px);
192 box-shadow: 0 10px 22px -6px rgba(140,109,255,0.6);
193 color: #fff;
194 }
195 .compare-cta.is-disabled,
196 .compare-cta[aria-disabled="true"] {
197 opacity: 0.55;
198 cursor: not-allowed;
199 filter: grayscale(0.4);
200 transform: none;
201 box-shadow: none;
202 }
203 .compare-cta-secondary {
204 display: inline-flex; align-items: center; gap: 6px;
205 padding: 9px 14px;
206 border-radius: 10px;
207 font-size: 13px;
208 font-weight: 500;
209 color: var(--text);
210 background: var(--bg-secondary);
211 border: 1px solid var(--border);
212 text-decoration: none;
213 transition: border-color 140ms ease, color 140ms ease;
214 }
215 .compare-cta-secondary:hover {
216 border-color: var(--border-strong);
217 color: var(--text-strong);
218 }
219
220 /* Summary stats — commits ahead / behind */
221 .compare-stats {
222 display: flex;
223 flex-wrap: wrap;
224 gap: 8px;
225 margin: 0 0 var(--space-4);
226 }
227 .compare-stat-badge {
228 display: inline-flex; align-items: center; gap: 6px;
229 padding: 6px 12px;
230 border-radius: 9999px;
231 font-size: 12.5px;
232 font-weight: 600;
233 font-family: var(--font-mono);
234 font-variant-numeric: tabular-nums;
235 border: 1px solid var(--border);
236 background: var(--bg-secondary);
237 color: var(--text);
238 }
239 .compare-stat-badge .compare-stat-count { color: var(--text-strong); }
240 .compare-stat-badge.is-ahead {
241 color: var(--green);
242 border-color: rgba(52,211,153,0.32);
243 background: rgba(52,211,153,0.10);
244 }
245 .compare-stat-badge.is-behind {
246 color: var(--red);
247 border-color: rgba(248,113,113,0.32);
248 background: rgba(248,113,113,0.10);
249 }
250 .compare-stat-badge.is-files {
251 color: var(--text-link);
252 border-color: rgba(140,109,255,0.32);
253 background: rgba(140,109,255,0.08);
254 }
255
256 /* Diff stats bar — +/- counts */
257 .compare-diffstats {
258 display: inline-flex; align-items: center; gap: 10px;
259 padding: 6px 12px;
260 border-radius: 9999px;
261 font-size: 12.5px;
262 font-family: var(--font-mono);
263 font-variant-numeric: tabular-nums;
264 background: var(--bg-secondary);
265 border: 1px solid var(--border);
266 }
267 .compare-diffstats .compare-add { color: var(--green); font-weight: 700; }
268 .compare-diffstats .compare-del { color: var(--red); font-weight: 700; }
269 .compare-diffstats .compare-files-c { color: var(--text-muted); }
270
271 /* Section heading row above commit list / diff */
272 .compare-section-head {
273 display: flex; align-items: center; justify-content: space-between;
274 gap: 12px;
275 margin: 0 0 12px;
276 flex-wrap: wrap;
277 }
278 .compare-section-title {
279 font-size: 11.5px;
280 font-family: var(--font-mono);
281 letter-spacing: 0.14em;
282 text-transform: uppercase;
283 color: var(--text-muted);
284 font-weight: 600;
285 margin: 0;
286 }
287
288 /* Commit list */
289 .compare-commits {
290 display: flex; flex-direction: column;
291 margin: 0 0 var(--space-5);
292 background: var(--bg-elevated);
293 border: 1px solid var(--border);
294 border-radius: 12px;
295 overflow: hidden;
296 }
297 .compare-commit {
298 display: flex; align-items: center; gap: 14px;
299 padding: 12px 16px;
300 border-bottom: 1px solid var(--border);
301 transition: background 120ms ease;
302 }
303 .compare-commit:last-child { border-bottom: none; }
304 .compare-commit:hover { background: var(--bg-hover); }
305 .compare-commit-body { flex: 1; min-width: 0; }
306 .compare-commit-msg {
307 font-size: 14px;
308 color: var(--text-strong);
309 line-height: 1.4;
310 margin: 0 0 4px;
311 overflow: hidden;
312 text-overflow: ellipsis;
313 white-space: nowrap;
314 }
315 .compare-commit-msg a {
316 color: var(--text-strong);
317 text-decoration: none;
318 font-weight: 500;
319 }
320 .compare-commit-msg a:hover { color: var(--accent); }
321 .compare-commit-meta {
322 font-size: 12px;
323 color: var(--text-muted);
324 display: inline-flex; align-items: center; gap: 8px;
325 }
326 .compare-commit-meta .compare-commit-author {
327 color: var(--text);
328 font-weight: 500;
329 }
330 .compare-commit-meta .compare-commit-dot {
331 color: var(--text-faint);
332 }
333 .compare-commit-sha {
334 flex: 0 0 auto;
335 display: inline-flex; align-items: center;
336 padding: 4px 10px;
337 border-radius: 9999px;
338 background: var(--bg-secondary);
339 border: 1px solid var(--border);
340 color: var(--text);
341 font-family: var(--font-mono);
342 font-size: 12px;
343 text-decoration: none;
344 transition: border-color 140ms ease, color 140ms ease;
345 }
346 .compare-commit-sha:hover {
347 border-color: rgba(140,109,255,0.45);
348 color: var(--text-link);
349 }
350
351 /* Empty / identical state */
352 .compare-empty {
353 padding: 36px 28px;
354 text-align: center;
355 border: 1px dashed var(--border);
356 border-radius: 12px;
357 background: var(--bg-secondary);
358 color: var(--text-muted);
359 margin: 0 0 var(--space-5);
360 }
361 .compare-empty strong {
362 display: block;
363 color: var(--text-strong);
364 font-size: 15px;
365 margin-bottom: 6px;
366 }
367 .compare-empty p {
368 margin: 0 auto;
369 max-width: 460px;
370 font-size: 13.5px;
371 line-height: 1.55;
372 }
373 .compare-empty .compare-empty-icon {
374 display: inline-flex; align-items: center; justify-content: center;
375 width: 44px; height: 44px;
376 border-radius: 9999px;
377 background: linear-gradient(135deg, rgba(140,109,255,0.16), rgba(54,197,214,0.14));
378 border: 1px solid rgba(140,109,255,0.28);
379 color: var(--text-strong);
380 font-size: 18px;
381 margin: 0 auto 12px;
382 }
383
384 @media (max-width: 720px) {
385 .compare-hero { padding: 18px 18px 20px; }
386 .compare-form-actions { width: 100%; }
387 .compare-cta, .compare-cta-secondary { flex: 1 1 auto; justify-content: center; }
388 }
389`;
390
391/** Format an ISO date as a short relative string ("3h", "2d", "5w"). */
392function relTime(iso: string): string {
393 const t = Date.parse(iso);
394 if (!Number.isFinite(t)) return "";
395 const diff = Math.max(0, Date.now() - t);
396 const s = Math.floor(diff / 1000);
397 if (s < 60) return `${s}s ago`;
398 const m = Math.floor(s / 60);
399 if (m < 60) return `${m}m ago`;
400 const h = Math.floor(m / 60);
401 if (h < 24) return `${h}h ago`;
402 const d = Math.floor(h / 24);
403 if (d < 7) return `${d}d ago`;
404 const w = Math.floor(d / 7);
405 if (w < 5) return `${w}w ago`;
406 const mo = Math.floor(d / 30);
407 if (mo < 12) return `${mo}mo ago`;
408 const y = Math.floor(d / 365);
409 return `${y}y ago`;
410}
411
79136bbClaude412compare.get("/:owner/:repo/compare/:spec?", async (c) => {
413 const { owner, repo } = c.req.param();
414 const user = c.get("user");
415 const spec = c.req.param("spec");
416
417 if (!(await repoExists(owner, repo))) {
418 return c.html(
419 <Layout title="Not Found" user={user}>
bb0f894Claude420 <EmptyState title="Repository not found" />
79136bbClaude421 </Layout>,
422 404
423 );
424 }
425
426 const branches = await listBranches(owner, repo);
427
428 if (!spec || !spec.includes("...")) {
429 // Show compare picker
430 const defaultBase = branches.includes("main") ? "main" : branches[0] || "";
23b3ccaClaude431 const defaultHead =
432 branches.find((b) => b !== defaultBase) || defaultBase;
79136bbClaude433 return c.html(
434 <Layout title={`Compare — ${owner}/${repo}`} user={user}>
435 <RepoHeader owner={owner} repo={repo} />
436 <IssueNav owner={owner} repo={repo} active="code" />
23b3ccaClaude437 <style dangerouslySetInnerHTML={{ __html: COMPARE_STYLES }} />
438
439 <section class="compare-hero">
440 <div class="compare-hero-inner">
441 <div class="compare-eyebrow">
442 Compare changes · <strong>{owner}/{repo}</strong>
443 </div>
444 <h1 class="compare-title">
445 <span class="gradient-text">Compare</span> branches.
446 </h1>
447 <p class="compare-sub">
448 Pick a base and a head branch to see exactly what changed.
449 Open a pull request when the diff looks right.
450 </p>
451 </div>
452 </section>
453
454 <form method="get" action={`/${owner}/${repo}/compare`}>
455 <div class="compare-branches">
456 <div class="compare-branch-card is-base">
457 <label class="compare-branch-label" for="compare-base">
458 <span class="compare-branch-icon">⇤</span>
459 <strong>Base</strong>
460 <span style="color:var(--text-faint);font-weight:500">
461 · what you merge into
462 </span>
463 </label>
464 <select
465 id="compare-base"
466 name="base"
467 class="compare-branch-select"
468 >
469 {branches.map((b) => (
470 <option value={b} selected={b === defaultBase}>
471 {b}
472 </option>
473 ))}
474 </select>
475 </div>
476
477 <div class="compare-branch-arrow" aria-hidden="true">←</div>
478
479 <div class="compare-branch-card is-head">
480 <label class="compare-branch-label" for="compare-head">
481 <span class="compare-branch-icon">⇥</span>
482 <strong>Head</strong>
483 <span style="color:var(--text-faint);font-weight:500">
484 · what you want to merge
485 </span>
486 </label>
487 <select
488 id="compare-head"
489 name="head"
490 class="compare-branch-select"
491 >
492 {branches.map((b) => (
493 <option value={b} selected={b === defaultHead}>
494 {b}
495 </option>
496 ))}
497 </select>
498 </div>
499 </div>
500
501 <div class="compare-form-actions">
2e9136dClaude502 <button
bb0f894Claude503 type="submit"
23b3ccaClaude504 class="compare-cta"
2e9136dClaude505 onclick={`this.form.action='/${owner}/${repo}/compare/'+this.form.base.value+'...'+this.form.head.value; return true;`}
bb0f894Claude506 >
23b3ccaClaude507 Compare branches →
2e9136dClaude508 </button>
23b3ccaClaude509 <a
510 href={`/${owner}/${repo}`}
511 class="compare-cta-secondary"
512 >
513 Cancel
514 </a>
515 </div>
79136bbClaude516 </form>
517 </Layout>
518 );
519 }
520
521 const [base, head] = spec.split("...");
522
523 // Get diff
524 const repoDir = getRepoPath(owner, repo);
525 const proc = Bun.spawn(
526 ["git", "diff", `${base}...${head}`],
527 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
528 );
529 const raw = await new Response(proc.stdout).text();
530 await proc.exited;
531
532 // Get numstat
533 const statProc = Bun.spawn(
534 ["git", "diff", "--numstat", `${base}...${head}`],
535 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
536 );
537 const stat = await new Response(statProc.stdout).text();
538 await statProc.exited;
539
540 const files: GitDiffFile[] = stat
541 .trim()
542 .split("\n")
543 .filter(Boolean)
544 .map((line) => {
545 const [add, del, filePath] = line.split("\t");
546 return {
547 path: filePath,
548 status: "modified",
549 additions: add === "-" ? 0 : parseInt(add, 10),
550 deletions: del === "-" ? 0 : parseInt(del, 10),
551 patch: "",
552 };
553 });
554
555 // Get commits between
556 const logProc = Bun.spawn(
557 [
558 "git",
559 "log",
560 "--format=%H%x00%s%x00%an%x00%aI",
561 `${base}...${head}`,
562 ],
563 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
564 );
565 const logOutput = await new Response(logProc.stdout).text();
566 await logProc.exited;
567
568 const commitsBetween = logOutput
569 .trim()
570 .split("\n")
571 .filter(Boolean)
572 .map((line) => {
573 const [sha, msg, author, date] = line.split("\0");
574 return { sha, message: msg, author, date };
575 });
576
23b3ccaClaude577 // Aggregate diff stats for the badge bar.
578 const totalAdditions = files.reduce((s, f) => s + f.additions, 0);
579 const totalDeletions = files.reduce((s, f) => s + f.deletions, 0);
580 const isIdentical = base === head;
581 const hasChanges = commitsBetween.length > 0 || files.length > 0;
582
79136bbClaude583 return c.html(
584 <Layout title={`${base}...${head} — ${owner}/${repo}`} user={user}>
585 <RepoHeader owner={owner} repo={repo} />
586 <IssueNav owner={owner} repo={repo} active="code" />
23b3ccaClaude587 <style dangerouslySetInnerHTML={{ __html: COMPARE_STYLES }} />
588
589 <section class="compare-hero">
590 <div class="compare-hero-inner">
591 <div class="compare-eyebrow">
592 Comparing · <strong>{owner}/{repo}</strong>
593 </div>
594 <h1 class="compare-title">
595 {hasChanges ? (
596 <>
597 <span class="gradient-text">
598 {commitsBetween.length} commit
599 {commitsBetween.length !== 1 ? "s" : ""}
600 </span>{" "}
601 ahead.
602 </>
603 ) : isIdentical ? (
604 <>
605 <span class="gradient-text">Nothing</span> to compare.
606 </>
607 ) : (
608 <>
609 <span class="gradient-text">No changes</span> between branches.
610 </>
611 )}
612 </h1>
613 <p class="compare-sub">
614 <span style="font-family:var(--font-mono);color:var(--text)">
615 {base}
616 </span>{" "}
617 <span style="color:var(--text-faint)">←</span>{" "}
618 <span style="font-family:var(--font-mono);color:var(--text)">
619 {head}
620 </span>
621 {hasChanges
622 ? " — review the diff below, then open a pull request when it looks right."
623 : isIdentical
624 ? " — base and head point to the same ref. Pick different branches to see a diff."
625 : " — these branches diverge but produce no diff. Nothing to merge."}
626 </p>
627 </div>
628 </section>
629
630 {/* Summary stats row */}
631 {hasChanges && (
632 <div class="compare-stats">
633 <span class="compare-stat-badge is-ahead">
634 <span class="compare-stat-count">+{commitsBetween.length}</span>
635 commit{commitsBetween.length !== 1 ? "s" : ""} ahead
636 </span>
637 <span class="compare-stat-badge is-files">
638 <span class="compare-stat-count">{files.length}</span>
639 file{files.length !== 1 ? "s" : ""} changed
640 </span>
641 <span class="compare-diffstats">
642 <span class="compare-add">+{totalAdditions}</span>
643 <span class="compare-del">−{totalDeletions}</span>
644 <span class="compare-files-c">lines</span>
645 </span>
646 <a
647 href={`/${owner}/${repo}/pulls/new?base=${encodeURIComponent(base)}&head=${encodeURIComponent(head)}`}
648 class="compare-cta"
649 style="margin-left:auto"
650 >
651 Create pull request →
652 </a>
653 </div>
654 )}
655
656 {!hasChanges && (
657 <div class="compare-empty">
658 <div class="compare-empty-icon">≡</div>
659 <strong>
660 {isIdentical
661 ? "Nothing to compare."
662 : "No diff between these branches."}
663 </strong>
664 <p>
665 {isIdentical
666 ? "Base and head are the same branch. Push some commits to a feature branch, then come back here to compare and open a PR."
667 : "These branches diverge but produce no file changes. There's nothing to merge."}
668 </p>
669 <div
670 class="compare-form-actions"
671 style="justify-content:center;margin-top:16px"
672 >
673 <a
674 href={`/${owner}/${repo}/compare`}
675 class="compare-cta-secondary"
676 >
677 ← Pick different branches
678 </a>
679 <span
680 class="compare-cta is-disabled"
681 aria-disabled="true"
682 title="Nothing to compare yet"
683 >
684 Create pull request →
685 </span>
686 </div>
687 </div>
688 )}
79136bbClaude689
690 {commitsBetween.length > 0 && (
23b3ccaClaude691 <>
692 <div class="compare-section-head">
693 <h2 class="compare-section-title">Commits in this comparison</h2>
694 <span class="compare-stat-badge">
695 <span class="compare-stat-count">{commitsBetween.length}</span>
696 total
697 </span>
698 </div>
699 <div class="compare-commits">
700 {commitsBetween.map((cm) => (
701 <div class="compare-commit">
702 <div class="compare-commit-body">
703 <div class="compare-commit-msg">
704 <a href={`/${owner}/${repo}/commit/${cm.sha}`}>
705 {cm.message}
706 </a>
707 </div>
708 <div class="compare-commit-meta">
709 <span class="compare-commit-author">{cm.author}</span>
710 <span class="compare-commit-dot">·</span>
711 <span>{relTime(cm.date)}</span>
712 </div>
79136bbClaude713 </div>
23b3ccaClaude714 <a
715 href={`/${owner}/${repo}/commit/${cm.sha}`}
716 class="compare-commit-sha"
717 >
718 {cm.sha.slice(0, 7)}
719 </a>
79136bbClaude720 </div>
23b3ccaClaude721 ))}
722 </div>
723 </>
79136bbClaude724 )}
725
23b3ccaClaude726 {files.length > 0 && (
727 <>
728 <div class="compare-section-head">
729 <h2 class="compare-section-title">File changes</h2>
730 <span class="compare-diffstats">
731 <span class="compare-add">+{totalAdditions}</span>
732 <span class="compare-del">−{totalDeletions}</span>
733 <span class="compare-files-c">
734 across {files.length} file{files.length !== 1 ? "s" : ""}
735 </span>
736 </span>
737 </div>
ea9ed4cClaude738 <DiffView
739 raw={raw}
740 files={files}
741 viewFileBase={`/${owner}/${repo}/blob/${head}`}
742 />
23b3ccaClaude743 </>
744 )}
79136bbClaude745 </Layout>
746 );
747});
748
749export default compare;