Files
Verso/services/web/app/src/Features/GitSync/GitSyncController.mjs
T
claudeandClaude Sonnet 4.6 0caa4ff144
Build and Deploy Verso / deploy (push) Successful in 12m35s
feat(git-sync): configurable branch, first-push confirmation, PDF-only replace
Branch:
- New "Branch" text field (default: main) stored in gitSyncBranch on
  the project. Used for git fetch, symbolic-ref, and push target.
  Validated server-side: alphanumeric + / _ . - only, no ..

First-push confirmation:
- When pushFiles is enabled and the user hasn't confirmed for the
  current subPath, clicking "Push now" shows an inline warning:
  "The <subPath> directory will be completely replaced …"
  with "Yes, push and replace" / "Cancel" buttons.
- Confirmation is stored in localStorage keyed by projectId+subPath,
  so it's shown again if the subPath is changed.
- Auto-push bypasses the dialog (user already opted in explicitly).

PDF-only replace:
- Already correct with the fetch-first approach: only the specific
  file at pdfPath is written; other files in the same directory
  (e.g. output/old.pdf) are preserved from the fetched remote state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 07:23:53 +00:00

116 lines
3.7 KiB
JavaScript

import logger from '@overleaf/logger'
import GitSyncHandler from './GitSyncHandler.mjs'
import SessionManager from '../Authentication/SessionManager.mjs'
import { expressify } from '@overleaf/promise-utils'
async function configureGitSync(req, res) {
const projectId = req.params.project_id
const {
remoteUrl,
subPath = '',
pdfPath = '',
pushFiles = true,
pushPdf = true,
branch = 'main',
} = req.body
if (typeof remoteUrl !== 'string' || remoteUrl.trim() === '') {
return res.status(400).json({ error: 'remoteUrl is required' })
}
const trimmedUrl = remoteUrl.trim()
if (!trimmedUrl.startsWith('https://') && !trimmedUrl.startsWith('git@')) {
return res.status(400).json({ error: 'remoteUrl must start with https:// or git@' })
}
const trimmedSubPath = String(subPath).trim().replace(/^\/+|\/+$/g, '')
if (trimmedSubPath.includes('..')) {
return res.status(400).json({ error: 'subPath must not contain ..' })
}
const trimmedPdfPath = String(pdfPath).trim().replace(/^\/+/, '')
if (trimmedPdfPath.includes('..')) {
return res.status(400).json({ error: 'pdfPath must not contain ..' })
}
const trimmedBranch = String(branch).trim()
if (!/^[a-zA-Z0-9/_.-]+$/.test(trimmedBranch) || trimmedBranch.includes('..')) {
return res.status(400).json({ error: 'Invalid branch name' })
}
await GitSyncHandler.setConfig(projectId, {
remoteUrl: trimmedUrl,
subPath: trimmedSubPath,
pdfPath: trimmedPdfPath,
pushFiles: Boolean(pushFiles),
pushPdf: Boolean(pushPdf),
branch: trimmedBranch,
})
logger.debug({ projectId }, 'git sync: config saved')
res.sendStatus(204)
}
async function pushToGit(req, res) {
const projectId = req.params.project_id
try {
const userId = SessionManager.getLoggedInUserId(req.session)
const { buildId, clsiServerId } = req.body ?? {}
const { remoteUrl, subPath, pdfPath, pushFiles, pushPdf, branch } =
await GitSyncHandler.getConfig(projectId)
if (!remoteUrl) {
return res.status(400).json({ error: 'No git remote configured for this project' })
}
logger.debug({ projectId }, 'git sync: starting push')
await GitSyncHandler.pushToRemote(projectId, remoteUrl, subPath, {
pdfPath,
pdfBuildId: buildId,
pdfClsiServerId: clsiServerId,
userId,
pushFiles,
pushPdf,
branch,
})
res.sendStatus(204)
} catch (err) {
logger.warn({ err, projectId }, 'git sync: push failed')
res.status(500).json({ error: err.message })
}
}
async function pullFromGit(req, res) {
const projectId = req.params.project_id
try {
const userId = SessionManager.getLoggedInUserId(req.session)
const { remoteUrl, subPath } = await GitSyncHandler.getConfig(projectId)
if (!remoteUrl) {
return res.status(400).json({ error: 'No git remote configured for this project' })
}
logger.debug({ projectId }, 'git sync: starting pull')
await GitSyncHandler.pullFromRemote(projectId, remoteUrl, subPath, userId)
res.sendStatus(204)
} catch (err) {
logger.warn({ err, projectId }, 'git sync: pull failed')
res.status(500).json({ error: err.message })
}
}
async function getGitSyncConfig(req, res) {
const projectId = req.params.project_id
const config = await GitSyncHandler.getConfig(projectId)
res.json({
remoteUrl: config.remoteUrl ?? '',
subPath: config.subPath,
pdfPath: config.pdfPath,
pushFiles: config.pushFiles,
pushPdf: config.pushPdf,
branch: config.branch,
})
}
export default {
configureGitSync: expressify(configureGitSync),
pushToGit: expressify(pushToGit),
pullFromGit: expressify(pullFromGit),
getGitSyncConfig: expressify(getGitSyncConfig),
}