From 57a1ce4f14657ef68cfda1e8a1249b809de48739 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 24 Jun 2026 11:07:18 +0000 Subject: [PATCH] perf(git-sync): depth=1 fetch and skip fetch when remote is unchanged --depth=1 limits fetch to only the latest commit's tree objects instead of the full history, dramatically reducing fetch time on repos with many commits. A git ls-remote check before fetching skips the fetch entirely when local HEAD already matches remote HEAD, which is the common case after a push with no external changes. Co-Authored-By: Claude Sonnet 4.6 --- .../src/Features/GitSync/GitSyncHandler.mjs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/services/web/app/src/Features/GitSync/GitSyncHandler.mjs b/services/web/app/src/Features/GitSync/GitSyncHandler.mjs index bc20a55edd..bc7e70beb5 100644 --- a/services/web/app/src/Features/GitSync/GitSyncHandler.mjs +++ b/services/web/app/src/Features/GitSync/GitSyncHandler.mjs @@ -169,13 +169,25 @@ async function pushToRemote( // objects are sent — git handles the diff automatically. const repoDir = await ensureRepo(projectId, remoteUrl, branch) - // Fetch remote state (trees only, no blobs) to preserve files outside the managed area. - // read-tree updates the index without checking out any files, so no blobs are downloaded. + // Fetch remote state (trees only, latest commit only) to preserve files outside the managed area. + // Skipped when remote HEAD already matches local HEAD (common case: re-push without external changes). try { - await spawnGit(['fetch', '--filter=blob:none', 'origin', branch], repoDir) - // --mixed: moves HEAD to FETCH_HEAD + updates index, no working tree change. - // This makes FETCH_HEAD the parent of our next commit so the diff only shows subPath changes. - await spawnGit(['reset', '--mixed', 'FETCH_HEAD'], repoDir) + let needsFetch = true + try { + const remoteRef = await spawnGitOutput(['ls-remote', 'origin', branch], repoDir) + const remoteHead = remoteRef.split(/\s/)[0].trim() + const localHead = await spawnGitOutput(['rev-parse', 'HEAD'], repoDir) + needsFetch = remoteHead !== localHead + } catch { + // Can't compare — proceed with fetch + } + if (needsFetch) { + // --depth=1 --filter=blob:none: only the latest commit's tree objects, no blobs + await spawnGit(['fetch', '--filter=blob:none', '--depth=1', 'origin', branch], repoDir) + // --mixed: moves HEAD to FETCH_HEAD + updates index, no working tree change. + // This makes FETCH_HEAD the parent of our next commit so the diff only shows subPath changes. + await spawnGit(['reset', '--mixed', 'FETCH_HEAD'], repoDir) + } } catch { // Empty remote or first push — proceed with empty index } @@ -256,8 +268,8 @@ async function pullFromRemote(projectId, remoteUrl, subPath, branch, userId) { logger.debug({ projectId, subPath }, 'git sync: fetching remote for pull') const repoDir = await ensureRepo(projectId, remoteUrl, branch) - // Fetch with blob filter — only trees/commits downloaded upfront - await spawnGit(['fetch', '--filter=blob:none', 'origin', branch], repoDir) + // depth=1 + blob filter: only the latest commit's tree objects, no blobs upfront + await spawnGit(['fetch', '--filter=blob:none', '--depth=1', 'origin', branch], repoDir) if (subPath) { // Checkout only the subPath directory — triggers lazy blob fetch for those files only