From 9c041338b380239edab7efd1b00466ed34799ef1 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Mon, 3 Aug 2026 00:23:24 +0200 Subject: [PATCH] Changes by @fporrmann Added missing return in clean sync flow Fixed incorrect initialization order Rewrote commit comparison for merge state evaluation Fixed a problem with the getBlobContent function Removed old log lines Fixed merging when there were changes in Gitlab and Overleaf Removed additional log line --- .../gitlab-sync/app/src/GitLabApiClient.mjs | 111 +++++++++++------- .../modules/gitlab-sync/app/src/GitMerge.mjs | 87 ++++++-------- 2 files changed, 104 insertions(+), 94 deletions(-) diff --git a/services/web/modules/gitlab-sync/app/src/GitLabApiClient.mjs b/services/web/modules/gitlab-sync/app/src/GitLabApiClient.mjs index f738723627..fb6bd61035 100644 --- a/services/web/modules/gitlab-sync/app/src/GitLabApiClient.mjs +++ b/services/web/modules/gitlab-sync/app/src/GitLabApiClient.mjs @@ -193,6 +193,19 @@ function getTokenRefreshTimestamp(token, safetyMarginInSec = 300) { return token.created_at + token.expires_in - safetyMarginInSec; } + +async function compareCommitsFull(token, repoFullName, from, to) { + const url = new URL(`${GITLAB_API_BASE}/projects/${projectPath(repoFullName)}/repository/compare`) + url.searchParams.set('from', from) + url.searchParams.set('to', to) + + return await fetchGitLabJson(url.toString(), { + headers: buildHeaders(token), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) + }, 'compareCommitsFull') +} + + // ---------------------- exports ------------------------------- // // OAuth @@ -221,18 +234,18 @@ function exchangeCodeForToken(code) { } function refreshToken(refreshToken) { - return fetchGitLabJson(`${GITLAB_URL}/oauth/token`, { - method: 'POST', - headers: buildHeaders(), - json: { - client_id: Settings.gitlabSync.clientID, - client_secret: Settings.gitlabSync.clientSecret, - refresh_token: refreshToken, - grant_type: 'refresh_token', - redirect_uri: Settings.gitlabSync.callbackURL, - }, - signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) - }, 'refreshToken').then(r => [r.access_token, r.refresh_token, getTokenRefreshTimestamp(r)]) + return fetchGitLabJson(`${GITLAB_URL}/oauth/token`, { + method: 'POST', + headers: buildHeaders(), + json: { + client_id: Settings.gitlabSync.clientID, + client_secret: Settings.gitlabSync.clientSecret, + refresh_token: refreshToken, + grant_type: 'refresh_token', + redirect_uri: Settings.gitlabSync.callbackURL, + }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) + }, 'refreshToken').then(r => [r.access_token, r.refresh_token, getTokenRefreshTimestamp(r)]) } function revokeToken(token) { @@ -405,13 +418,13 @@ function createCommit(token, repoFullName, { tree, message, branch, start_sha, f const actions = Array.isArray(tree?.entries) ? tree.entries : [] let payload = { - branch: branch || GITLAB_DEFAULT_BRANCH, - commit_message: message, - actions, + branch: branch || GITLAB_DEFAULT_BRANCH, + commit_message: message, + actions, } if (branch && branch != GITLAB_DEFAULT_BRANCH) { - payload.start_sha = start_sha + payload.start_sha = start_sha } return fetchGitLabJson(`${GITLAB_API_BASE}/projects/${projectPath(repoFullName)}/repository/commits`, { @@ -448,31 +461,40 @@ async function listBlobsAtCommit(token, repoFullName, commit) { } async function getBlobContent(token, repoFullName, sha) { + if (!sha) return null + const url = `${GITLAB_API_BASE}/projects/${projectPath(repoFullName)}/repository/blobs/${sha}` - try { - const json = await fetchGitLabJson(url, { - headers: buildHeaders(token), - signal: AbortSignal.timeout(REQUEST_LONG_TIMEOUT_MS) - }, 'getBlobContent') - return json.content - } catch (err) { - throw OError.tag(err, 'Failed to fetch blob content from GitLab', { repoFullName, sha }) - } + return await fetchGitLabJson(url, { + headers: buildHeaders(token), + signal: AbortSignal.timeout(REQUEST_LONG_TIMEOUT_MS) + }, 'getBlobContent').then(r => Buffer.from(r.content, 'base64')) } async function listNewCommitsWithStatus(token, fullName, branchName, fromCommit) { - const url = new URL(`${GITLAB_API_BASE}/projects/${projectPath(fullName)}/repository/compare`) - url.searchParams.set('from', fromCommit) - url.searchParams.set('to', branchName) - url.searchParams.set('straight', 'true') + const forward = await compareCommitsFull(token, fullName, fromCommit, branchName); + const reverse = await compareCommitsFull(token, fullName, branchName, fromCommit); + const forwardHasCommits = forward.commits.length > 0; + const reverseHasCommits = reverse.commits.length > 0; - const data = await fetchGitLabJson(url.toString(), { - headers: buildHeaders(token), - signal: AbortSignal.timeout(REQUEST_LONG_TIMEOUT_MS) - }, 'listNewCommitsWithStatus') + let diverged = false - const commits = (data.commits || []).map(c => ({ + if (forward.compare_same_ref) { + // Identical + } + else if (forwardHasCommits && !reverseHasCommits) { + // Ahead + } + else if (!forwardHasCommits && reverseHasCommits) { + // Behind (here also considered a diverged state) + diverged = true + } + else if (forwardHasCommits && reverseHasCommits) { + // Diverged + diverged = true + } + + const commits = (forward.commits || []).map(c => ({ message: c.message || c.title || '', author: { name: c.author_name || '', @@ -482,7 +504,7 @@ async function listNewCommitsWithStatus(token, fullName, branchName, fromCommit) sha: c.id, })) - return { commits, diverged: commits.length > 0 } + return { commits, diverged } } // branches @@ -538,16 +560,16 @@ async function waitForMergeRequestToBeReady(token, repoFullName, mergeRequestIid while (Date.now() - startedAt < MERGE_REQUEST_TIMEOUT_MS) { const mergeRequest = await fetchGitLabJson(`${GITLAB_API_BASE}/projects/${projectPath(repoFullName)}/merge_requests/${mergeRequestIid}`, { - headers: buildHeaders(token), - signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), - }, 'getMergeRequest') + headers: buildHeaders(token), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }, 'getMergeRequest') const status = mergeRequest.detailed_merge_status if (status === 'mergeable') { return { canMerge: true, - reason: status, + reason: status, } } @@ -584,13 +606,13 @@ async function mergeOpenMergeRequest(token, repoFullName, mergeRequestIid) { function mergeBranch(token, repoFullName, base, head) { return createMergeRequest(token, repoFullName, head, base, `Merge ${head} into ${base}`) .then(async mergeRequest => { - const ret = await waitForMergeRequestToBeReady(token, repoFullName, mergeRequest.iid) - if (!ret.canMerge) { + const ret = await waitForMergeRequestToBeReady(token, repoFullName, mergeRequest.iid) + if (!ret.canMerge) { throw new GitConflictError("Merge request cannot be merged due to conflicts, reason: " + ret.reason) - } + } - await mergeOpenMergeRequest(token, repoFullName, mergeRequest.iid) - return getBranchHead(token, repoFullName, base) + await mergeOpenMergeRequest(token, repoFullName, mergeRequest.iid) + return getBranchHead(token, repoFullName, base) }) } @@ -613,6 +635,7 @@ function getRepoZipball(token, repoFullName, sha) { }, 'getRepoZipball') } + export default { maxConcurrency, getOAuth2Url, diff --git a/services/web/modules/gitlab-sync/app/src/GitMerge.mjs b/services/web/modules/gitlab-sync/app/src/GitMerge.mjs index 2b67a324d1..7b9d7c8dfa 100644 --- a/services/web/modules/gitlab-sync/app/src/GitMerge.mjs +++ b/services/web/modules/gitlab-sync/app/src/GitMerge.mjs @@ -123,9 +123,9 @@ async function resolveCleanSyncState( return null } - try - { - // Possible changes in OL + // Possible changes in OL + if (defaultBranchHead === projectSyncState.lastSyncCommit) { + try { const olBranchHead = await exportChangesToGit({ token, @@ -135,55 +135,41 @@ async function resolveCleanSyncState( currentVersion, message, baseCommit: projectSyncState.lastSyncCommit, + defaultBranchName, }) - - if (!olBranchHead) { - if ( defaultBranchHead === projectSyncState.lastSyncCommit) { - await SyncStateManager.updateProjectState(projectId, { lastSyncVersion: currentVersion }) - return null - } - - const lastSyncCommit = defaultBranchHead - const lastSyncVersion = await applyGitSnapshotToProject({ - token, - userId, - projectId, - repoFullName, - lastSyncCommit, - }) - await SyncStateManager.updateProjectState(projectId, { lastSyncVersion, lastSyncCommit }) + if (!olBranchHead) { // no real canges in OL, but currentVersion is ahead + await SyncStateManager.updateProjectState(projectId, { lastSyncVersion: currentVersion }) return null } + const lastSyncCommit = olBranchHead + const lastSyncVersion = await applyGitSnapshotToProject({ + token, + userId, + projectId, + repoFullName, + lastSyncCommit, + }) - if (defaultBranchHead === projectSyncState.lastSyncCommit) { - const lastSyncCommit = olBranchHead - const lastSyncVersion = await applyGitSnapshotToProject({ - token, - userId, - projectId, - repoFullName, - lastSyncCommit, - }) - await SyncStateManager.updateProjectState(projectId, { lastSyncVersion, lastSyncCommit }) - } - } - catch (err) { - logger.error({ err, projectId }, 'Error during clean sync state resolution, fallback to merge') - } + await SyncStateManager.updateProjectState(projectId, { lastSyncVersion, lastSyncCommit }) + return null + } catch (err) { + logger.error({ err, projectId }, 'Error during clean sync state resolution, fallback to merge') + } + } - // Normal commit failed, fallback to merge, first create a temporary branch with OL changes + // Git head is ahead of last known sync commit, create a temporary branch with OL changes and perform a merge with the default branch const tempBranchName = generateBranchName() const olBranchHead = - await exportChangesToGit({ - token, - projectId, - repoFullName, - lastSyncVersion: projectSyncState.lastSyncVersion, - currentVersion, - message, - baseCommit: projectSyncState.lastSyncCommit, - branch: tempBranchName, - }) + await exportChangesToGit({ + token, + projectId, + repoFullName, + lastSyncVersion: projectSyncState.lastSyncVersion, + currentVersion, + message, + baseCommit: projectSyncState.lastSyncCommit, + branch: tempBranchName, + }) // Merge the temporary branch into the default branch const { mergeCommit, conflict } = await mergeWithTempBranch( @@ -293,7 +279,7 @@ async function resolveConflictSyncState( repoFullName, defaultBranchName, newOlBranchHead, - tempBranchName, + tempBranchName, ) if (conflict) { @@ -455,7 +441,7 @@ async function resolveDetachedSyncState( parentCommit: baseCommit, entries: [...cleanLocalEntries, ...conflictLocalEntries], message, - branch: tempBranchName, + branch: tempBranchName, }) // merge: default <- temp (GH <- OL) @@ -621,7 +607,7 @@ async function createCommitFromEntries({ tree: newTree, start_sha: parentCommit, message, - branch, + branch, }) } @@ -632,7 +618,8 @@ async function mergeWithTempBranch( tempBranchName ) { try { - const mergeCommit = await api.mergeBranch(token, repoFullName, branchName, tempBranchName) + const mergeCommit = + await api.mergeBranch(token, repoFullName, branchName, tempBranchName) return { conflict: false, mergeCommit } @@ -713,7 +700,7 @@ async function exportChangesToGit({ parentCommit: baseCommit, entries, message, - branch, + branch, }) return newCommit