Files
Verso/services/web/frontend/js/features/integrations-panel/git-sync-widget.tsx
T
claude 8214ca6121 feat: add pragmatic git sync — push project to external git remote
Adds a "Git Sync" section in the Integrations rail panel that lets
project owners configure an HTTPS remote URL (with embedded auth token)
and force-push all project files as a single commit.

Backend:
- GitSyncHandler: assembles project docs + binary files into a temp dir,
  runs git init/commit/push --force, then cleans up
- GitSyncController: GET/POST /project/:id/git-sync (configure),
  POST /project/:id/git-sync/push (trigger)
- Project model: gitRemote field
- Dockerfile: ensures git is present at runtime
- Env flag: OVERLEAF_ENABLE_GIT_SYNC=true (set in k8s manifest)

Frontend:
- GitSyncWidget: URL input + Save + Push Now buttons, success/error feedback
- Integrations panel: shows widget when gitSyncEnabled
- Rail: shows Integrations tab when gitSyncEnabled (was only gitBridgeEnabled)
- i18n: en + fr translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 20:01:16 +00:00

117 lines
3.5 KiB
TypeScript

import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import { postJSON } from '@/infrastructure/fetch-json'
import OLButton from '@/shared/components/ol/ol-button'
import OLFormControl from '@/shared/components/ol/ol-form-control'
import OLFormGroup from '@/shared/components/ol/ol-form-group'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
import MaterialIcon from '@/shared/components/material-icon'
import OLNotification from '@/shared/components/ol/ol-notification'
type Action = 'save' | 'push'
type Status = 'idle' | 'busy' | 'success' | 'error'
export default function GitSyncWidget() {
const { t } = useTranslation()
const projectId = getMeta('ol-project_id')
const initialRemote = getMeta('ol-gitRemote')
const [remoteUrl, setRemoteUrl] = useState(initialRemote ?? '')
const [lastAction, setLastAction] = useState<Action>('save')
const [status, setStatus] = useState<Status>('idle')
const [errorMsg, setErrorMsg] = useState('')
async function handleSave(e: React.FormEvent) {
e.preventDefault()
setLastAction('save')
setStatus('busy')
setErrorMsg('')
try {
await postJSON(`/project/${projectId}/git-sync`, {
body: { remoteUrl },
})
setStatus('success')
} catch (err: any) {
setStatus('error')
setErrorMsg(err?.data?.error ?? String(err))
}
}
async function handlePush() {
setLastAction('push')
setStatus('busy')
setErrorMsg('')
try {
await postJSON(`/project/${projectId}/git-sync/push`, { body: {} })
setStatus('success')
} catch (err: any) {
setStatus('error')
setErrorMsg(err?.data?.error ?? String(err))
}
}
const isBusy = status === 'busy'
return (
<div className="git-sync-widget">
<div className="git-sync-widget-header">
<MaterialIcon type="merge" />
<span className="git-sync-widget-title">{t('git_sync')}</span>
</div>
<p className="git-sync-widget-description">{t('git_sync_description')}</p>
<form onSubmit={handleSave}>
<OLFormGroup controlId="git-sync-remote-url">
<OLFormLabel>{t('git_sync_remote_url')}</OLFormLabel>
<OLFormControl
type="text"
value={remoteUrl}
onChange={e => setRemoteUrl(e.target.value)}
placeholder="https://user:token@github.com/org/repo.git"
disabled={isBusy}
/>
</OLFormGroup>
<div className="git-sync-widget-actions">
<OLButton
type="submit"
variant="secondary"
size="sm"
disabled={isBusy || !remoteUrl.trim()}
isLoading={isBusy && lastAction === 'save'}
loadingLabel={t('saving')}
>
{t('save')}
</OLButton>
<OLButton
type="button"
variant="primary"
size="sm"
onClick={handlePush}
disabled={isBusy || !remoteUrl.trim()}
isLoading={isBusy && lastAction === 'push'}
loadingLabel={t('git_sync_pushing')}
>
{t('git_sync_push_now')}
</OLButton>
</div>
</form>
{status === 'success' && (
<OLNotification
type="success"
content={
lastAction === 'push'
? t('git_sync_push_success')
: t('git_sync_saved')
}
/>
)}
{status === 'error' && (
<OLNotification type="error" content={errorMsg} />
)}
</div>
)
}