feat(git-lfs): add Git LFS status and sidecar bundling
Adds Git LFS support to the backend API and related UI. The app now exposes commands to inspect, install, track and pull. A sidecar git-lfs binary is bundled and a prep script is added. This prepares the correct binary for each target platform. - Expose Git LFS status and management commands in API - Bundle and prepare a sidecar git-lfs binary for targets - Update packaging, docs, and README with LFS notes
This commit is contained in:
+141
-1
@@ -69,7 +69,9 @@
|
||||
fetchRemote,
|
||||
getCommitNote,
|
||||
getFileBlame,
|
||||
getGitLfsStatus,
|
||||
getStatus,
|
||||
installGitLfs,
|
||||
lastCommitMessage,
|
||||
listBranches,
|
||||
listRemotes,
|
||||
@@ -90,7 +92,9 @@
|
||||
lockWorktree,
|
||||
moveWorktree,
|
||||
pruneWorktrees,
|
||||
pruneGitLfsObjects,
|
||||
pull,
|
||||
pullGitLfsObjects,
|
||||
push,
|
||||
pushCommitNotes,
|
||||
pushTag,
|
||||
@@ -125,12 +129,14 @@
|
||||
startInteractiveRebase,
|
||||
setSyncBadge,
|
||||
stageFiles,
|
||||
trackGitLfsPattern,
|
||||
stashApply,
|
||||
stashDrop,
|
||||
stashPop,
|
||||
stashPush,
|
||||
undoLastCommit,
|
||||
unlockWorktree,
|
||||
untrackGitLfsPattern,
|
||||
unstageFiles,
|
||||
} from "./lib/git";
|
||||
|
||||
@@ -155,6 +161,7 @@
|
||||
GitCommitComparison,
|
||||
GitDiffFile,
|
||||
GitFileStatus,
|
||||
GitLfsStatus,
|
||||
GitRepositoryFile,
|
||||
GitRemote,
|
||||
PullStrategy,
|
||||
@@ -376,6 +383,10 @@
|
||||
let worktrees: GitWorktree[] = [];
|
||||
let worktreesLoading = false;
|
||||
let worktreeError = "";
|
||||
let gitLfsDialogOpen = false;
|
||||
let gitLfsStatus: GitLfsStatus | null = null;
|
||||
let gitLfsLoading = false;
|
||||
let gitLfsError = "";
|
||||
let compareSelectOpen = false;
|
||||
let compareDialogOpen = false;
|
||||
let interactiveRebaseOpen = false;
|
||||
@@ -956,7 +967,7 @@
|
||||
}
|
||||
|
||||
async function autoRefreshTick() {
|
||||
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || newBranchCommit || commitNoteTarget || globalSearchOpen || helpOpen) return;
|
||||
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || gitLfsDialogOpen || newBranchCommit || commitNoteTarget || globalSearchOpen || helpOpen) return;
|
||||
const path = activeRepoPath;
|
||||
autoRefreshInFlight = true;
|
||||
try {
|
||||
@@ -2955,6 +2966,114 @@
|
||||
worktreeError = "";
|
||||
}
|
||||
|
||||
async function openGitLfsDialog() {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
gitLfsDialogOpen = true;
|
||||
gitLfsStatus = null;
|
||||
gitLfsError = "";
|
||||
await refreshGitLfsStatus();
|
||||
const inspectedStatus = gitLfsStatus as GitLfsStatus | null;
|
||||
trackEvent("git_lfs_dialog_opened", {
|
||||
available: inspectedStatus?.available ? 1 : 0,
|
||||
repository_uses_lfs: inspectedStatus?.repository_uses_lfs ? 1 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshGitLfsStatus() {
|
||||
if (!activeRepoPath || gitLfsLoading) return;
|
||||
gitLfsLoading = true;
|
||||
gitLfsError = "";
|
||||
try {
|
||||
gitLfsStatus = await getGitLfsStatus(activeRepoPath);
|
||||
} catch (error) {
|
||||
gitLfsError = errorToMessage(error);
|
||||
} finally {
|
||||
gitLfsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runGitLfsOperation(
|
||||
label: string,
|
||||
task: () => Promise<GitLfsStatus>,
|
||||
eventName: string,
|
||||
): Promise<boolean> {
|
||||
if (!activeRepoPath || isBusy) return false;
|
||||
const repository = activeRepoPath;
|
||||
operation = label;
|
||||
gitLfsError = "";
|
||||
try {
|
||||
gitLfsStatus = await task();
|
||||
applyStatus(await getStatus(repository));
|
||||
await refreshExplorerFiles(repository);
|
||||
trackEvent(eventName, {
|
||||
patterns: gitLfsStatus.patterns.length,
|
||||
files: gitLfsStatus.files.length,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
gitLfsError = errorToMessage(error);
|
||||
return false;
|
||||
} finally {
|
||||
operation = "";
|
||||
}
|
||||
}
|
||||
|
||||
function activateGitLfs() {
|
||||
return runGitLfsOperation(
|
||||
"Activating Git LFS",
|
||||
() => installGitLfs(activeRepoPath),
|
||||
"git_lfs_activated",
|
||||
);
|
||||
}
|
||||
|
||||
function addGitLfsPattern(pattern: string, lockable: boolean): Promise<boolean> {
|
||||
return runGitLfsOperation(
|
||||
`Tracking ${pattern} with Git LFS`,
|
||||
() => trackGitLfsPattern(activeRepoPath, pattern, lockable),
|
||||
"git_lfs_pattern_added",
|
||||
);
|
||||
}
|
||||
|
||||
async function removeGitLfsPattern(pattern: string) {
|
||||
await runGitLfsOperation(
|
||||
`Removing Git LFS pattern ${pattern}`,
|
||||
() => untrackGitLfsPattern(activeRepoPath, pattern),
|
||||
"git_lfs_pattern_removed",
|
||||
);
|
||||
}
|
||||
|
||||
async function pullGitLfsFiles() {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
let credential: StoredCredential | null = null;
|
||||
const key = await currentCredKey("pull");
|
||||
if (key) credential = await credLoad(key);
|
||||
await runGitLfsOperation(
|
||||
"Downloading Git LFS objects",
|
||||
() => pullGitLfsObjects(
|
||||
activeRepoPath,
|
||||
selectedRemote || undefined,
|
||||
credential?.username,
|
||||
credential?.password,
|
||||
),
|
||||
"git_lfs_objects_pulled",
|
||||
);
|
||||
}
|
||||
|
||||
async function pruneGitLfsCache() {
|
||||
await runGitLfsOperation(
|
||||
"Pruning Git LFS cache",
|
||||
() => pruneGitLfsObjects(activeRepoPath),
|
||||
"git_lfs_cache_pruned",
|
||||
);
|
||||
}
|
||||
|
||||
function closeGitLfsDialog() {
|
||||
if (isBusy) return;
|
||||
gitLfsDialogOpen = false;
|
||||
gitLfsStatus = null;
|
||||
gitLfsError = "";
|
||||
}
|
||||
|
||||
function openNewBranchDialog(commit: GitCommit) {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
newBranchCommit = commit;
|
||||
@@ -4705,6 +4824,7 @@
|
||||
else if (event.key === "Escape" && renameBranchTarget) renameBranchTarget = null;
|
||||
else if (event.key === "Escape" && deleteBranchTarget) closeDeleteBranchDialog();
|
||||
else if (event.key === "Escape" && worktreeDialogOpen) closeWorktreeDialog();
|
||||
else if (event.key === "Escape" && gitLfsDialogOpen) closeGitLfsDialog();
|
||||
else if (event.key === "Escape" && interactiveRebaseOpen && !isBusy) interactiveRebaseOpen = false;
|
||||
else if (event.key === "Escape" && reflogOpen && !isBusy) reflogOpen = false;
|
||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||
@@ -4778,6 +4898,7 @@
|
||||
onFetchPrune={fetchPruneRepo}
|
||||
onForcePush={forcePushRepo}
|
||||
onSyncOptions={openSyncOptions}
|
||||
onOpenLfs={openGitLfsDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -5565,6 +5686,25 @@
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if gitLfsDialogOpen}
|
||||
{#await import("./lib/components/GitLfsDialog.svelte") then module}
|
||||
<module.default
|
||||
status={gitLfsStatus}
|
||||
language={appLanguage}
|
||||
isLoading={gitLfsLoading}
|
||||
{isBusy}
|
||||
error={gitLfsError}
|
||||
onRefresh={refreshGitLfsStatus}
|
||||
onInstall={activateGitLfs}
|
||||
onTrack={addGitLfsPattern}
|
||||
onUntrack={removeGitLfsPattern}
|
||||
onPull={pullGitLfsFiles}
|
||||
onPrune={pruneGitLfsCache}
|
||||
onClose={closeGitLfsDialog}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<!-- Create a branch from a specific commit in the history -->
|
||||
{#if newBranchCommit}
|
||||
<NewBranchDialog
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Box,
|
||||
ChevronDown,
|
||||
Code2,
|
||||
CloudDownload,
|
||||
@@ -41,6 +42,7 @@
|
||||
export let onFetchPrune: () => void = () => {};
|
||||
export let onForcePush: () => void = () => {};
|
||||
export let onSyncOptions: () => void = () => {};
|
||||
export let onOpenLfs: () => void = () => {};
|
||||
|
||||
let historyOpen = false;
|
||||
let syncOpen = false;
|
||||
@@ -142,6 +144,7 @@
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArchiveRestore,
|
||||
Box,
|
||||
Check,
|
||||
CircleDashed,
|
||||
Download,
|
||||
HardDriveDownload,
|
||||
Link2,
|
||||
LoaderCircle,
|
||||
PackageCheck,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
X,
|
||||
} from "@lucide/svelte";
|
||||
import type { AppLanguage, GitLfsPattern, GitLfsStatus } from "../types";
|
||||
|
||||
interface Props {
|
||||
status: GitLfsStatus | null;
|
||||
language: AppLanguage;
|
||||
isLoading: boolean;
|
||||
isBusy: boolean;
|
||||
error?: string;
|
||||
onRefresh: () => void | Promise<void>;
|
||||
onInstall: () => boolean | void | Promise<boolean | void>;
|
||||
onTrack: (pattern: string, lockable: boolean) => boolean | Promise<boolean>;
|
||||
onUntrack: (pattern: string) => void | Promise<void>;
|
||||
onPull: () => void | Promise<void>;
|
||||
onPrune: () => void | Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
status = null,
|
||||
language = "en",
|
||||
isLoading = false,
|
||||
isBusy = false,
|
||||
error = "",
|
||||
onRefresh = () => {},
|
||||
onInstall = () => {},
|
||||
onTrack = () => false,
|
||||
onUntrack = () => {},
|
||||
onPull = () => {},
|
||||
onPrune = () => {},
|
||||
onClose = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
let pattern = $state("");
|
||||
let lockable = $state(false);
|
||||
let isGerman = $derived(language === "de");
|
||||
let setupReady = $derived(Boolean(status?.filters_configured && status?.hook_installed));
|
||||
let downloadedCount = $derived(status?.files.filter((file) => file.downloaded).length ?? 0);
|
||||
let totalSize = $derived(status?.files.reduce((sum, file) => sum + file.size, 0) ?? 0);
|
||||
|
||||
function formatBytes(value: number): string {
|
||||
if (!value) return "—";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
|
||||
const amount = value / 1024 ** index;
|
||||
return `${amount >= 10 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function versionLabel(value: string | null | undefined): string {
|
||||
return value?.match(/git-lfs\/([^\s]+)/)?.[1] ?? value ?? "—";
|
||||
}
|
||||
|
||||
function canRemove(item: GitLfsPattern): boolean {
|
||||
return item.source === ".gitattributes";
|
||||
}
|
||||
|
||||
async function submitPattern(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
const value = pattern.trim();
|
||||
if (!value || !setupReady) return;
|
||||
if (await onTrack(value, lockable)) {
|
||||
pattern = "";
|
||||
lockable = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmPrune() {
|
||||
const confirmed = window.confirm(isGerman
|
||||
? "Nicht mehr benötigte lokale LFS-Objekte sicher bereinigen? Nicht gepushte und aktuell verwendete Objekte bleiben erhalten."
|
||||
: "Safely prune unused local LFS objects? Unpushed and currently used objects are retained.");
|
||||
if (confirmed) await onPrune();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop" role="presentation">
|
||||
<div class="dialog lfs-dialog" role="dialog" aria-modal="true" aria-labelledby="lfs-dialog-title">
|
||||
<header class="dialog-header lfs-dialog-header">
|
||||
<div class="lfs-heading">
|
||||
<span class="lfs-mark" aria-hidden="true"><Box size={19} /></span>
|
||||
<div>
|
||||
<span class="eyebrow">Large file storage</span>
|
||||
<p class="dialog-title" id="lfs-dialog-title">Git LFS</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-header-actions">
|
||||
<button class="btn-sm" type="button" onclick={onRefresh} disabled={isBusy || isLoading}>
|
||||
<RefreshCw class={isLoading ? "spin" : undefined} size={15} aria-hidden="true" />
|
||||
{isGerman ? "Prüfen" : "Check"}
|
||||
</button>
|
||||
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label={isGerman ? "Schließen" : "Close"}>
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="lfs-main">
|
||||
{#if error}
|
||||
<div class="lfs-error" role="alert"><AlertTriangle size={15} aria-hidden="true" />{error}</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading && !status}
|
||||
<div class="lfs-loading"><LoaderCircle class="spin" size={22} aria-hidden="true" /><span>{isGerman ? "Git LFS wird geprüft…" : "Inspecting Git LFS…"}</span></div>
|
||||
{:else if status}
|
||||
<div class="lfs-content">
|
||||
<section class="lfs-diagnostics" aria-label={isGerman ? "LFS-Diagnose" : "LFS diagnostics"}>
|
||||
<article class:ready={status.available} class:error={!status.available}>
|
||||
<span class="diagnostic-icon">{#if status.available}<PackageCheck size={18} />{:else}<AlertTriangle size={18} />{/if}</span>
|
||||
<div><small>01 · {isGerman ? "Erweiterung" : "Extension"}</small><strong>{status.available ? `Git LFS ${versionLabel(status.version)}` : (isGerman ? "Nicht gefunden" : "Not found")}</strong><span>{status.bundled ? (isGerman ? "Mit Gitty gebündelt" : "Bundled with Gitty") : (isGerman ? "Systeminstallation" : "System installation")}</span></div>
|
||||
</article>
|
||||
<span class:ready={status.available} class="diagnostic-link" aria-hidden="true"><Link2 size={14} /></span>
|
||||
<article class:ready={setupReady} class:warning={status.available && !setupReady}>
|
||||
<span class="diagnostic-icon">{#if setupReady}<ShieldCheck size={18} />{:else}<CircleDashed size={18} />{/if}</span>
|
||||
<div><small>02 · Repository</small><strong>{setupReady ? (isGerman ? "Aktiv" : "Active") : (isGerman ? "Einrichtung nötig" : "Setup required")}</strong><span>{status.filters_configured ? "Filter ✓" : "Filter —"} · {status.hook_installed ? "Pre-push ✓" : "Pre-push —"}</span></div>
|
||||
</article>
|
||||
<span class:ready={setupReady} class="diagnostic-link" aria-hidden="true"><Link2 size={14} /></span>
|
||||
<article class:ready={status.files.length > 0}>
|
||||
<span class="diagnostic-icon"><ArchiveRestore size={18} /></span>
|
||||
<div><small>03 · {isGerman ? "Objekte" : "Objects"}</small><strong>{status.files.length} {isGerman ? "Dateien" : "files"}</strong><span>{downloadedCount} {isGerman ? "lokal" : "local"} · {formatBytes(totalSize)}</span></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{#if !status.available}
|
||||
<section class="lfs-missing">
|
||||
<span class="lfs-missing-icon"><AlertTriangle size={22} aria-hidden="true" /></span>
|
||||
<div>
|
||||
<strong>{isGerman ? "Git LFS ist nicht ausführbar" : "Git LFS cannot be started"}</strong>
|
||||
<p>{isGerman ? "Gitty liefert Git LFS normalerweise mit. Installiere Gitty erneut oder installiere git-lfs systemweit und starte die App neu." : "Gitty normally includes Git LFS. Reinstall Gitty or install git-lfs system-wide, then restart the app."}</p>
|
||||
</div>
|
||||
</section>
|
||||
{:else}
|
||||
{#if !setupReady}
|
||||
<section class="lfs-setup-card">
|
||||
<div><strong>{isGerman ? "Für dieses Repository aktivieren" : "Activate for this repository"}</strong><p>{isGerman ? "Richtet Clean-/Smudge-Filter lokal ein und installiert den Pre-push-Hook. Globale Git-Einstellungen bleiben unverändert." : "Configures local clean/smudge filters and installs the pre-push hook. Global Git settings remain unchanged."}</p></div>
|
||||
<button class="btn-primary" type="button" onclick={onInstall} disabled={isBusy}><ShieldCheck size={15} aria-hidden="true" />{isGerman ? "LFS aktivieren" : "Activate LFS"}</button>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<div class="lfs-grid">
|
||||
<section class="lfs-panel patterns-panel">
|
||||
<header><div><span class="eyebrow">Tracking rules</span><h3>{isGerman ? "Muster" : "Patterns"}</h3></div><span>{status.patterns.length}</span></header>
|
||||
<form class="lfs-track-form" onsubmit={submitPattern}>
|
||||
<label><span>{isGerman ? "Neues Muster" : "New pattern"}</span><input type="text" bind:value={pattern} disabled={isBusy || !setupReady} autocomplete="off" spellcheck="false" placeholder="*.psd, Assets/**, video.mp4" /></label>
|
||||
<label class="lfs-lockable"><input type="checkbox" bind:checked={lockable} disabled={isBusy || !setupReady} /><span><strong>Lockable</strong><small>{isGerman ? "Schreibgeschützt, solange nicht gesperrt" : "Read-only until locked"}</small></span></label>
|
||||
<button class="btn-primary" type="submit" disabled={isBusy || !setupReady || !pattern.trim()}><Plus size={15} aria-hidden="true" />{isGerman ? "Hinzufügen" : "Add"}</button>
|
||||
</form>
|
||||
|
||||
<div class="lfs-pattern-list">
|
||||
{#if status.patterns.length === 0}
|
||||
<div class="lfs-empty"><Box size={20} aria-hidden="true" /><span>{isGerman ? "Noch keine Dateien werden über LFS verwaltet." : "No files are tracked through LFS yet."}</span></div>
|
||||
{:else}
|
||||
{#each status.patterns as item (`${item.source}:${item.pattern}`)}
|
||||
<article>
|
||||
<div><code>{item.pattern}</code><span>{item.source}{item.lockable ? " · lockable" : ""}</span></div>
|
||||
<button class="btn-sm danger" type="button" onclick={() => onUntrack(item.pattern)} disabled={isBusy || !canRemove(item)} title={canRemove(item) ? (isGerman ? "Muster entfernen" : "Remove pattern") : (isGerman ? "Nur Muster aus der Wurzel-.gitattributes können hier entfernt werden" : "Only patterns from the root .gitattributes can be removed here") }><Trash2 size={14} aria-hidden="true" /></button>
|
||||
</article>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="lfs-panel objects-panel">
|
||||
<header><div><span class="eyebrow">Current checkout</span><h3>{isGerman ? "LFS-Dateien" : "LFS files"}</h3></div><span>{status.files.length}</span></header>
|
||||
<div class="lfs-object-list">
|
||||
{#if status.files.length === 0}
|
||||
<div class="lfs-empty"><HardDriveDownload size={20} aria-hidden="true" /><span>{isGerman ? "Im aktuellen Stand sind keine LFS-Objekte vorhanden." : "The current checkout has no LFS objects."}</span></div>
|
||||
{:else}
|
||||
{#each status.files as file (`${file.oid}:${file.name}`)}
|
||||
<article>
|
||||
<span class:downloaded={file.downloaded} class="object-state" title={file.downloaded ? (isGerman ? "Objekt lokal vorhanden" : "Object available locally") : (isGerman ? "Nur LFS-Zeiger vorhanden" : "LFS pointer only")}>{#if file.downloaded}<Check size={12} />{:else}<Download size={12} />{/if}</span>
|
||||
<div title={file.name}><strong>{file.name}</strong><span>{formatBytes(file.size)} · <code>{file.oid.slice(0, 10)}</code></span></div>
|
||||
</article>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer class="lfs-footer">
|
||||
<div><ShieldCheck size={14} aria-hidden="true" /><span>{isGerman ? ".gitattributes bleibt als normale Änderung sichtbar und muss committed werden." : ".gitattributes remains a normal change and must be committed."}</span></div>
|
||||
<div>
|
||||
<button class="btn-secondary" type="button" onclick={confirmPrune} disabled={isBusy || !status?.available}><Trash2 size={14} aria-hidden="true" />{isGerman ? "Cache bereinigen" : "Prune cache"}</button>
|
||||
<button class="btn-primary" type="button" onclick={onPull} disabled={isBusy || !setupReady}><HardDriveDownload size={15} aria-hidden="true" />{isGerman ? "Objekte laden" : "Pull objects"}</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lfs-dialog { --lfs-success: #4eca76; --lfs-warning: #f0b648; --lfs-danger: #e86060; width: min(880px, calc(100vw - 32px)); height: auto; max-height: min(660px, calc(100vh - 32px)); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; border-radius: 12px; }
|
||||
.lfs-dialog-header, .lfs-heading, .dialog-header-actions, .lfs-footer, .lfs-footer > div { display: flex; align-items: center; }
|
||||
.lfs-dialog-header { min-height: 58px; justify-content: space-between; padding: 10px 13px; }
|
||||
.lfs-heading { gap: 9px; }
|
||||
.lfs-mark { display: grid; width: 34px; height: 34px; place-items: center; border: 1px solid color-mix(in srgb, var(--color-accent) 34%, var(--color-border)); border-radius: 9px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
|
||||
.dialog-header-actions { gap: 7px; }
|
||||
.lfs-main { min-height: 0; overflow: hidden; }
|
||||
.lfs-error { display: flex; align-items: center; gap: 8px; margin: 10px 13px 0; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--lfs-danger) 38%, var(--color-border)); border-radius: 7px; color: var(--lfs-danger); background: color-mix(in srgb, var(--lfs-danger) 8%, transparent); font-size: 11px; }
|
||||
.lfs-loading { display: grid; min-height: 280px; place-items: center; align-content: center; gap: 10px; color: var(--color-ink-dim); font-size: 12px; }
|
||||
.lfs-content { min-height: 0; max-height: 548px; padding: 12px 13px 13px; overflow: auto; }
|
||||
.lfs-diagnostics { display: grid; grid-template-columns: minmax(0, 1fr) 22px minmax(0, 1fr) 22px minmax(0, 1fr); align-items: center; margin-bottom: 11px; }
|
||||
.lfs-diagnostics article { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 9px; min-height: 64px; padding: 8px 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); }
|
||||
.lfs-diagnostics article.ready { border-color: color-mix(in srgb, var(--lfs-success) 34%, var(--color-border)); }
|
||||
.lfs-diagnostics article.warning { border-color: color-mix(in srgb, var(--lfs-warning) 38%, var(--color-border)); }
|
||||
.lfs-diagnostics article.error { border-color: color-mix(in srgb, var(--lfs-danger) 38%, var(--color-border)); }
|
||||
.diagnostic-icon { display: grid; width: 29px; height: 29px; place-items: center; border-radius: 8px; color: var(--color-ink-dim); background: var(--color-surface-hover); }
|
||||
article.ready .diagnostic-icon { color: var(--lfs-success); background: color-mix(in srgb, var(--lfs-success) 10%, transparent); }
|
||||
article.warning .diagnostic-icon { color: var(--lfs-warning); }
|
||||
article.error .diagnostic-icon { color: var(--lfs-danger); }
|
||||
.lfs-diagnostics article div { display: grid; min-width: 0; gap: 2px; }
|
||||
.lfs-diagnostics small { color: var(--color-ink-faint); font-size: 9px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.lfs-diagnostics strong { overflow: hidden; color: var(--color-ink); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lfs-diagnostics article div > span { color: var(--color-ink-dim); font-size: 10px; }
|
||||
.diagnostic-link { display: grid; place-items: center; color: var(--color-border); }
|
||||
.diagnostic-link.ready { color: color-mix(in srgb, var(--lfs-success) 60%, var(--color-border)); }
|
||||
.lfs-missing, .lfs-setup-card { display: flex; align-items: center; gap: 11px; padding: 12px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); }
|
||||
.lfs-missing-icon { display: grid; flex: 0 0 auto; place-items: center; color: var(--lfs-danger); }
|
||||
.lfs-missing strong, .lfs-setup-card strong { color: var(--color-ink); font-size: 12px; }
|
||||
.lfs-missing p, .lfs-setup-card p { margin: 4px 0 0; color: var(--color-ink-dim); font-size: 10.5px; line-height: 1.5; }
|
||||
.lfs-setup-card { justify-content: space-between; margin-bottom: 11px; border-color: color-mix(in srgb, var(--lfs-warning) 32%, var(--color-border)); }
|
||||
.lfs-setup-card > div { max-width: 650px; }
|
||||
.lfs-setup-card button { flex: 0 0 auto; }
|
||||
.lfs-grid { display: grid; grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr); gap: 10px; height: clamp(230px, 30vh, 285px); min-height: 0; }
|
||||
.lfs-panel { display: grid; min-height: 0; grid-template-rows: auto auto minmax(0, 1fr); border: 1px solid var(--color-border-subtle); border-radius: 10px; overflow: hidden; background: var(--color-surface-raised); }
|
||||
.objects-panel { grid-template-rows: auto minmax(0, 1fr); }
|
||||
.lfs-panel > header { display: flex; align-items: center; justify-content: space-between; min-height: 48px; padding: 8px 10px; border-bottom: 1px solid var(--color-border-subtle); background: color-mix(in srgb, var(--app-dialog-chrome) 74%, transparent); }
|
||||
.lfs-panel h3 { margin: 2px 0 0; color: var(--color-ink); font-size: 13px; }
|
||||
.lfs-panel > header > span { display: grid; min-width: 25px; height: 22px; place-items: center; border-radius: 11px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 10%, transparent); font-size: 10px; font-weight: 800; }
|
||||
.lfs-track-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 7px 9px; padding: 9px 10px; border-bottom: 1px solid var(--color-border-subtle); }
|
||||
.lfs-track-form > label:first-child { display: grid; gap: 5px; grid-column: 1 / -1; }
|
||||
.lfs-track-form label > span:first-child { color: var(--color-ink-dim); font-size: 9.5px; font-weight: 700; }
|
||||
.lfs-track-form input[type="text"] { width: 100%; }
|
||||
.lfs-lockable { display: inline-flex; width: fit-content; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
|
||||
.lfs-lockable input[type="checkbox"] { width: 14px; min-width: 14px; height: 14px; margin: 0; padding: 0; border-radius: 3px; accent-color: var(--color-accent); cursor: pointer; box-shadow: none; }
|
||||
.lfs-lockable > span { display: flex; align-items: baseline; gap: 4px; white-space: nowrap; }
|
||||
.lfs-lockable strong { color: var(--color-ink); font-size: 10px; }
|
||||
.lfs-lockable small { color: var(--color-ink-faint); font-size: 8.5px; }
|
||||
.lfs-pattern-list, .lfs-object-list { min-height: 0; overflow: auto; }
|
||||
.lfs-pattern-list article, .lfs-object-list article { display: flex; align-items: center; gap: 9px; min-height: 49px; padding: 8px 10px; border-bottom: 1px solid var(--color-border-subtle); }
|
||||
.lfs-pattern-list article:last-child, .lfs-object-list article:last-child { border-bottom: 0; }
|
||||
.lfs-pattern-list article > div, .lfs-object-list article > div { display: grid; min-width: 0; flex: 1; gap: 3px; }
|
||||
.lfs-pattern-list code, .lfs-object-list strong { overflow: hidden; color: var(--color-ink); font-size: 10.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lfs-pattern-list article span, .lfs-object-list article span { color: var(--color-ink-faint); font-size: 9px; }
|
||||
.object-state { display: grid; width: 24px; height: 24px; flex: 0 0 auto; place-items: center; border-radius: 50%; color: var(--lfs-warning); background: color-mix(in srgb, var(--lfs-warning) 10%, transparent); }
|
||||
.object-state.downloaded { color: var(--lfs-success); background: color-mix(in srgb, var(--lfs-success) 10%, transparent); }
|
||||
.lfs-empty { display: grid; width: 100%; height: 100%; min-height: 80px; place-items: center; align-content: center; gap: 7px; padding: 14px; color: var(--color-ink-faint); text-align: center; }
|
||||
.lfs-empty span { max-width: 260px; font-size: 10px; line-height: 1.45; }
|
||||
.lfs-footer { min-height: 54px; justify-content: space-between; gap: 14px; padding: 9px 13px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
|
||||
.lfs-footer > div { gap: 7px; }
|
||||
.lfs-footer > div:first-child { min-width: 0; color: var(--color-ink-dim); font-size: 9.5px; }
|
||||
.lfs-footer > div:last-child { flex: 0 0 auto; gap: 8px; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.dialog-backdrop { padding: 10px; }
|
||||
.lfs-dialog { width: min(620px, 100%); height: min(760px, 100%); max-height: none; }
|
||||
.lfs-content { height: 100%; max-height: none; }
|
||||
.lfs-diagnostics { grid-template-columns: 1fr; gap: 7px; }
|
||||
.diagnostic-link { display: none; }
|
||||
.lfs-grid { grid-template-columns: 1fr; height: auto; }
|
||||
.lfs-panel { min-height: 210px; }
|
||||
.lfs-footer { align-items: flex-start; flex-direction: column; }
|
||||
.lfs-footer > div:last-child { width: 100%; justify-content: flex-end; }
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.lfs-dialog-header { min-height: 54px; }
|
||||
.lfs-mark { width: 31px; height: 31px; }
|
||||
.lfs-track-form { grid-template-columns: 1fr; }
|
||||
.lfs-track-form > label:first-child { grid-column: 1; }
|
||||
.lfs-track-form button { justify-self: end; }
|
||||
.lfs-lockable > span { white-space: normal; }
|
||||
.lfs-footer > div:first-child { display: none; }
|
||||
.lfs-footer > div:last-child { justify-content: stretch; }
|
||||
.lfs-footer button { flex: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
BookOpen,
|
||||
Box,
|
||||
Check,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
@@ -468,6 +469,126 @@
|
||||
},
|
||||
];
|
||||
|
||||
deCategories.splice(deCategories.findIndex((category) => category.id === "remote") + 1, 0, {
|
||||
id: "lfs",
|
||||
label: "Git LFS",
|
||||
description: "Große Binärdateien tracken, Objekte synchronisieren und bestehende Repositories sicher umstellen.",
|
||||
sections: [
|
||||
{
|
||||
id: "lfs-overview",
|
||||
title: "Was Git LFS macht",
|
||||
summary: "Git LFS ersetzt große Dateien im Git-Verlauf durch kleine Zeigerdateien. Die eigentlichen Inhalte liegen im LFS-Speicher des Remotes und werden beim Checkout oder Pull passend geladen.",
|
||||
steps: [
|
||||
"Nutze LFS vor allem für große Binärdateien wie PSD-, Video-, Audio-, Modell- oder Archivdateien, die Git nicht sinnvoll als Text-Diff verwalten kann.",
|
||||
"Gitty liefert die Git-LFS-Erweiterung in Desktop-Installern mit und zeigt Version, Filter sowie Pre-push-Hook im LFS-Dialog an.",
|
||||
"Die LFS-Regeln stehen in .gitattributes und gehören deshalb wie normaler Quellcode in das Repository.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "lfs-setup",
|
||||
title: "Git LFS in Gitty einrichten",
|
||||
summary: "Die Einrichtung gilt für das aktuell geöffnete Repository und verändert keine globalen Git-Einstellungen.",
|
||||
steps: [
|
||||
"Öffne im Repository das Menü Synchronisieren und wähle Git LFS.",
|
||||
"Klicke auf LFS aktivieren, damit Gitty die lokalen Filter und den Pre-push-Hook einrichtet.",
|
||||
"Füge ein Muster wie *.psd, Assets/** oder video.mp4 hinzu. Lockable markiert Dateien, die über einen kompatiblen LFS-Server gesperrt werden können.",
|
||||
"Stage und committe anschließend .gitattributes zusammen mit den gewünschten Dateien.",
|
||||
],
|
||||
commands: [
|
||||
{ command: "git lfs install --local", description: "LFS nur im aktuellen Repository aktivieren" },
|
||||
{ command: "git lfs track \"*.psd\"", description: "Ein Dateimuster über Git LFS verwalten" },
|
||||
{ command: "git add .gitattributes", description: "Die erzeugten Tracking-Regeln stagen" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "lfs-sync",
|
||||
title: "LFS-Objekte synchronisieren",
|
||||
summary: "Ein normaler Pull in Gitty prüft nach erfolgreicher Git-Synchronisierung automatisch auf LFS und lädt benötigte Objekte mit demselben Remote und denselben Zugangsdaten. Ein zweiter Pull ist nicht nötig.",
|
||||
steps: [
|
||||
"Normales Push nutzt den LFS-Pre-push-Hook und lädt neue LFS-Objekte vor den Git-Referenzen hoch.",
|
||||
"Objekte laden im LFS-Dialog ist ein manueller Reparatur- oder Aktualisierungsschritt, falls lokale Inhalte fehlen.",
|
||||
"Cache bereinigen entfernt sicher nicht mehr benötigte lokale Objekte; aktuell verwendete und noch nicht gepushte Inhalte bleiben erhalten.",
|
||||
],
|
||||
commands: [
|
||||
{ command: "git lfs status", description: "LFS-Zustand und ausstehende Änderungen prüfen" },
|
||||
{ command: "git lfs pull", description: "Benötigte LFS-Objekte manuell laden" },
|
||||
{ command: "git lfs prune", description: "Nicht mehr benötigte lokale LFS-Objekte bereinigen" },
|
||||
{ command: "git lfs lock <datei>", description: "Eine als lockable markierte Datei auf einem kompatiblen Remote sperren" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "lfs-migrate",
|
||||
title: "Bestehende Dateien umstellen",
|
||||
summary: "Ein neu hinzugefügtes Tracking-Muster schreibt vorhandene Commits nicht rückwirkend um. Aktuelle Dateien lassen sich neu normalisieren; eine vollständige Migration verändert dagegen die Historie.",
|
||||
commands: [
|
||||
{ command: "git add --renormalize .", description: "Aktuelle Dateien erneut durch die neuen LFS-Regeln führen" },
|
||||
{ command: "git lfs migrate import --include=\"*.psd\" --everything", description: "Passende Dateien in der gesamten Historie nach LFS migrieren" },
|
||||
],
|
||||
note: "Vorsicht: git lfs migrate import schreibt Commit-Hashes um. Stimme die Migration mit allen Beteiligten ab, erstelle vorher ein Backup und rechne bei bereits veröffentlichten Branches mit einem koordinierten Force-Push.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
enCategories.splice(enCategories.findIndex((category) => category.id === "remote") + 1, 0, {
|
||||
id: "lfs",
|
||||
label: "Git LFS",
|
||||
description: "Track large binary files, synchronize objects, and migrate existing repositories safely.",
|
||||
sections: [
|
||||
{
|
||||
id: "lfs-overview",
|
||||
title: "What Git LFS does",
|
||||
summary: "Git LFS replaces large files in Git history with small pointer files. The actual content is stored in the remote's LFS storage and downloaded for the relevant checkout or pull.",
|
||||
steps: [
|
||||
"Use LFS mainly for large binary files such as PSDs, videos, audio, models, or archives that Git cannot usefully manage as text diffs.",
|
||||
"Gitty bundles the Git LFS extension in desktop installers and displays its version, filters, and pre-push hook in the LFS dialog.",
|
||||
"LFS rules live in .gitattributes, so commit them to the repository like regular source code.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "lfs-setup",
|
||||
title: "Set up Git LFS in Gitty",
|
||||
summary: "Setup applies to the currently open repository and does not change global Git settings.",
|
||||
steps: [
|
||||
"Open the Sync menu in the repository and select Git LFS.",
|
||||
"Select Activate LFS so Gitty configures the local filters and pre-push hook.",
|
||||
"Add a pattern such as *.psd, Assets/**, or video.mp4. Lockable marks files that can be locked through a compatible LFS server.",
|
||||
"Stage and commit .gitattributes together with the files you want to track.",
|
||||
],
|
||||
commands: [
|
||||
{ command: "git lfs install --local", description: "Activate LFS only in the current repository" },
|
||||
{ command: "git lfs track \"*.psd\"", description: "Manage a file pattern through Git LFS" },
|
||||
{ command: "git add .gitattributes", description: "Stage the generated tracking rules" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "lfs-sync",
|
||||
title: "Synchronize LFS objects",
|
||||
summary: "After a successful regular pull, Gitty automatically checks for LFS and downloads required objects with the same remote and credentials. A second pull is not necessary.",
|
||||
steps: [
|
||||
"A normal push uses the LFS pre-push hook to upload new LFS objects before Git references are published.",
|
||||
"Pull objects in the LFS dialog is a manual repair or refresh action when local content is missing.",
|
||||
"Prune cache safely removes unused local objects while retaining current and unpushed content.",
|
||||
],
|
||||
commands: [
|
||||
{ command: "git lfs status", description: "Inspect LFS state and pending changes" },
|
||||
{ command: "git lfs pull", description: "Download required LFS objects manually" },
|
||||
{ command: "git lfs prune", description: "Remove unused local LFS objects" },
|
||||
{ command: "git lfs lock <file>", description: "Lock a lockable file on a compatible remote" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "lfs-migrate",
|
||||
title: "Migrate existing files",
|
||||
summary: "Adding a tracking pattern does not rewrite existing commits. Current files can be renormalized, while a complete migration changes repository history.",
|
||||
commands: [
|
||||
{ command: "git add --renormalize .", description: "Run current files through the new LFS rules again" },
|
||||
{ command: "git lfs migrate import --include=\"*.psd\" --everything", description: "Move matching files to LFS throughout repository history" },
|
||||
],
|
||||
note: "Caution: git lfs migrate import rewrites commit hashes. Coordinate the migration with every contributor, create a backup first, and expect a coordinated force push for published branches.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Extended handbook chapters. Keeping these additions next to the shared data makes
|
||||
// it straightforward to compare the German and English coverage section by section.
|
||||
deCategories.find((category) => category.id === "start")?.sections.push(
|
||||
@@ -1703,6 +1824,7 @@
|
||||
{:else if category.id === "basics"}<GitCommitHorizontal size={17} aria-hidden="true" />
|
||||
{:else if category.id === "branches"}<GitBranch size={17} aria-hidden="true" />
|
||||
{:else if category.id === "remote"}<Cloud size={17} aria-hidden="true" />
|
||||
{:else if category.id === "lfs"}<Box size={17} aria-hidden="true" />
|
||||
{:else if category.id === "troubleshooting"}<Wrench size={17} aria-hidden="true" />
|
||||
{:else if category.id === "workflows"}<ListChecks size={17} aria-hidden="true" />
|
||||
{:else if category.id === "reference"}<Library size={17} aria-hidden="true" />
|
||||
@@ -1715,7 +1837,7 @@
|
||||
|
||||
<div class="help-nav-tip">
|
||||
<span class="help-tip-icon"><Lightbulb size={16} aria-hidden="true" /></span>
|
||||
<span>{isGerman ? "Suche auch nach Befehlen wie" : "Try commands such as"} <code>rebase</code>, <code>stash</code> {isGerman ? "oder" : "or"} <code>reflog</code>.</span>
|
||||
<span>{isGerman ? "Suche auch nach Befehlen wie" : "Try commands such as"} <code>rebase</code>, <code>lfs</code> {isGerman ? "oder" : "or"} <code>reflog</code>.</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
GitCommitComparison,
|
||||
GitLfsStatus,
|
||||
GitRepositoryFile,
|
||||
GitRemote,
|
||||
MergeStrategy,
|
||||
@@ -90,6 +91,35 @@ export function getStatus(path: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("get_status", { path });
|
||||
}
|
||||
|
||||
export function getGitLfsStatus(path: string): Promise<GitLfsStatus> {
|
||||
return invoke<GitLfsStatus>("git_lfs_status", { path });
|
||||
}
|
||||
|
||||
export function installGitLfs(path: string): Promise<GitLfsStatus> {
|
||||
return invoke<GitLfsStatus>("git_lfs_install", { path });
|
||||
}
|
||||
|
||||
export function trackGitLfsPattern(path: string, pattern: string, lockable = false): Promise<GitLfsStatus> {
|
||||
return invoke<GitLfsStatus>("git_lfs_track", { path, pattern, lockable });
|
||||
}
|
||||
|
||||
export function untrackGitLfsPattern(path: string, pattern: string): Promise<GitLfsStatus> {
|
||||
return invoke<GitLfsStatus>("git_lfs_untrack", { path, pattern });
|
||||
}
|
||||
|
||||
export function pullGitLfsObjects(path: string, remote?: string, username?: string, password?: string): Promise<GitLfsStatus> {
|
||||
return invoke<GitLfsStatus>("git_lfs_pull", {
|
||||
path,
|
||||
remote: remote || null,
|
||||
username: username ?? null,
|
||||
password: password ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function pruneGitLfsObjects(path: string): Promise<GitLfsStatus> {
|
||||
return invoke<GitLfsStatus>("git_lfs_prune", { path });
|
||||
}
|
||||
|
||||
// Sets the taskbar icon badge to ahead + behind + changed status files (0 clears it). Windows only — a no-op on
|
||||
// other platforms, since Windows has no native numeric badge to fall back to.
|
||||
export function setSyncBadge(ahead: number, behind: number, changes: number): Promise<void> {
|
||||
|
||||
@@ -193,6 +193,34 @@ export interface GitRepositoryFile {
|
||||
status: FileStatusKind | null;
|
||||
}
|
||||
|
||||
export interface GitLfsPattern {
|
||||
pattern: string;
|
||||
source: string;
|
||||
lockable: boolean;
|
||||
tracked: boolean;
|
||||
}
|
||||
|
||||
export interface GitLfsFile {
|
||||
name: string;
|
||||
size: number;
|
||||
checkout: boolean;
|
||||
downloaded: boolean;
|
||||
oid_type: string;
|
||||
oid: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface GitLfsStatus {
|
||||
available: boolean;
|
||||
bundled: boolean;
|
||||
version: string | null;
|
||||
filters_configured: boolean;
|
||||
hook_installed: boolean;
|
||||
repository_uses_lfs: boolean;
|
||||
patterns: GitLfsPattern[];
|
||||
files: GitLfsFile[];
|
||||
}
|
||||
|
||||
export interface RepositoryBundle {
|
||||
status: GitStatus;
|
||||
branches: GitBranch[];
|
||||
|
||||
Reference in New Issue
Block a user