Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/common/utils/git/gitStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,24 @@ if [ "$LOCAL_SHA" = "$REMOTE_SHA" ]; then
fi

# Remote has new commits or ref moved - fetch updates
#
# --no-filter (NOT --filter=blob:none): a filtered fetch permanently converts
# the repo into a promisor/partial clone (git writes remote.origin.promisor +
# remote.origin.partialclonefilter on the first filtered fetch, and the
# configured filter then applies to every subsequent plain fetch). That leaves
# every commit fetched by this background loop without its blobs, so a later
# "git worktree add" (workspace creation) must lazy-fetch blobs from the
# remote mid-checkout and any transient network failure aborts it with
# "fatal: could not fetch <oid> from promisor remote". --no-filter both avoids
# poisoning healthy repos and overrides the persisted filter config in repos
# that were already converted, so they heal going forward.
git -c protocol.version=2 \\
-c fetch.negotiationAlgorithm=skipping \\
fetch origin \\
--prune \\
--no-tags \\
--no-recurse-submodules \\
--no-write-fetch-head \\
--filter=blob:none \\
--no-filter \\
Comment thread
ibetitsmike marked this conversation as resolved.
Comment thread
ibetitsmike marked this conversation as resolved.
2>&1
Comment thread
ibetitsmike marked this conversation as resolved.
`;
20 changes: 18 additions & 2 deletions src/node/runtime/SSHRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ function isUnresolvedDeltaPushFailure(errorMsg: string): boolean {
}

function isMissingObjectCheckoutFailure(message: string): boolean {
return /unable to read sha1 file|Could not reset index file|missing (blob|tree|commit)|bad object|unable to read tree|object file .* is empty|loose object .* is corrupt/i.test(
// "could not fetch ... from promisor remote": the checkout needed objects the
// repo does not have and a lazy fetch from upstream failed (e.g. transient
// network drop). The objects are still missing locally, so the same
// repair-from-local path applies.
return /unable to read sha1 file|Could not reset index file|missing (blob|tree|commit)|bad object|unable to read tree|object file .* is empty|loose object .* is corrupt|could not fetch .* from promisor remote/i.test(
message
);
}
Expand Down Expand Up @@ -2657,6 +2661,18 @@ export class SSHRuntime extends RemoteRuntime {
// path where ensureBaseRepo() has retry/error handling instead of risking
// materializing a worktree from still-poisoned shared config.
`git --git-dir=${baseRepoPathArg} symbolic-ref HEAD ${baseRepoUnbornHeadArg} 2>/dev/null || { echo WARM_MISS:base-head-normalization-failed; exit 0; }`,
// Best-effort promisor strip, mirroring ensureBaseRepo()'s epilogue. The
// warm path skips ensureBaseRepo(), and background status fetches that
// ran `git fetch --filter=blob:none` inside sibling worktrees register
// the shared base repo as a promisor remote (remote.origin.promisor +
// partialclonefilter). Left in place, `git worktree add` below would
// lazy-fetch missing blobs from upstream mid-checkout, so a transient
// network drop aborts workspace creation with "could not fetch <oid>
// from promisor remote" instead of the repairable missing-objects path.
...BASE_REPO_PROMISOR_CONFIG_KEYS.map(
(key) =>
`git --git-dir=${baseRepoPathArg} config --local --unset-all ${shescape.quote(key)} 2>/dev/null || true`
),
Comment thread
ibetitsmike marked this conversation as resolved.
];

const originPreamble = originUrlArg
Expand Down Expand Up @@ -2706,7 +2722,7 @@ export class SSHRuntime extends RemoteRuntime {
"wt_status=$?",
'if [ "$wt_status" -ne 0 ]; then',
' case "$wt_output" in',
' *"unable to read sha1 file"*|*"Could not reset index file"*|*"missing blob"*|*"missing tree"*|*"missing commit"*|*"bad object"*|*"unable to read tree"*) wt_reason=missing-objects ;;',
' *"unable to read sha1 file"*|*"Could not reset index file"*|*"missing blob"*|*"missing tree"*|*"missing commit"*|*"bad object"*|*"unable to read tree"*|*"from promisor remote"*) wt_reason=missing-objects ;;',
" *) wt_reason=worktree-add-failed ;;",
" esac",
` git -C ${baseRepoPathArg} worktree remove --force ${workspacePathArg} >/dev/null 2>&1 || rm -rf ${workspacePathArg}`,
Expand Down
20 changes: 20 additions & 0 deletions tests/runtime/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2206,6 +2206,12 @@ describeIntegration("Runtime integration tests", () => {
`git --git-dir="${baseRepoPath}" config --local core.bare true`,
`git --git-dir="${baseRepoPath}" config --local core.worktree "${bogusWorktreePath}"`,
`git --git-dir="${baseRepoPath}" symbolic-ref HEAD refs/heads/main`,
// Simulate what a background `git fetch --filter=blob:none` from a
// sibling worktree registers in the shared gitdir. A promisor base
// repo lazy-fetches missing blobs over the network mid-checkout, so
// the warm path must strip these before `git worktree add`.
`git --git-dir="${baseRepoPath}" config --local remote.origin.promisor true`,
`git --git-dir="${baseRepoPath}" config --local remote.origin.partialclonefilter blob:none`,
].join(" && ")
);
expect(poisonResult.exitCode).toBe(0);
Expand Down Expand Up @@ -2243,6 +2249,20 @@ describeIntegration("Runtime integration tests", () => {
);
expect(baseRepoCoreWorktreeCheck.exitCode).toBe(1);

// Promisor/partial-clone registration must be stripped so worktree
// materialization never lazy-fetches blobs over the network.
const baseRepoPromisorCheck = await execSSH(
runtime,
`git --git-dir="${baseRepoPath}" config --get remote.origin.promisor`
);
expect(baseRepoPromisorCheck.exitCode).toBe(1);

const baseRepoFilterCheck = await execSSH(
runtime,
`git --git-dir="${baseRepoPath}" config --get remote.origin.partialclonefilter`
);
expect(baseRepoFilterCheck.exitCode).toBe(1);

const baseHeadSymbolicCheck = await execSSH(
runtime,
`git --git-dir="${baseRepoPath}" symbolic-ref -q HEAD`
Expand Down
Loading