feat(git-sync): independent toggles for project files and PDF push
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:
claude
2026-06-23 23:21:37 +00:00
co-authored by Claude Sonnet 4.6
parent 9a474f7790
commit be8aef44fe
10 changed files with 92 additions and 31 deletions
@@ -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,
+2
View File
@@ -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