feat(git): add advanced clone options (shallow, sparse, flags)
Add advanced clone options support across the frontend and backend. Users can specify a branch, shallow limits, blobless mode, sparse paths, and custom clone flags when initiating a clone operation. The backend validates inputs, parses custom flags without invoking a shell, and configures sparse checkouts after a successful clone. A small parsing dependency was added and tracked-file parsing was improved to better handle git ls-files output. - Introduce CloneRunOptions and validation helpers for clone inputs - Parse custom flags with a shell-like tokenizer and block managed flags - Support sparse checkout configuration and shallow clone constraints
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { Cloud, Download, FolderOpen, GitBranch, Globe2, LoaderCircle, LockKeyhole, RefreshCw, Search, X } from "@lucide/svelte";
|
||||
import { listIntegrationRepositories } from "../git";
|
||||
import { configuredIntegrationSources } from "../integrations";
|
||||
import type { AppLanguage, GitIntegrationProvider, GitIntegrationRepository, GitIntegrationSettings, GitIntegrationSource } from "../types";
|
||||
import type { AppLanguage, CloneOptions, GitIntegrationProvider, GitIntegrationRepository, GitIntegrationSettings, GitIntegrationSource } from "../types";
|
||||
|
||||
type CloneSource = "url" | "integrations";
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
error: string;
|
||||
language: AppLanguage;
|
||||
integrations: GitIntegrationSettings;
|
||||
onClone: (remoteUrl: string, parentPath: string, directoryName: string, provider?: GitIntegrationProvider, accountId?: string) => void;
|
||||
onClone: (remoteUrl: string, parentPath: string, directoryName: string, options: CloneOptions, provider?: GitIntegrationProvider, accountId?: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,15 @@
|
||||
let repositoryScrollbarDragScrollTop = 0;
|
||||
let repositoryScrollbarFrame: number | undefined;
|
||||
let repositoryRequestId = 0;
|
||||
let shallowClone = $state(false);
|
||||
let branchToClone = $state("");
|
||||
let shallowLimitMode = $state<"depth" | "since">("depth");
|
||||
let shallowDepth = $state(1);
|
||||
let shallowSince = $state("");
|
||||
let customFlags = $state("");
|
||||
let sparseCheckout = $state(false);
|
||||
let bloblessClone = $state(false);
|
||||
let sparsePathInput = $state("");
|
||||
|
||||
const isGerman = $derived(language === "de");
|
||||
const configuredSources = $derived(configuredIntegrationSources(integrations));
|
||||
@@ -69,7 +78,10 @@
|
||||
});
|
||||
const selectedRepository = $derived(activeRepositories.find((repository) => repository.id === selectedRepositoryId));
|
||||
const directorySuggestion = $derived(directoryNameFromRemoteUrl(remoteUrl));
|
||||
const canSubmit = $derived(!isBusy && remoteUrl.trim().length > 0 && parentPath.trim().length > 0);
|
||||
const sparsePaths = $derived.by(() => [...new Set(sparsePathInput.split(/\r?\n/).map((path) => path.trim()).filter(Boolean))]);
|
||||
const shallowLimitValid = $derived(!shallowClone || (shallowLimitMode === "depth" ? Number.isInteger(shallowDepth) && shallowDepth > 0 : shallowSince.trim().length > 0));
|
||||
const cloneOptionsValid = $derived(shallowLimitValid);
|
||||
const canSubmit = $derived(!isBusy && remoteUrl.trim().length > 0 && parentPath.trim().length > 0 && cloneOptionsValid);
|
||||
|
||||
$effect(() => {
|
||||
const nextError = error || browseError;
|
||||
@@ -260,7 +272,22 @@
|
||||
|
||||
function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
if (canSubmit) onClone(remoteUrl.trim(), parentPath.trim(), directoryName.trim(), source === "integrations" ? activeSource?.provider : undefined, source === "integrations" ? activeSource?.accountId : undefined);
|
||||
if (canSubmit) onClone(
|
||||
remoteUrl.trim(),
|
||||
parentPath.trim(),
|
||||
directoryName.trim(),
|
||||
{
|
||||
branch: shallowClone && branchToClone.trim() ? branchToClone.trim() : null,
|
||||
blobless: sparseCheckout && bloblessClone,
|
||||
customFlags: shallowClone ? customFlags.trim() : "",
|
||||
shallowDepth: shallowClone && shallowLimitMode === "depth" ? shallowDepth : null,
|
||||
shallowSince: shallowClone && shallowLimitMode === "since" ? shallowSince : null,
|
||||
sparse: sparseCheckout,
|
||||
sparsePaths: sparseCheckout ? sparsePaths : [],
|
||||
},
|
||||
source === "integrations" ? activeSource?.provider : undefined,
|
||||
source === "integrations" ? activeSource?.accountId : undefined,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -298,6 +325,48 @@
|
||||
<label class="clone-dialog-field"><span>{isGerman ? "Ordnername" : "Folder name"}</span><input bind:value={directoryName} oninput={handleDirectoryInput} autocomplete="off" spellcheck="false" placeholder={directorySuggestion || "Optional"} disabled={isBusy} /></label>
|
||||
</div>
|
||||
|
||||
<section class="clone-options" aria-label={isGerman ? "Clone-Optionen" : "Clone options"}>
|
||||
<div class="clone-option-card" class:expanded={shallowClone}>
|
||||
<label class="clone-option-toggle">
|
||||
<input type="checkbox" bind:checked={shallowClone} disabled={isBusy} />
|
||||
<span><strong>Shallow Clone</strong><small>{isGerman ? "Nur die neuesten Commits laden" : "Download only the latest commits"}</small></span>
|
||||
</label>
|
||||
{#if shallowClone}
|
||||
<div class="clone-option-body">
|
||||
<label class="clone-option-detail"><span>{isGerman ? "Zu klonender Branch" : "Branch to clone"}</span><input bind:value={branchToClone} autocomplete="off" spellcheck="false" placeholder={isGerman ? "Standard-Branch" : "Default branch"} disabled={isBusy} /></label>
|
||||
<fieldset class="clone-history-mode">
|
||||
<legend>{isGerman ? "Historie begrenzen nach" : "Limit history by"}</legend>
|
||||
<label><input type="radio" bind:group={shallowLimitMode} value="depth" disabled={isBusy} />{isGerman ? "Commit-Tiefe" : "Commit depth"}</label>
|
||||
<label><input type="radio" bind:group={shallowLimitMode} value="since" disabled={isBusy} />{isGerman ? "Seit Datum" : "Since date"}</label>
|
||||
</fieldset>
|
||||
{#if shallowLimitMode === "depth"}
|
||||
<label class="clone-option-detail"><span>{isGerman ? "Tiefe" : "Depth"}</span><input type="number" min="1" step="1" bind:value={shallowDepth} disabled={isBusy} aria-invalid={!Number.isInteger(shallowDepth) || shallowDepth < 1} /></label>
|
||||
{:else}
|
||||
<label class="clone-option-detail"><span>{isGerman ? "Seit" : "Since"}</span><input type="date" bind:value={shallowSince} disabled={isBusy} aria-invalid={!shallowSince.trim()} /></label>
|
||||
{/if}
|
||||
<label class="clone-option-detail"><span>{isGerman ? "Zusätzliche Flags" : "Custom flags"}</span><input bind:value={customFlags} autocomplete="off" spellcheck="false" placeholder="--recurse-submodules --single-branch" disabled={isBusy} /></label>
|
||||
<small class="clone-option-help">{isGerman ? "Flags wie in der Git-Kommandozeile; verwaltete oder unsichere Flags werden abgewiesen." : "Enter flags as on the Git command line; managed or unsafe flags are rejected."}</small>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="clone-option-card" class:expanded={sparseCheckout}>
|
||||
<label class="clone-option-toggle">
|
||||
<input type="checkbox" bind:checked={sparseCheckout} disabled={isBusy} />
|
||||
<span><strong>Sparse Checkout</strong><small>{isGerman ? "Nur ausgewählte Pfade auschecken" : "Check out only selected paths"}</small></span>
|
||||
</label>
|
||||
{#if sparseCheckout}
|
||||
<div class="clone-option-body">
|
||||
<label class="clone-blobless-toggle"><input type="checkbox" bind:checked={bloblessClone} disabled={isBusy} /><span><strong>Blobless Clone</strong><small>{isGerman ? "Lädt zunächst Bäume und Commits ohne Dateiinhalte. Blobs werden bei Bedarf nachgeladen." : "Fetch trees and commits without file contents initially. Blobs are downloaded on demand."}</small></span></label>
|
||||
<label class="clone-option-detail clone-sparse-paths">
|
||||
<span>{isGerman ? "Pfade einschließen" : "Paths to include"}</span>
|
||||
<textarea bind:value={sparsePathInput} rows="3" spellcheck="false" placeholder={"src/\ndocs/\nREADME.md"} disabled={isBusy}></textarea>
|
||||
<small class="clone-option-help">{isGerman ? "Ein Pfad pro Zeile. Nur diese Pfade werden im Arbeitsverzeichnis ausgecheckt. Ohne Pfade werden nur Dateien im Repository-Root ausgecheckt." : "Enter one path per line. Only these paths will be checked out in the working directory. With no paths, only files in the repository root are checked out."}</small>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if source === "url"}
|
||||
<label class="clone-dialog-field clone-url-field">
|
||||
<span>{isGerman ? "Repository-URL" : "Repository URL"}</span>
|
||||
@@ -400,11 +469,33 @@
|
||||
.clone-source-list button.active { color: var(--color-ink); background: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface-hover)); box-shadow: inset 3px 0 0 var(--color-accent); }
|
||||
.clone-source-list button.active :global(svg) { color: var(--color-accent); }
|
||||
.clone-source-nav > p { margin: 12px 14px 0; color: var(--color-ink-faint); font-size: 9px; line-height: 1.45; }
|
||||
.clone-dialog-content { display: grid; grid-template-rows: auto auto minmax(0, 1fr); grid-auto-rows: auto; align-content: stretch; gap: 14px; min-width: 0; min-height: 0; padding: 16px 18px; overflow: hidden; }
|
||||
.clone-dialog-content { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); grid-auto-rows: auto; align-content: stretch; gap: 12px; min-width: 0; min-height: 0; padding: 16px 18px; overflow: hidden; }
|
||||
.clone-dialog-title { display: grid; gap: 3px; }
|
||||
.clone-dialog-title > span { color: var(--color-accent); font-size: 8.5px; font-weight: 850; text-transform: uppercase; letter-spacing: .07em; }
|
||||
.clone-dialog-title h3 { margin: 0; color: var(--color-ink); font-size: 16px; font-weight: 650; }
|
||||
.clone-url-field { align-self: start; margin-top: 2px; }
|
||||
.clone-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 8px; }
|
||||
.clone-option-card { min-width: 0; border: 1px solid var(--color-border-subtle); border-radius: 6px; background: color-mix(in srgb, var(--color-surface-raised) 70%, transparent); }
|
||||
.clone-option-card.expanded { border-color: color-mix(in srgb, var(--color-accent) 34%, var(--color-border)); }
|
||||
.clone-option-toggle { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8px; min-height: 42px; padding: 6px 9px; cursor: pointer; }
|
||||
.clone-option-toggle > input { width: 14px; height: 14px; margin: 0; accent-color: var(--color-accent); }
|
||||
.clone-option-toggle > span { display: grid; min-width: 0; gap: 2px; }
|
||||
.clone-option-toggle strong { color: var(--color-ink); font-size: 10.5px; }
|
||||
.clone-option-toggle small { overflow: hidden; color: var(--color-ink-faint); font-size: 8.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.clone-option-body { display: grid; gap: 8px; padding: 1px 9px 9px 31px; }
|
||||
.clone-option-detail { display: grid; grid-template-columns: minmax(82px, auto) minmax(64px, 1fr); align-items: center; gap: 8px; color: var(--color-ink-faint); font-size: 8.5px; font-weight: 750; text-transform: uppercase; letter-spacing: .035em; }
|
||||
.clone-option-detail input { height: 28px; min-width: 0; font-size: 10px; }
|
||||
.clone-history-mode { display: flex; flex-wrap: wrap; align-items: center; gap: 7px 12px; min-width: 0; margin: 0; padding: 0; border: 0; color: var(--color-ink-dim); font-size: 9.5px; }
|
||||
.clone-history-mode legend { float: left; min-width: 100%; margin-bottom: 1px; color: var(--color-ink-faint); font-size: 8.5px; font-weight: 750; text-transform: uppercase; letter-spacing: .035em; }
|
||||
.clone-history-mode label, .clone-blobless-toggle { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
|
||||
.clone-history-mode input, .clone-blobless-toggle > input { width: 13px; height: 13px; margin: 0; accent-color: var(--color-accent); }
|
||||
.clone-option-help { color: var(--color-ink-faint); font-size: 8px; line-height: 1.35; }
|
||||
.clone-blobless-toggle { align-items: start; }
|
||||
.clone-blobless-toggle > span { display: grid; gap: 2px; }
|
||||
.clone-blobless-toggle strong { color: var(--color-ink); font-size: 9.5px; }
|
||||
.clone-blobless-toggle small { color: var(--color-ink-faint); font-size: 8px; }
|
||||
.clone-sparse-paths { grid-template-columns: 1fr; gap: 5px; }
|
||||
.clone-sparse-paths textarea { min-height: 54px; max-height: 72px; resize: vertical; font: 9.5px/1.35 var(--font-mono); }
|
||||
.integration-browser { display: grid; grid-template-rows: auto minmax(0, 1fr); grid-auto-rows: auto; gap: 8px; min-height: 0; }
|
||||
.repository-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) 32px; gap: 6px; }
|
||||
.repository-toolbar label { position: relative; min-width: 0; }
|
||||
@@ -470,6 +561,8 @@
|
||||
.clone-source-list { display: flex; width: max-content; min-width: 100%; padding: 0 6px; }
|
||||
.clone-source-list button { width: auto; min-height: 34px; padding-inline: 9px; border-radius: 5px; }
|
||||
.clone-source-list button.active { box-shadow: inset 0 -2px 0 var(--color-accent); }
|
||||
.clone-dialog-content { padding: 13px 12px; overflow: hidden; }
|
||||
.clone-options { grid-template-columns: 1fr; }
|
||||
.clone-dialog-content { grid-template-rows: auto; grid-auto-rows: auto; align-content: start; padding: 13px 12px; overflow: auto; }
|
||||
.integration-browser { min-height: 260px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user