Introduce a new dialog that allows users to view file changes as individual hunks and selectively stage, unstage, or discard them. This provides more granular control over modifications, similar to `git add -p`. Enhance branch panel usability by allowing double-click to checkout a branch.
166 lines
6.0 KiB
Svelte
166 lines
6.0 KiB
Svelte
<script lang="ts">
|
|
import { Check, FileDiff, RotateCcw, Undo2 } from "@lucide/svelte";
|
|
import type { FileStatusKind, GitFileStatus, GitStatus } from "../types";
|
|
|
|
interface Props {
|
|
changedFiles: GitFileStatus[];
|
|
stagedCount: number;
|
|
unstagedCount: number;
|
|
hasRepository: boolean;
|
|
isBusy: boolean;
|
|
status: GitStatus | null;
|
|
onStage: (file: GitFileStatus) => void;
|
|
onUnstage: (file: GitFileStatus) => void;
|
|
onDiscard: (file: GitFileStatus, staged: boolean) => void;
|
|
onPatch: (file: GitFileStatus, staged: boolean) => void;
|
|
onStageAll: () => void;
|
|
onUnstageAll: () => void;
|
|
}
|
|
|
|
let {
|
|
changedFiles = [],
|
|
stagedCount = 0,
|
|
unstagedCount = 0,
|
|
hasRepository = false,
|
|
isBusy = false,
|
|
status = null,
|
|
onStage = () => {},
|
|
onUnstage = () => {},
|
|
onDiscard = () => {},
|
|
onPatch = () => {},
|
|
onStageAll = () => {},
|
|
onUnstageAll = () => {},
|
|
}: Props = $props();
|
|
|
|
function statusLabel(kind: FileStatusKind | null): string {
|
|
return kind ?? "none";
|
|
}
|
|
|
|
function displayPath(file: GitFileStatus): string {
|
|
return file.old_path ? `${file.old_path} -> ${file.path}` : file.path;
|
|
}
|
|
|
|
function baseName(path: string): string {
|
|
return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
|
|
}
|
|
|
|
function fileName(file: GitFileStatus): string {
|
|
return file.old_path ? `${baseName(file.old_path)} -> ${baseName(file.path)}` : baseName(file.path);
|
|
}
|
|
|
|
function canPatch(kind: FileStatusKind | null): boolean {
|
|
return kind === "modified";
|
|
}
|
|
|
|
let hasUnstaged = $derived(changedFiles.some((f) => f.unstaged !== null));
|
|
let hasStaged = $derived(changedFiles.some((f) => f.staged !== null));
|
|
</script>
|
|
|
|
<section class="panel grid grid-rows-[auto_auto_1fr] overflow-hidden" aria-label="Working tree status">
|
|
<div class="section-head">
|
|
<div>
|
|
<span class="eyebrow">Working tree</span>
|
|
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Status</h2>
|
|
</div>
|
|
<div class="flex items-center gap-1.5">
|
|
<span class="pill pill-count">{stagedCount} staged</span>
|
|
<span class="pill pill-count">{unstagedCount} unstaged</span>
|
|
</div>
|
|
</div>
|
|
|
|
{#if hasRepository && changedFiles.length > 0}
|
|
<div class="status-toolbar">
|
|
<button
|
|
class="btn-sm"
|
|
type="button"
|
|
onclick={onStageAll}
|
|
disabled={isBusy || !hasUnstaged}
|
|
title="Stage all unstaged files"
|
|
>
|
|
<Check size={14} aria-hidden="true" />
|
|
Stage all
|
|
</button>
|
|
<button
|
|
class="btn-sm"
|
|
type="button"
|
|
onclick={onUnstageAll}
|
|
disabled={isBusy || !hasStaged}
|
|
title="Unstage all staged files"
|
|
>
|
|
<Undo2 size={14} aria-hidden="true" />
|
|
Unstage all
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if !hasRepository}
|
|
<div class="blank-state">No repository loaded.</div>
|
|
{:else if status?.clean}
|
|
<div class="blank-state">Working tree is clean.</div>
|
|
{:else if changedFiles.length === 0}
|
|
<div class="blank-state">No file changes returned.</div>
|
|
{:else}
|
|
<div class="overflow-auto p-2">
|
|
{#each changedFiles as file (`${file.old_path ?? ""}:${file.path}`)}
|
|
<article class="file-row">
|
|
<div class="file-title">
|
|
<strong title={displayPath(file)}>{fileName(file)}</strong>
|
|
</div>
|
|
|
|
<div class="change-lanes">
|
|
<div class="change-lane" class:inactive={!file.staged}>
|
|
<div class="lane-header">
|
|
<span class="lane-name">Staged</span>
|
|
<span class={`status-badge ${file.staged ?? "none"}`}>{statusLabel(file.staged)}</span>
|
|
</div>
|
|
<div class="lane-actions">
|
|
{#if file.staged}
|
|
<button class="btn-sm" type="button" onclick={() => onUnstage(file)} disabled={isBusy} title="Unstage file">
|
|
<Undo2 size={14} aria-hidden="true" />
|
|
Unstage
|
|
</button>
|
|
<button class="btn-sm" type="button" onclick={() => onPatch(file, true)} disabled={isBusy || !canPatch(file.staged)} title="Stage, unstage, or discard selected lines">
|
|
<FileDiff size={14} aria-hidden="true" />
|
|
Lines
|
|
</button>
|
|
<button class="btn-sm" type="button" onclick={() => onDiscard(file, true)} disabled={isBusy} title="Discard staged changes">
|
|
<RotateCcw size={14} aria-hidden="true" />
|
|
Discard
|
|
</button>
|
|
{:else}
|
|
<span class="quiet">No staged change</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="change-lane" class:inactive={!file.unstaged}>
|
|
<div class="lane-header">
|
|
<span class="lane-name">Unstaged</span>
|
|
<span class={`status-badge ${file.unstaged ?? "none"}`}>{statusLabel(file.unstaged)}</span>
|
|
</div>
|
|
<div class="lane-actions">
|
|
{#if file.unstaged}
|
|
<button class="btn-sm" type="button" onclick={() => onStage(file)} disabled={isBusy} title="Stage file">
|
|
<Check size={14} aria-hidden="true" />
|
|
Stage
|
|
</button>
|
|
<button class="btn-sm" type="button" onclick={() => onPatch(file, false)} disabled={isBusy || !canPatch(file.unstaged)} title="Stage or discard selected lines">
|
|
<FileDiff size={14} aria-hidden="true" />
|
|
Lines
|
|
</button>
|
|
<button class="btn-sm" type="button" onclick={() => onDiscard(file, false)} disabled={isBusy} title="Discard unstaged changes">
|
|
<RotateCcw size={14} aria-hidden="true" />
|
|
Discard
|
|
</button>
|
|
{:else}
|
|
<span class="quiet">No unstaged change</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</section>
|