This commit is contained in:
2026-06-27 23:08:19 +02:00
parent 25700821d1
commit 5b44a65e51
22 changed files with 3388 additions and 3538 deletions
+143
View File
@@ -0,0 +1,143 @@
<script lang="ts">
import { ArrowRight, X } from "@lucide/svelte";
import type { GitCommitComparison, GitDiffFile, FileStatusKind } from "../types";
type DiffLineKind = "meta" | "hunk" | "add" | "del" | "context";
interface Props {
comparison: GitCommitComparison;
selectedDiffPath: string;
isBusy: boolean;
onClose: () => void;
onSelectFile: (file: GitDiffFile) => void;
}
let {
comparison,
selectedDiffPath = "",
isBusy = false,
onClose = () => {},
onSelectFile = () => {},
}: Props = $props();
function statusLabel(kind: FileStatusKind): string {
return kind;
}
function displayDiffFile(file: GitDiffFile): string {
return file.old_path ? `${file.old_path} -> ${file.path}` : file.path;
}
function diffLineKind(line: string): DiffLineKind {
if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("--- ") ||
line.startsWith("+++ ") || line.startsWith("new file") || line.startsWith("deleted file") ||
line.startsWith("rename ") || line.startsWith("similarity ")) return "meta";
if (line.startsWith("@@")) return "hunk";
if (line.startsWith("+")) return "add";
if (line.startsWith("-")) return "del";
return "context";
}
function diffLines(patch: string): { kind: DiffLineKind; text: string }[] {
return patch.replace(/\n$/, "").split("\n").map((text) => ({ kind: diffLineKind(text), text }));
}
function stripDiffPathPrefix(value: string): string {
const trimmed = value.trim();
if (trimmed === "/dev/null") return trimmed;
if (trimmed.startsWith("a/") || trimmed.startsWith("b/")) return trimmed.slice(2);
return trimmed;
}
function patchFilePath(segment: string): string {
let plusPath = "";
let minusPath = "";
for (const line of segment.split("\n")) {
if (line.startsWith("+++ ")) plusPath = stripDiffPathPrefix(line.slice(4));
else if (line.startsWith("--- ")) minusPath = stripDiffPathPrefix(line.slice(4));
else if (line.startsWith("@@")) break;
}
return (plusPath && plusPath !== "/dev/null") ? plusPath : minusPath;
}
function buildDiffByPath(patch: string): Map<string, string> {
const map = new Map<string, string>();
if (!patch.trim()) return map;
const segments: string[] = [];
let current: string[] = [];
for (const line of patch.split("\n")) {
if (line.startsWith("diff --git ") && current.length > 0) {
segments.push(current.join("\n"));
current = [];
}
current.push(line);
}
if (current.length > 0) segments.push(current.join("\n"));
for (const segment of segments) {
const path = patchFilePath(segment);
if (path) map.set(path, segment);
}
return map;
}
let diffByPath = $derived(buildDiffByPath(comparison.patch));
let selectedDiffFile = $derived(comparison.files.find((f) => f.path === selectedDiffPath) ?? null);
let selectedDiffPatch = $derived(selectedDiffFile ? (diffByPath.get(selectedDiffFile.path) ?? "") : "");
</script>
<div
class="dialog-backdrop"
role="presentation"
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<div class="dialog" role="dialog" aria-modal="true" aria-label="Commit comparison" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Compare</span>
<h2 class="dialog-range">
<span class="hash">{comparison.from_short}</span>
<ArrowRight size={16} aria-hidden="true" />
<span class="hash">{comparison.to_short}</span>
</h2>
</div>
<button class="dialog-close" type="button" onclick={onClose} title="Close">
<X size={18} aria-hidden="true" />
</button>
</header>
{#if comparison.files.length === 0}
<div class="blank-state">No differences to show — these versions are identical.</div>
{:else}
<div class="dialog-body">
<aside class="dialog-files" aria-label="Changed files">
{#each comparison.files as file (`${file.old_path ?? ""}:${file.path}`)}
<button
class="dialog-file-row"
class:active={selectedDiffPath === file.path}
type="button"
onclick={() => onSelectFile(file)}
title={displayDiffFile(file)}
>
<span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span>
<strong>{displayDiffFile(file)}</strong>
<span class="diff-counts">
<span class="adds">+{file.additions}</span>
<span class="dels">-{file.deletions}</span>
</span>
</button>
{/each}
</aside>
<div class="dialog-diff" aria-label="File diff">
{#if !selectedDiffFile}
<div class="blank-state">Select a file to see its changes.</div>
{:else if selectedDiffPatch.trim().length === 0}
<div class="blank-state">No textual changes for this file.</div>
{:else}
<pre class="diff-view" aria-label="Unified diff">{#each diffLines(selectedDiffPatch) as line}<span class={`diff-line ${line.kind}`}>{line.text || " "}</span>{/each}</pre>
{/if}
</div>
</div>
{/if}
</div>
</div>