* Sync local project to the shared bare base repo on the remote. * * OpenSSH runtimes use native `git push` so Git negotiates incremental object * transfer automatically. SSH2 runtimes keep the bundle path so sync does not * depend on a local OpenSSH CLI or local known_hosts state. *
(
projectPath: string,
initLogger: InitLogger,
abortSignal?: AbortSignal
)
| 2250 | * shared metadata instead of authoritative snapshot state. |
| 2251 | */ |
| 2252 | protected async syncProjectToRemote( |
| 2253 | projectPath: string, |
| 2254 | initLogger: InitLogger, |
| 2255 | abortSignal?: AbortSignal |
| 2256 | ): Promise<void> { |
| 2257 | if (abortSignal?.aborted) { |
| 2258 | throw new Error(OPERATION_ABORTED_ERROR); |
| 2259 | } |
| 2260 | |
| 2261 | const layout = this.getProjectLayout(projectPath); |
| 2262 | const projectKey = this.getProjectSyncKey(layout.projectId); |
| 2263 | const retryCleanupBaseRepoPathArg = expandTildeForSSH(layout.baseRepoPath); |
| 2264 | |
| 2265 | // Keep retries, cancellation handling, and retry cleanup inside the project-scoped |
| 2266 | // sync lock so a follow-up init cannot race the shared base repo while we are healing it. |
| 2267 | await enqueueProjectSync(projectKey, abortSignal, async () => { |
| 2268 | // Latches once a thin-pack delta-base failure is observed so every |
| 2269 | // subsequent attempt in this sync push a self-contained pack (`--no-thin`). |
| 2270 | // Stays `false` for connection-reset / killed-by-signal retries since |
| 2271 | // those don't benefit from forcing a larger pack. |
| 2272 | let forceNoThinNextAttempt = false; |
| 2273 | for (let attempt = 1; attempt <= PROJECT_SYNC_MAX_ATTEMPTS; attempt++) { |
| 2274 | if (abortSignal?.aborted) { |
| 2275 | throw new Error(OPERATION_ABORTED_ERROR); |
| 2276 | } |
| 2277 | |
| 2278 | try { |
| 2279 | await this.syncProjectToRemoteOnce(projectPath, layout, initLogger, abortSignal, { |
| 2280 | forceNoThin: forceNoThinNextAttempt, |
| 2281 | }); |
| 2282 | return; |
| 2283 | } catch (error) { |
| 2284 | const errorMsg = getErrorMessage(error); |
| 2285 | if (abortSignal?.aborted || errorMsg === OPERATION_ABORTED_ERROR) { |
| 2286 | throw error instanceof Error ? error : new Error(errorMsg); |
| 2287 | } |
| 2288 | if ( |
| 2289 | !this.isRetryableProjectSyncError(errorMsg) || |
| 2290 | attempt === PROJECT_SYNC_MAX_ATTEMPTS |
| 2291 | ) { |
| 2292 | throw new Error(`Failed to sync project: ${errorMsg}`); |
| 2293 | } |
| 2294 | |
| 2295 | if (isUnresolvedDeltaPushFailure(errorMsg)) { |
| 2296 | forceNoThinNextAttempt = true; |
| 2297 | } |
| 2298 | |
| 2299 | log.info( |
| 2300 | `Sync failed (attempt ${attempt}/${PROJECT_SYNC_MAX_ATTEMPTS}), will retry: ${errorMsg}` |
| 2301 | ); |
| 2302 | await this.cleanupRetryableProjectSyncFailure( |
| 2303 | retryCleanupBaseRepoPathArg, |
| 2304 | attempt, |
| 2305 | PROJECT_SYNC_MAX_ATTEMPTS, |
| 2306 | abortSignal |
| 2307 | ); |
| 2308 | if (abortSignal?.aborted) { |
| 2309 | throw new Error(OPERATION_ABORTED_ERROR); |