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
This commit is contained in:
yu-i-i
2026-09-28 00:34:31 +02:00
parent 3bfeb32bef
commit 9c041338b3
2 changed files with 104 additions and 94 deletions
@@ -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,
@@ -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