feat(git stash): add stash listing and push/apply/pop/drop UI

This change introduces Git stash support end-to-end, including a new
backend command to list stashes and operations to push, apply, pop, and
drop them. The frontend now fetches stashes as part of the repository
bundle and provides a dedicated panel to manage shelved changes.

- Add GitStash model and Tauri commands for stash operations
- Implement StashPanel component and wire it into the app
- Adjust sidebar layout and add stash-specific styling
This commit is contained in:
Christoph Brandau
2026-07-04 00:16:27 +02:00
parent 201bb90bf7
commit 1d67312ee4
7 changed files with 497 additions and 5 deletions
+116
View File
@@ -0,0 +1,116 @@
<script lang="ts">
import { Archive, Download, Trash2, Upload } from "@lucide/svelte";
import type { GitStash } from "../types";
interface Props {
stashes: GitStash[];
changedCount: number;
hasRepository: boolean;
isBusy: boolean;
onPush: (message: string, includeUntracked: boolean) => void;
onApply: (stash: GitStash) => void;
onPop: (stash: GitStash) => void;
onDrop: (stash: GitStash) => void;
}
let {
stashes = [],
changedCount = 0,
hasRepository = false,
isBusy = false,
onPush = () => {},
onApply = () => {},
onPop = () => {},
onDrop = () => {},
}: Props = $props();
let message = $state("");
let includeUntracked = $state(true);
function submitPush() {
onPush(message, includeUntracked);
message = "";
}
function stashTitle(stash: GitStash): string {
return stash.message || stash.selector;
}
</script>
<section class="panel stash-panel overflow-hidden" aria-label="Git stash">
<div class="section-head">
<div>
<span class="eyebrow">Stash</span>
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Shelved changes</h2>
</div>
<span class="pill pill-count">{stashes.length}</span>
</div>
{#if !hasRepository}
<div class="blank-state">No repository loaded.</div>
{:else}
<div class="stash-create">
<input
class="stash-input"
type="text"
bind:value={message}
placeholder="Optional message"
disabled={isBusy || changedCount === 0}
onkeydown={(event) => {
if (event.key === "Enter" && changedCount > 0 && !isBusy) submitPush();
}}
/>
<label class="stash-check">
<input type="checkbox" bind:checked={includeUntracked} disabled={isBusy || changedCount === 0} />
Untracked
</label>
<button
class="btn-sm stash-save-button"
type="button"
onclick={submitPush}
disabled={isBusy || changedCount === 0}
title="Save current working tree changes to a stash"
>
<Archive size={14} aria-hidden="true" />
Stash
</button>
</div>
{#if stashes.length === 0}
<div class="blank-state stash-empty">No stashes saved.</div>
{:else}
<div class="stash-list">
{#each stashes as stash (stash.selector)}
<article class="stash-row">
<div class="stash-row-main">
<strong title={stashTitle(stash)}>{stashTitle(stash)}</strong>
<span>
{stash.selector}
{#if stash.branch}
on {stash.branch}
{/if}
{#if stash.date}
- {stash.date}
{/if}
</span>
</div>
<div class="stash-actions">
<button class="btn-sm" type="button" onclick={() => onApply(stash)} disabled={isBusy} title="Apply stash and keep it">
<Download size={13} aria-hidden="true" />
Apply
</button>
<button class="btn-sm" type="button" onclick={() => onPop(stash)} disabled={isBusy} title="Apply stash and remove it if successful">
<Upload size={13} aria-hidden="true" />
Pop
</button>
<button class="btn-sm danger" type="button" onclick={() => onDrop(stash)} disabled={isBusy} title="Delete stash">
<Trash2 size={13} aria-hidden="true" />
Drop
</button>
</div>
</article>
{/each}
</div>
{/if}
{/if}
</section>