Files
GitLite/src/lib/RepoToolbar.svelte
T
Christoph 66c85321ea feat(submodules): add submodule management and recursive clone
Add a new backend module to manage Git submodules (list, initialize/update,
stage, sync, add) and expose Tauri commands (list_submodules,
submodule_action, add_submodule). The implementation enforces safe relative
paths, a maximum nesting depth, and guards (dirty/conflicted checks and
initialization/no-op semantics) to avoid unsafe operations.

Refactor clone logic to a testable run_git_clone_command and enable
automatic initialization of submodules during clone by passing
--recurse-submodules. Also disallow certain submodule-related custom clone
flags so callers cannot override this behaviour.

Update README with a Submodules section and add frontend components and types
to surface submodule UI (dialogs, toolbar badge). Unit tests were added for
submodule discovery, initialization/update semantics, and recursive clone
behavior.
2026-09-16 22:31:14 +02:00

278 lines
12 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import {
Box,
Boxes,
Bug,
ChevronDown,
Code2,
CloudDownload,
CloudOff,
Download,
FolderOpen,
GitCompare,
History,
ListRestart,
LoaderCircle,
RefreshCw,
Search,
Upload,
Settings2,
Terminal,
} from "@lucide/svelte";
export let hasRepository: boolean = false;
export let isBusy: boolean = false;
export let operation: string = "";
export let ahead: number = 0;
export let behind: number = 0;
export let localOnly: boolean = false;
export let language: "en" | "de" = "en";
export let editorName: string = "Editor";
export let terminalName: string = "Terminal";
export let fileManagerName: string = "Explorer";
export let onFetch: () => void = () => {};
export let onPull: () => void = () => {};
export let onPush: () => void = () => {};
export let onRefresh: () => void = () => {};
export let onSearch: () => void = () => {};
export let onCompare: () => void = () => {};
export let onInteractiveRebase: () => void = () => {};
export let onReflog: () => void = () => {};
export let onBisect: () => void = () => {};
export let onOpenInExplorer: () => void = () => {};
export let onOpenInEditor: () => void = () => {};
export let onOpenTerminal: () => void = () => {};
export let onFetchPrune: () => void = () => {};
export let onForcePush: () => void = () => {};
export let onSyncOptions: () => void = () => {};
export let uninitializedSubmoduleCount = 0;
export let onOpenSubmodules: () => void = () => {};
export let onOpenLfs: () => void = () => {};
let historyOpen = false;
let syncOpen = false;
let toolbarElement: HTMLDivElement;
$: isGerman = language === "de";
$: submoduleLabel = uninitializedSubmoduleCount > 0
? (isGerman ? `Submodule verwalten ${uninitializedSubmoduleCount} nicht initialisiert` : `Manage submodules ${uninitializedSubmoduleCount} not initialized`)
: (isGerman ? "Submodule verwalten" : "Manage submodules");
$: pushLabel = localOnly ? (isGerman ? "Veröffentlichen" : "Publish") : "Push";
$: pushTitle = localOnly
? (isGerman
? "Dieser Branch existiert nur lokal. Veröffentlichen erstellt den Remote-Branch und richtet das Tracking ein."
: "This branch exists only locally. Publish creates the remote branch and configures tracking.")
: "Push";
function runHistoryAction(action: () => void) {
historyOpen = false;
action();
}
function handleWindowClick(event: MouseEvent) {
if (toolbarElement && !toolbarElement.contains(event.target as Node)) { historyOpen = false; syncOpen = false; }
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && historyOpen) {
event.stopPropagation();
historyOpen = false;
}
}
</script>
<svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
<div bind:this={toolbarElement} class="repo-toolbar" role="toolbar" aria-label={isGerman ? "Repository-Aktionen" : "Repository actions"}>
<div class="repo-toolbar-sync">
<div class="repo-action-group repo-sync-actions">
<button
class="repo-action fetch"
onclick={onFetch}
disabled={!hasRepository || isBusy}
title="Fetch"
aria-label="Fetch"
>
{#if operation === "Fetching"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<CloudDownload size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">Fetch</span>
</button>
<button
class="repo-action sync-primary"
onclick={onPull}
disabled={!hasRepository || isBusy}
title="Pull"
aria-label={behind > 0
? `Pull, ${behind} ${isGerman ? (behind === 1 ? "Remote-Commit voraus" : "Remote-Commits voraus") : (behind === 1 ? "commit behind" : "commits behind")}`
: "Pull"}
>
{#if operation === "Pulling"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<Download size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">Pull</span>
{#if behind > 0}<span class="repo-action-count behind">↓{behind}</span>{/if}
</button>
<button
class="repo-action sync-primary"
class:publish-local={localOnly}
onclick={onPush}
disabled={!hasRepository || isBusy}
title={pushTitle}
aria-label={localOnly
? pushTitle
: ahead > 0
? `Push, ${ahead} ${isGerman ? (ahead === 1 ? "lokaler Commit voraus" : "lokale Commits voraus") : (ahead === 1 ? "commit ahead" : "commits ahead")}`
: "Push"}
>
{#if operation === "Pushing"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<Upload size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">{pushLabel}</span>
{#if localOnly}
<span class="repo-action-local-marker"><CloudOff size={9} aria-hidden="true" />{isGerman ? "NUR LOKAL" : "LOCAL"}</span>
{:else if ahead > 0}
<span class="repo-action-count ahead">↑{ahead}</span>
{/if}
</button>
<div class="repo-history-wrap">
<button class="repo-action" type="button" onclick={() => { syncOpen = !syncOpen; historyOpen = false; }} disabled={!hasRepository || isBusy} aria-label={isGerman ? "Sync-Optionen" : "Sync options"} aria-haspopup="menu">
<ChevronDown size={14} aria-hidden="true" />
</button>
{#if syncOpen}
<div class="repo-history-menu" role="menu">
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onFetchPrune(); }}><CloudDownload size={15} /><span><strong>Fetch + Prune</strong><small>{isGerman ? "Veraltete Remote-Branches entfernen" : "Remove stale remote branches"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onForcePush(); }}><Upload size={15} /><span><strong>Force with lease</strong><small>{isGerman ? "Sicheres Pushen nach Rebase" : "Safe push after rebase"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onSyncOptions(); }}><Settings2 size={15} /><span><strong>{isGerman ? "Remotes & Strategien" : "Remotes & strategies"}</strong><small>{isGerman ? "Upstream, Pull und Remote verwalten" : "Manage upstream, pull and remotes"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onOpenLfs(); }}><Box size={15} /><span><strong>Git LFS</strong><small>{isGerman ? "Große Dateien und LFS-Installation verwalten" : "Manage large files and LFS installation"}</small></span></button>
</div>
{/if}
</div>
</div>
</div>
<div class="repo-toolbar-divider" aria-hidden="true"></div>
<div class="repo-action-group repo-inspect-actions">
<button
class="repo-action"
onclick={onSearch}
disabled={!hasRepository || isBusy}
title={isGerman ? "Globale Suche" : "Global search"}
aria-label={isGerman ? "Globale Suche" : "Global search"}
>
<Search size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Suchen" : "Search"}</span>
</button>
<button
class="repo-action"
onclick={onCompare}
disabled={!hasRepository || isBusy}
title={isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"}
aria-label={isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"}
>
<GitCompare size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Vergleichen" : "Compare"}</span>
</button>
<div class="repo-history-wrap">
<button
class="repo-action history-trigger"
class:active={historyOpen}
type="button"
onclick={() => { historyOpen = !historyOpen; }}
disabled={!hasRepository || isBusy}
aria-haspopup="menu"
aria-expanded={historyOpen}
title={isGerman ? "Verlauf und Rebase" : "History and rebase"}
>
<History size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Verlauf" : "History"}</span>
<ChevronDown class="history-chevron" size={13} aria-hidden="true" />
</button>
{#if historyOpen}
<div class="repo-history-menu" role="menu">
<button type="button" role="menuitem" onclick={() => runHistoryAction(onReflog)}>
<History size={15} aria-hidden="true" />
<span>
<strong>Reflog</strong>
<small>{isGerman ? "Lokale Referenzbewegungen" : "Local reference movements"}</small>
</span>
</button>
<button type="button" role="menuitem" onclick={() => runHistoryAction(onInteractiveRebase)}>
<ListRestart size={15} aria-hidden="true" />
<span>
<strong>{isGerman ? "Interaktiver Rebase" : "Interactive rebase"}</strong>
<small>{isGerman ? "Commits ordnen und zusammenfassen" : "Reorder and combine commits"}</small>
</span>
</button>
<button type="button" role="menuitem" onclick={() => runHistoryAction(onBisect)}>
<Bug size={15} aria-hidden="true" />
<span>
<strong>Git Bisect</strong>
<small>{isGerman ? "Fehlerhaften Commit schrittweise finden" : "Find a bad commit step by step"}</small>
</span>
</button>
</div>
{/if}
</div>
<button
class="repo-action"
type="button"
onclick={onOpenSubmodules}
disabled={!hasRepository || isBusy}
title={submoduleLabel}
aria-label={submoduleLabel}
>
<Boxes size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Submodule" : "Submodules"}</span>
{#if uninitializedSubmoduleCount > 0}<span class="repo-action-count ahead" aria-hidden="true">{uninitializedSubmoduleCount}</span>{/if}
</button>
</div>
<div class="repo-toolbar-spacer"></div>
<div class="repo-toolbar-divider utility" aria-hidden="true"></div>
<div class="repo-action-group repo-utility-actions">
<button class="repo-action" onclick={onOpenInEditor} disabled={!hasRepository || isBusy} title={isGerman ? `Repository in ${editorName} öffnen` : `Open repository in ${editorName}`}>
<Code2 size={15} aria-hidden="true" />
<span class="repo-action-label utility-label">{editorName}</span>
</button>
<button class="repo-action" onclick={onOpenTerminal} disabled={!hasRepository || isBusy} title={isGerman ? `${terminalName} im Repository öffnen` : `Open ${terminalName} in repository`}>
<Terminal size={15} aria-hidden="true" />
<span class="repo-action-label utility-label">{terminalName}</span>
</button>
<button
class="repo-action"
onclick={onOpenInExplorer}
disabled={!hasRepository || isBusy}
title={isGerman ? `Repository in ${fileManagerName} öffnen` : `Open repository in ${fileManagerName}`}
aria-label={isGerman ? `Repository in ${fileManagerName} öffnen` : `Open repository in ${fileManagerName}`}
>
<FolderOpen size={15} aria-hidden="true" />
<span class="repo-action-label utility-label">{fileManagerName}</span>
</button>
<button
class="repo-action"
onclick={onRefresh}
disabled={isBusy || !hasRepository}
title={isGerman ? "Manuell aktualisieren" : "Refresh now"}
aria-label={isGerman ? "Manuell aktualisieren" : "Refresh now"}
>
<RefreshCw class={operation === "Refreshing" ? "spin" : ""} size={16} aria-hidden="true" />
<span class="repo-action-label utility-label">{isGerman ? "Aktualisieren" : "Refresh"}</span>
</button>
</div>
</div>