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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user