Git LFS section is live with:

Enable LFS toggle — turns LFS on/off for the repo; all other controls dim when disabled
File locking toggle — enables the LFS locking protocol for binary assets
Maximum file size — optional per-file size cap in MB (blank = unlimited)
Info callout linking to the git-lfs client install page and noting the .gitattributes requirement
This commit is contained in:
2026-05-07 16:12:25 +02:00
parent 39eeccb314
commit 803672a610
7 changed files with 328 additions and 2 deletions
+139 -1
View File
@@ -14,6 +14,7 @@ import {
useDefaultDescription, useUpdateDefaultDescription,
useExcludedFiles, useUpdateExcludedFiles,
} from '../api/queries/prs'
import { useLFSSettings, useUpdateLFSSettings } from '../api/queries/lfs'
import { useRecentRepos } from '../hooks/useRecentRepos'
import { useAuth } from '../contexts/AuthContext'
import { Skeleton } from '../ui/Skeleton'
@@ -153,9 +154,10 @@ export default function RepoSettingsPage() {
{section === 'default-reviewers' && <DefaultReviewersSection owner={owner} repo={repoName} />}
{section === 'default-description' && <DefaultDescriptionSection owner={owner} repo={repoName} />}
{section === 'excluded-files' && <ExcludedFilesSection owner={owner} repo={repoName} />}
{section === 'git-lfs' && <GitLFSSection owner={owner} repo={repoName} />}
{!['repository-details','repository-permissions','access-keys','access-tokens',
'branch-restrictions','branching-model','merge-strategies','webhooks',
'default-reviewers','default-description','excluded-files'].includes(section) && <ComingSoon sectionId={section} />}
'default-reviewers','default-description','excluded-files','git-lfs'].includes(section) && <ComingSoon sectionId={section} />}
</main>
</div>
)
@@ -1680,6 +1682,142 @@ function ExcludedFilesSection({ owner, repo }: { owner: string; repo: string })
)
}
// ─── Git LFS ─────────────────────────────────────────────────────────────────
function GitLFSSection({ owner, repo }: { owner: string; repo: string }) {
const { data, isLoading } = useLFSSettings(owner, repo)
const update = useUpdateLFSSettings(owner, repo)
const [maxSizeInput, setMaxSizeInput] = useState('')
const [sizeError, setSizeError] = useState('')
const [saved, setSaved] = useState(false)
useEffect(() => {
if (data) setMaxSizeInput(data.maxFileSizeMB === 0 ? '' : String(data.maxFileSizeMB))
}, [data])
async function toggle(field: 'enabled' | 'lockingEnabled', value: boolean) {
await update.mutateAsync({ [field]: value })
}
async function saveMaxSize(e: React.FormEvent) {
e.preventDefault()
setSizeError('')
const raw = maxSizeInput.trim()
const mb = raw === '' ? 0 : parseInt(raw, 10)
if (raw !== '' && (isNaN(mb) || mb < 0)) {
setSizeError('Enter a positive number of megabytes, or leave blank for unlimited.')
return
}
await update.mutateAsync({ maxFileSizeMB: mb })
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
if (isLoading) {
return (
<div className="max-w-2xl px-6 py-6 space-y-4">
<Skeleton className="h-6 w-48 rounded" />
<Skeleton className="h-24 rounded" />
<Skeleton className="h-24 rounded" />
</div>
)
}
const enabled = data?.enabled ?? false
const lockingEnabled = data?.lockingEnabled ?? true
return (
<div className="max-w-2xl px-6 py-6 space-y-6">
<div>
<h1 className="text-xl font-semibold text-[var(--c-text)]">Git LFS</h1>
<p className="text-sm text-[var(--c-muted)] mt-1">
Git Large File Storage replaces large binary files (images, videos, datasets) with lightweight text pointers inside Git,
while storing the actual file content on the server.
</p>
</div>
{/* Enable / disable */}
<div className="border border-[var(--c-border)] rounded-lg divide-y divide-[var(--c-border)]">
<div className="flex items-start justify-between gap-4 px-4 py-4">
<div>
<p className="text-sm font-medium text-[var(--c-text)]">Enable Git LFS</p>
<p className="text-xs text-[var(--c-muted)] mt-0.5">
Allow contributors to push large files using the LFS protocol.
</p>
</div>
<button
onClick={() => toggle('enabled', !enabled)}
disabled={update.isPending}
className={`relative shrink-0 inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${enabled ? 'bg-[var(--c-brand)]' : 'bg-[var(--c-border)]'}`}
>
<span className={`inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${enabled ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
<div className={`flex items-start justify-between gap-4 px-4 py-4 transition-opacity ${enabled ? '' : 'opacity-40 pointer-events-none'}`}>
<div>
<p className="text-sm font-medium text-[var(--c-text)]">File locking</p>
<p className="text-xs text-[var(--c-muted)] mt-0.5">
Allow contributors to lock LFS files to prevent conflicting edits on binary assets.
</p>
</div>
<button
onClick={() => toggle('lockingEnabled', !lockingEnabled)}
disabled={update.isPending || !enabled}
className={`relative shrink-0 inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${lockingEnabled ? 'bg-[var(--c-brand)]' : 'bg-[var(--c-border)]'}`}
>
<span className={`inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${lockingEnabled ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
</div>
{/* Max file size */}
<div className={`space-y-3 transition-opacity ${enabled ? '' : 'opacity-40 pointer-events-none'}`}>
<div>
<p className="text-sm font-medium text-[var(--c-text)]">Maximum file size</p>
<p className="text-xs text-[var(--c-muted)] mt-0.5">
Reject LFS pushes for individual files exceeding this size. Leave blank for no limit.
</p>
</div>
<form onSubmit={saveMaxSize} className="flex items-center gap-2">
<div className="relative">
<input
type="number"
min="1"
value={maxSizeInput}
onChange={e => setMaxSizeInput(e.target.value)}
disabled={!enabled}
placeholder="Unlimited"
className="w-36 border border-[var(--c-border)] rounded px-3 py-2 text-sm bg-[var(--c-surface)] text-[var(--c-text)] focus:outline-none focus:border-[var(--c-brand-focus)] disabled:opacity-50 pr-10"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-[var(--c-muted)] pointer-events-none">MB</span>
</div>
<button
type="submit"
disabled={update.isPending || !enabled}
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50"
>
{update.isPending ? 'Saving…' : 'Save'}
</button>
{saved && <span className="text-xs text-[var(--c-success)]">Saved</span>}
</form>
{sizeError && <p className="text-xs text-[var(--c-danger)]">{sizeError}</p>}
</div>
{/* Info box */}
<div className="rounded-lg bg-[var(--c-surface-muted)] border border-[var(--c-border)] px-4 py-3 flex gap-3">
<svg className="shrink-0 mt-0.5 text-[var(--c-brand)]" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
<p className="text-xs text-[var(--c-muted)] leading-relaxed">
Git LFS must be <a href="https://git-lfs.com" target="_blank" rel="noreferrer" className="text-[var(--c-brand)] hover:underline">installed on the client</a> to push and pull tracked files.
Add a <code className="font-mono bg-[var(--c-surface)] px-1 rounded">.gitattributes</code> file to your repository to specify which file patterns LFS should track.
</p>
</div>
</div>
)
}
// ─── Coming soon ──────────────────────────────────────────────────────────────
function ComingSoon({ sectionId }: { sectionId: SectionId }) {