feat(git-sync): independent toggles for project files and PDF push
Build and Deploy Verso / deploy (push) Successful in 12m48s
Build and Deploy Verso / deploy (push) Successful in 12m48s
Two new boolean fields on the project (gitSyncPushFiles, gitSyncPushPdf, both default true) let users control what gets pushed independently: - "Push project files" switch — skip all docs/binary files when off - "Push compiled PDF" switch — grayed out when no pdfPath is set The push button and auto-push are disabled when both switches would result in nothing being pushed. Config is stored in MongoDB so settings persist per-project. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { expressify } from '@overleaf/promise-utils'
|
||||
|
||||
async function configureGitSync(req, res) {
|
||||
const projectId = req.params.project_id
|
||||
const { remoteUrl, subPath = '', pdfPath = '' } = req.body
|
||||
const { remoteUrl, subPath = '', pdfPath = '', pushFiles = true, pushPdf = true } = req.body
|
||||
|
||||
if (typeof remoteUrl !== 'string' || remoteUrl.trim() === '') {
|
||||
return res.status(400).json({ error: 'remoteUrl is required' })
|
||||
@@ -30,6 +30,8 @@ async function configureGitSync(req, res) {
|
||||
remoteUrl: trimmedUrl,
|
||||
subPath: trimmedSubPath,
|
||||
pdfPath: trimmedPdfPath,
|
||||
pushFiles: Boolean(pushFiles),
|
||||
pushPdf: Boolean(pushPdf),
|
||||
})
|
||||
logger.debug({ projectId }, 'git sync: config saved')
|
||||
res.sendStatus(204)
|
||||
@@ -40,7 +42,8 @@ async function pushToGit(req, res) {
|
||||
try {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
const { buildId, clsiServerId } = req.body ?? {}
|
||||
const { remoteUrl, subPath, pdfPath } = await GitSyncHandler.getConfig(projectId)
|
||||
const { remoteUrl, subPath, pdfPath, pushFiles, pushPdf } =
|
||||
await GitSyncHandler.getConfig(projectId)
|
||||
if (!remoteUrl) {
|
||||
return res.status(400).json({ error: 'No git remote configured for this project' })
|
||||
}
|
||||
@@ -50,6 +53,8 @@ async function pushToGit(req, res) {
|
||||
pdfBuildId: buildId,
|
||||
pdfClsiServerId: clsiServerId,
|
||||
userId,
|
||||
pushFiles,
|
||||
pushPdf,
|
||||
})
|
||||
res.sendStatus(204)
|
||||
} catch (err) {
|
||||
@@ -82,6 +87,8 @@ async function getGitSyncConfig(req, res) {
|
||||
remoteUrl: config.remoteUrl ?? '',
|
||||
subPath: config.subPath,
|
||||
pdfPath: config.pdfPath,
|
||||
pushFiles: config.pushFiles,
|
||||
pushPdf: config.pushPdf,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,18 @@ async function spawnGit(args, cwd) {
|
||||
})
|
||||
}
|
||||
|
||||
async function setConfig(projectId, { remoteUrl, subPath, pdfPath }) {
|
||||
async function setConfig(projectId, { remoteUrl, subPath, pdfPath, pushFiles, pushPdf }) {
|
||||
await Project.updateOne(
|
||||
{ _id: new ObjectId(projectId) },
|
||||
{ $set: { gitRemote: remoteUrl, gitSyncPath: subPath, gitSyncPdfPath: pdfPath } }
|
||||
{
|
||||
$set: {
|
||||
gitRemote: remoteUrl,
|
||||
gitSyncPath: subPath,
|
||||
gitSyncPdfPath: pdfPath,
|
||||
gitSyncPushFiles: pushFiles,
|
||||
gitSyncPushPdf: pushPdf,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,11 +79,15 @@ async function getConfig(projectId) {
|
||||
gitRemote: 1,
|
||||
gitSyncPath: 1,
|
||||
gitSyncPdfPath: 1,
|
||||
gitSyncPushFiles: 1,
|
||||
gitSyncPushPdf: 1,
|
||||
}).lean()
|
||||
return {
|
||||
remoteUrl: project?.gitRemote ?? null,
|
||||
subPath: project?.gitSyncPath ?? '',
|
||||
pdfPath: project?.gitSyncPdfPath ?? '',
|
||||
pushFiles: project?.gitSyncPushFiles ?? true,
|
||||
pushPdf: project?.gitSyncPushPdf ?? true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,39 +95,41 @@ async function pushToRemote(
|
||||
projectId,
|
||||
remoteUrl,
|
||||
subPath,
|
||||
{ pdfPath, pdfBuildId, pdfClsiServerId, userId } = {}
|
||||
{ pdfPath, pdfBuildId, pdfClsiServerId, userId, pushFiles = true, pushPdf = true } = {}
|
||||
) {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), 'verso-git-'))
|
||||
logger.debug({ projectId, tmpDir, subPath }, 'git sync: writing project to temp dir')
|
||||
logger.debug({ projectId, tmpDir, subPath, pushFiles, pushPdf }, 'git sync: writing project to temp dir')
|
||||
try {
|
||||
const fileRoot = subPath ? join(tmpDir, subPath) : tmpDir
|
||||
|
||||
// Fetch all text documents
|
||||
const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)
|
||||
for (const [path, doc] of Object.entries(docs)) {
|
||||
const rel = path.startsWith('/') ? path.slice(1) : path
|
||||
const dest = join(fileRoot, rel)
|
||||
await mkdir(dirname(dest), { recursive: true })
|
||||
await writeFile(dest, doc.lines.join('\n'))
|
||||
}
|
||||
if (pushFiles) {
|
||||
// Fetch all text documents
|
||||
const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)
|
||||
for (const [path, doc] of Object.entries(docs)) {
|
||||
const rel = path.startsWith('/') ? path.slice(1) : path
|
||||
const dest = join(fileRoot, rel)
|
||||
await mkdir(dirname(dest), { recursive: true })
|
||||
await writeFile(dest, doc.lines.join('\n'))
|
||||
}
|
||||
|
||||
// Fetch all binary files
|
||||
const files = await ProjectEntityHandler.promises.getAllFiles(projectId)
|
||||
for (const [path, file] of Object.entries(files)) {
|
||||
const rel = path.startsWith('/') ? path.slice(1) : path
|
||||
const dest = join(fileRoot, rel)
|
||||
await mkdir(dirname(dest), { recursive: true })
|
||||
const { stream } =
|
||||
await HistoryManager.promises.requestBlobWithProjectId(
|
||||
projectId,
|
||||
file.hash
|
||||
)
|
||||
await pipeline(stream, createWriteStream(dest))
|
||||
// Fetch all binary files
|
||||
const files = await ProjectEntityHandler.promises.getAllFiles(projectId)
|
||||
for (const [path, file] of Object.entries(files)) {
|
||||
const rel = path.startsWith('/') ? path.slice(1) : path
|
||||
const dest = join(fileRoot, rel)
|
||||
await mkdir(dirname(dest), { recursive: true })
|
||||
const { stream } =
|
||||
await HistoryManager.promises.requestBlobWithProjectId(
|
||||
projectId,
|
||||
file.hash
|
||||
)
|
||||
await pipeline(stream, createWriteStream(dest))
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally include the compiled PDF at a configured path
|
||||
// clsiServerId is optional (absent in single-server CE deployments)
|
||||
if (pdfPath && pdfBuildId) {
|
||||
if (pushPdf && pdfPath && pdfBuildId) {
|
||||
const pdfStream = await ClsiManager.promises.getOutputFileStream(
|
||||
projectId,
|
||||
userId,
|
||||
|
||||
@@ -607,6 +607,8 @@ const _ProjectController = {
|
||||
gitRemote: 1,
|
||||
gitSyncPath: 1,
|
||||
gitSyncPdfPath: 1,
|
||||
gitSyncPushFiles: 1,
|
||||
gitSyncPushPdf: 1,
|
||||
}),
|
||||
userIsMemberOfGroupSubscription: sessionUser
|
||||
? (async () =>
|
||||
@@ -1029,6 +1031,8 @@ const _ProjectController = {
|
||||
gitRemote: project.gitRemote ?? '',
|
||||
gitSyncPath: project.gitSyncPath ?? '',
|
||||
gitSyncPdfPath: project.gitSyncPdfPath ?? '',
|
||||
gitSyncPushFiles: project.gitSyncPushFiles ?? true,
|
||||
gitSyncPushPdf: project.gitSyncPushPdf ?? true,
|
||||
wsUrl,
|
||||
showSupport: Features.hasFeature('support'),
|
||||
showTemplatesServerPro,
|
||||
|
||||
@@ -111,6 +111,8 @@ export const ProjectSchema = new Schema(
|
||||
gitRemote: { type: String },
|
||||
gitSyncPath: { type: String },
|
||||
gitSyncPdfPath: { type: String },
|
||||
gitSyncPushFiles: { type: Boolean },
|
||||
gitSyncPushPdf: { type: Boolean },
|
||||
},
|
||||
{ minimize: false }
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ meta(name="ol-gitSyncEnabled" data-type="boolean" content=gitSyncEnabled)
|
||||
meta(name="ol-gitRemote" content=gitRemote)
|
||||
meta(name="ol-gitSyncPath" content=gitSyncPath)
|
||||
meta(name="ol-gitSyncPdfPath" content=gitSyncPdfPath)
|
||||
meta(name="ol-gitSyncPushFiles" data-type="boolean" content=gitSyncPushFiles)
|
||||
meta(name="ol-gitSyncPushPdf" data-type="boolean" content=gitSyncPushPdf)
|
||||
meta(name="ol-compilesUserContentDomain" content=settings.compilesUserContentDomain)
|
||||
//- enable doc hash checking for all projects
|
||||
//- used in public/js/libs/sharejs.js
|
||||
|
||||
@@ -805,6 +805,8 @@
|
||||
"git_sync_remote_url": "",
|
||||
"git_sync_saved": "",
|
||||
"git_sync_auto_push_on_compile": "",
|
||||
"git_sync_push_files": "",
|
||||
"git_sync_push_pdf": "",
|
||||
"git_sync_pdf_path": "",
|
||||
"git_sync_pdf_path_placeholder": "",
|
||||
"git_sync_sub_path": "",
|
||||
|
||||
@@ -25,6 +25,8 @@ export default function GitSyncWidget() {
|
||||
const [remoteUrl, setRemoteUrl] = useState(getMeta('ol-gitRemote') ?? '')
|
||||
const [subPath, setSubPath] = useState(getMeta('ol-gitSyncPath') ?? '')
|
||||
const [pdfPath, setPdfPath] = useState(getMeta('ol-gitSyncPdfPath') ?? '')
|
||||
const [pushFiles, setPushFiles] = useState(getMeta('ol-gitSyncPushFiles') ?? true)
|
||||
const [pushPdf, setPushPdf] = useState(getMeta('ol-gitSyncPushPdf') ?? true)
|
||||
const [autoPush, setAutoPush] = useState(
|
||||
() => localStorage.getItem(AUTO_PUSH_KEY) === 'true'
|
||||
)
|
||||
@@ -35,7 +37,6 @@ export default function GitSyncWidget() {
|
||||
const wasCompilingRef = useRef(false)
|
||||
const isBusyRef = useRef(false)
|
||||
|
||||
// Keep isBusyRef in sync so the compile watcher can check it
|
||||
useEffect(() => {
|
||||
isBusyRef.current = status === 'busy'
|
||||
}, [status])
|
||||
@@ -58,7 +59,7 @@ export default function GitSyncWidget() {
|
||||
|
||||
async function saveConfig() {
|
||||
await postJSON(`/project/${projectId}/git-sync`, {
|
||||
body: { remoteUrl, subPath, pdfPath },
|
||||
body: { remoteUrl, subPath, pdfPath, pushFiles, pushPdf },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,6 +122,7 @@ export default function GitSyncWidget() {
|
||||
}
|
||||
|
||||
const isBusy = status === 'busy'
|
||||
const nothingToPush = !pushFiles && (!pushPdf || !pdfPath.trim())
|
||||
|
||||
return (
|
||||
<div className="git-sync-widget">
|
||||
@@ -153,6 +155,17 @@ export default function GitSyncWidget() {
|
||||
/>
|
||||
</OLFormGroup>
|
||||
|
||||
<OLFormGroup controlId="git-sync-push-files">
|
||||
<FormCheck
|
||||
type="switch"
|
||||
id="git-sync-push-files"
|
||||
label={t('git_sync_push_files')}
|
||||
checked={pushFiles}
|
||||
onChange={e => setPushFiles(e.target.checked)}
|
||||
disabled={isBusy}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
|
||||
<OLFormGroup controlId="git-sync-pdf-path">
|
||||
<OLFormLabel>{t('git_sync_pdf_path')}</OLFormLabel>
|
||||
<OLFormControl
|
||||
@@ -164,6 +177,17 @@ export default function GitSyncWidget() {
|
||||
/>
|
||||
</OLFormGroup>
|
||||
|
||||
<OLFormGroup controlId="git-sync-push-pdf">
|
||||
<FormCheck
|
||||
type="switch"
|
||||
id="git-sync-push-pdf"
|
||||
label={t('git_sync_push_pdf')}
|
||||
checked={pushPdf}
|
||||
onChange={e => setPushPdf(e.target.checked)}
|
||||
disabled={isBusy || !pdfPath.trim()}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
|
||||
<OLFormGroup controlId="git-sync-auto-push">
|
||||
<FormCheck
|
||||
type="switch"
|
||||
@@ -171,7 +195,7 @@ export default function GitSyncWidget() {
|
||||
label={t('git_sync_auto_push_on_compile')}
|
||||
checked={autoPush}
|
||||
onChange={handleAutoPushToggle}
|
||||
disabled={isBusy || !remoteUrl.trim()}
|
||||
disabled={isBusy || !remoteUrl.trim() || nothingToPush}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
|
||||
@@ -191,7 +215,7 @@ export default function GitSyncWidget() {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handlePush}
|
||||
disabled={isBusy || !remoteUrl.trim()}
|
||||
disabled={isBusy || !remoteUrl.trim() || nothingToPush}
|
||||
isLoading={isBusy && lastAction === 'push'}
|
||||
loadingLabel={t('git_sync_pushing')}
|
||||
>
|
||||
|
||||
@@ -140,6 +140,8 @@ export interface Meta {
|
||||
'ol-gitRemote': string
|
||||
'ol-gitSyncPath': string
|
||||
'ol-gitSyncPdfPath': string
|
||||
'ol-gitSyncPushFiles': boolean
|
||||
'ol-gitSyncPushPdf': boolean
|
||||
'ol-github': { enabled: boolean; error: boolean }
|
||||
'ol-groupAuditLogs': []
|
||||
'ol-groupDomains': []
|
||||
|
||||
@@ -1054,6 +1054,8 @@
|
||||
"git_sync_remote_url": "Remote URL (with auth token)",
|
||||
"git_sync_saved": "Settings saved.",
|
||||
"git_sync_auto_push_on_compile": "Auto-push on successful compile",
|
||||
"git_sync_push_files": "Push project files",
|
||||
"git_sync_push_pdf": "Push compiled PDF",
|
||||
"git_sync_pdf_path": "PDF destination path (optional)",
|
||||
"git_sync_pdf_path_placeholder": "e.g. output/main.pdf (leave blank to skip PDF)",
|
||||
"git_sync_sub_path": "Subdirectory (optional)",
|
||||
|
||||
@@ -1057,6 +1057,8 @@
|
||||
"git_sync_remote_url": "URL distante (avec jeton d'auth)",
|
||||
"git_sync_saved": "Paramètres sauvegardés.",
|
||||
"git_sync_auto_push_on_compile": "Pousser automatiquement après compilation réussie",
|
||||
"git_sync_push_files": "Pousser les fichiers du projet",
|
||||
"git_sync_push_pdf": "Pousser le PDF compilé",
|
||||
"git_sync_pdf_path": "Chemin de destination du PDF (optionnel)",
|
||||
"git_sync_pdf_path_placeholder": "ex. output/main.pdf (laisser vide pour ne pas inclure le PDF)",
|
||||
"git_sync_sub_path": "Sous-dossier (optionnel)",
|
||||
|
||||
Reference in New Issue
Block a user