From a14548bc3ce60c6e449e4e25ad9ddae53172af11 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 20:18:04 +0000 Subject: [PATCH 1/6] test(server): reproduce clone credential errors from #31664 These tests reproduce pingdotgg/t3code#31664: when a clone fails because Git has no usable credentials or the remote reports the repository as not found, the error detail is only Git's raw stderr. It should say this is a credential problem on the environment running the clone and that Git uses that environment's own credentials, not the token saved in Settings. --- .../SourceControlRepositoryService.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 326b7931da292..279a2a1bfb50f 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -367,6 +367,51 @@ it.effect("reports both failures when the HTTPS retry fails too", () => }), ); +it.effect.each([ + [ + "Git has no credentials for the remote", + "authentication_failed", + ["fatal: could not read Username for 'https://github.com': terminal prompts disabled"], + ], + [ + "the remote hides the repository", + "remote_unreachable", + [ + "remote: Repository not found.", + "fatal: repository 'https://github.com/octocat/t3code/' not found", + ], + ], +] as const)( + "explains that clones use this environment's Git credentials when %s", + ([, reason, stderrLines]) => + Effect.gen(function* () { + const error = yield* Effect.flip( + cloneInTemporaryDirectory( + { + execute: (input) => + Effect.gen(function* () { + for (const line of stderrLines) { + yield* input.progress?.onStderrLine?.(line) ?? Effect.void; + } + return yield* new GitCommandError({ + operation: input.operation, + command: "git", + cwd: input.cwd, + reason, + detail: "Git command exited with a non-zero status.", + exitCode: 128, + }); + }), + }, + { remoteUrl: CLONE_URLS.url }, + ), + ); + + assert.match(error.detail, /credential/i); + assert.match(error.detail, /Settings/); + }), +); + it.effect.each([ "git@ssh.dev.azure.com:v3/Fabrikam/Project1/fab_repo", "ssh://user:pa@rt@ssh.dev.azure.com/v3/Fabrikam/Project1/fab_repo", From a28e794563da78fd2cdc2f5e680568fef23a9834 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 20:23:44 +0000 Subject: [PATCH 2/6] fix(server): explain clone credential failures and name the clone environment When a clone failed because Git had no usable credentials, or the remote reported the repository as not found, the error was Git's raw stderr. Users saved a GitHub token in Settings and retried, but clones never use that token. The clone error now says Git could not authenticate (or that the remote reports the repository as not found, as it does for private repositories the credentials cannot see), and that clones use the Git credentials of the environment running them, not the token saved in Settings. The SSH-to-HTTPS fallback states that note once. The web clone confirm step names the target environment when more than one is saved. Fixes pingdotgg/t3code#31664 --- .../SourceControlRepositoryService.test.ts | 2 +- .../SourceControlRepositoryService.ts | 30 +++++++++++++++---- apps/web/src/components/CommandPalette.tsx | 9 ++++++ .../components/CommandPaletteAddProject.tsx | 6 +++- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 279a2a1bfb50f..0218f77c10992 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -362,7 +362,7 @@ it.effect("reports both failures when the HTTPS retry fails too", () => assert.strictEqual(urls.length, 2); assert.strictEqual( error.detail, - "git@github.com: Permission denied (publickey). Cloning over HTTPS instead also failed: fatal: could not read Username for 'https://github.com': terminal prompts disabled", + "Git could not authenticate with the remote. Cloning over HTTPS instead also failed: Git could not authenticate with the remote. Clones use the Git credentials of the environment running them (a credential helper or SSH keys), not the token saved in Settings.", ); }), ); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 07c7b28d82ff2..58c2c93ea2ee9 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -164,6 +164,25 @@ function publishPushFailureDetail(input: { } } +const CLONE_CREDENTIALS_NOTE = + "Clones use the Git credentials of the environment running them (a credential helper or SSH keys), not the token saved in Settings."; + +// Hosts report private repositories the credentials cannot see as not found. +const REPOSITORY_NOT_FOUND_LINE = /^fatal: repository '.+' not found$/i; + +function cloneFailureDetail( + reason: GitCommandFailureReason | undefined, + stderrTail: ReadonlyArray, +): string { + if (reason === "authentication_failed") { + return `Git could not authenticate with the remote. ${CLONE_CREDENTIALS_NOTE}`; + } + if (stderrTail.some((line) => REPOSITORY_NOT_FOUND_LINE.test(line))) { + return `The remote reported the repository as not found, which also happens when it is private and the credentials Git used cannot see it. ${CLONE_CREDENTIALS_NOTE}`; + } + return stderrTail.length > 0 ? stderrTail.join(" ") : "The repository could not be cloned."; +} + /** * SSH rejection can be retried over HTTPS when the host's credential helper is available. * Explicit SSH requests must not fall back; unknown URL shapes must not be guessed. @@ -368,10 +387,7 @@ export const make = Effect.gen(function* () { new SourceControlRepositoryError({ operation: "cloneRepository", provider: input.provider ?? SourceControlProviderKind.make("unknown"), - detail: - stderrTail.length > 0 - ? stderrTail.join(" ") - : "The repository could not be cloned.", + detail: cloneFailureDetail(cause.reason, stderrTail), cause, }), ), @@ -402,7 +418,11 @@ export const make = Effect.gen(function* () { new SourceControlRepositoryError({ operation: "cloneRepository", provider: sshError.provider, - detail: `${sshError.detail} Cloning over HTTPS instead also failed: ${httpsError.detail}`, + detail: `${ + httpsError.detail.endsWith(CLONE_CREDENTIALS_NOTE) + ? sshError.detail.replace(` ${CLONE_CREDENTIALS_NOTE}`, "") + : sshError.detail + } Cloning over HTTPS instead also failed: ${httpsError.detail}`, cause: httpsError.cause, }), ), diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 196cb0215cc71..5857cbd88bc51 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -2373,6 +2373,14 @@ function OpenCommandPaletteDialog(props: { ); const canCloneProject = canWriteSourceControl && canCreateClonedProject; const isCloneDestinationStep = addProjectCloneFlow?.step === "confirm"; + const cloneEnvironmentLabel = + addProjectCloneFlow !== null && environments.length > 1 + ? (addProjectEnvironmentOptions.find( + (option) => option.environmentId === addProjectCloneFlow.environmentId, + )?.label ?? + environmentLabelById.get(addProjectCloneFlow.environmentId) ?? + null) + : null; const canSubmitBrowsePath = isBrowsing && !relativePathNeedsActiveProject && @@ -2880,6 +2888,7 @@ function OpenCommandPaletteDialog(props: { addProjectCloneFlow.repository?.nameWithOwner ?? addProjectCloneFlow.repositoryInput } description={addProjectCloneFlow.repository?.url ?? addProjectCloneFlow.remoteUrl} + environmentLabel={cloneEnvironmentLabel} /> ) : null} {browseAccessError ? ( diff --git a/apps/web/src/components/CommandPaletteAddProject.tsx b/apps/web/src/components/CommandPaletteAddProject.tsx index 808536403a4bb..6890f622b1129 100644 --- a/apps/web/src/components/CommandPaletteAddProject.tsx +++ b/apps/web/src/components/CommandPaletteAddProject.tsx @@ -153,6 +153,7 @@ export function RemoteRepositorySummary(props: { readonly source: AddProjectRemoteSource; readonly title: string; readonly description: string; + readonly environmentLabel: string | null; }) { return (
@@ -163,7 +164,10 @@ export function RemoteRepositorySummary(props: { {props.title} - {props.description} + + {props.description} + {props.environmentLabel === null ? null : ` on ${props.environmentLabel}`} +
From d0f31db40a5821a72f82da2a75bde6ae5302c765 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 20:32:25 +0000 Subject: [PATCH 3/6] fix: keep Git's clone error text and keep the environment label visible Review follow-up. A rejected credential is explained by the host in Git's own output, so the clone error keeps those lines and appends where clone credentials come from instead of replacing them. The clone confirm step shows the environment on its own line so a long URL cannot truncate it, and the GitHub section of the source control guide says clones use the server's Git credentials rather than the saved token. --- .../SourceControlRepositoryService.test.ts | 11 ++++++++++- .../sourceControl/SourceControlRepositoryService.ts | 8 +++++--- apps/web/src/components/CommandPaletteAddProject.tsx | 10 ++++++---- docs/user/source-control.md | 3 +++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 0218f77c10992..f46452e1d3178 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -362,7 +362,7 @@ it.effect("reports both failures when the HTTPS retry fails too", () => assert.strictEqual(urls.length, 2); assert.strictEqual( error.detail, - "Git could not authenticate with the remote. Cloning over HTTPS instead also failed: Git could not authenticate with the remote. Clones use the Git credentials of the environment running them (a credential helper or SSH keys), not the token saved in Settings.", + "git@github.com: Permission denied (publickey). Cloning over HTTPS instead also failed: fatal: could not read Username for 'https://github.com': terminal prompts disabled Clones use the Git credentials of the environment running them (a credential helper or SSH keys), not the token saved in Settings.", ); }), ); @@ -373,6 +373,14 @@ it.effect.each([ "authentication_failed", ["fatal: could not read Username for 'https://github.com': terminal prompts disabled"], ], + [ + "the host rejects the stored credentials", + "authentication_failed", + [ + "remote: HTTP Basic: Access denied. The provided password or token is incorrect.", + "fatal: Authentication failed for 'https://gitlab.com/group/repo.git/'", + ], + ], [ "the remote hides the repository", "remote_unreachable", @@ -407,6 +415,7 @@ it.effect.each([ ), ); + for (const line of stderrLines) assert.include(error.detail, line); assert.match(error.detail, /credential/i); assert.match(error.detail, /Settings/); }), diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 58c2c93ea2ee9..6105ee11732a6 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -170,17 +170,19 @@ const CLONE_CREDENTIALS_NOTE = // Hosts report private repositories the credentials cannot see as not found. const REPOSITORY_NOT_FOUND_LINE = /^fatal: repository '.+' not found$/i; +// Keep Git's lines, since hosts explain rejected credentials there, and add where credentials come from. function cloneFailureDetail( reason: GitCommandFailureReason | undefined, stderrTail: ReadonlyArray, ): string { + const output = stderrTail.join(" "); if (reason === "authentication_failed") { - return `Git could not authenticate with the remote. ${CLONE_CREDENTIALS_NOTE}`; + return `${output.length > 0 ? output : "Git could not authenticate with the remote."} ${CLONE_CREDENTIALS_NOTE}`; } if (stderrTail.some((line) => REPOSITORY_NOT_FOUND_LINE.test(line))) { - return `The remote reported the repository as not found, which also happens when it is private and the credentials Git used cannot see it. ${CLONE_CREDENTIALS_NOTE}`; + return `${output} Hosts also report a private repository as not found when the credentials Git used cannot see it. ${CLONE_CREDENTIALS_NOTE}`; } - return stderrTail.length > 0 ? stderrTail.join(" ") : "The repository could not be cloned."; + return output.length > 0 ? output : "The repository could not be cloned."; } /** diff --git a/apps/web/src/components/CommandPaletteAddProject.tsx b/apps/web/src/components/CommandPaletteAddProject.tsx index 6890f622b1129..3b045a6f6ba01 100644 --- a/apps/web/src/components/CommandPaletteAddProject.tsx +++ b/apps/web/src/components/CommandPaletteAddProject.tsx @@ -164,10 +164,12 @@ export function RemoteRepositorySummary(props: { {props.title} - - {props.description} - {props.environmentLabel === null ? null : ` on ${props.environmentLabel}`} - + {props.description} + {props.environmentLabel === null ? null : ( + + Clones on {props.environmentLabel} + + )} diff --git a/docs/user/source-control.md b/docs/user/source-control.md index 949bddd8afbf0..980c0025c71a3 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -41,6 +41,9 @@ If `gh` is signed in to several accounts or hosts, expand **GitHub** in the same the account each host uses or turn a host off. A saved token or `GH_TOKEN` takes precedence over that choice; a host turned off stays off either way. +Cloning runs Git on the server, so it uses the server's Git credentials, not the token +above. Run `gh auth setup-git` there, or configure another credential helper or an SSH key. + For GitHub Enterprise, sign in with `gh auth login --hostname YOUR_HOST`. T3 Code treats a custom server name as GitHub once it has a credential for that host. From 1d7894dbd04008de157ac68d47dc2797b8e9fe3f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 20:40:37 +0000 Subject: [PATCH 4/6] fix: lead clone credential errors with the guidance Review follow-up. Mobile shows three lines of a clone error and web toasts four, so the explanation and the note that clones use the environment's Git credentials now come before Git's own lines instead of after them. The source control guide says clones do not use a token saved in Settings, which stays true when `gh auth setup-git` makes Git use the GitHub CLI's login. --- .../SourceControlRepositoryService.test.ts | 9 ++++-- .../SourceControlRepositoryService.ts | 30 +++++++++++-------- docs/user/source-control.md | 4 +-- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index f46452e1d3178..9b02a7346b3f2 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -362,7 +362,7 @@ it.effect("reports both failures when the HTTPS retry fails too", () => assert.strictEqual(urls.length, 2); assert.strictEqual( error.detail, - "git@github.com: Permission denied (publickey). Cloning over HTTPS instead also failed: fatal: could not read Username for 'https://github.com': terminal prompts disabled Clones use the Git credentials of the environment running them (a credential helper or SSH keys), not the token saved in Settings.", + "Git could not authenticate. Clones use this environment's Git credentials, not a token saved in Settings. git@github.com: Permission denied (publickey). Cloning over HTTPS instead also failed: Git could not authenticate. fatal: could not read Username for 'https://github.com': terminal prompts disabled", ); }), ); @@ -416,8 +416,11 @@ it.effect.each([ ); for (const line of stderrLines) assert.include(error.detail, line); - assert.match(error.detail, /credential/i); - assert.match(error.detail, /Settings/); + // Clients clamp the error, so the guidance must come before Git's lines. + const guidanceEnd = error.detail.indexOf("Settings"); + assert.isAbove(guidanceEnd, -1); + assert.isBelow(guidanceEnd, error.detail.indexOf(stderrLines[0])); + assert.match(error.detail.slice(0, guidanceEnd), /credential/i); }), ); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 6105ee11732a6..a670626d4cf40 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -165,24 +165,28 @@ function publishPushFailureDetail(input: { } const CLONE_CREDENTIALS_NOTE = - "Clones use the Git credentials of the environment running them (a credential helper or SSH keys), not the token saved in Settings."; + "Clones use this environment's Git credentials, not a token saved in Settings."; // Hosts report private repositories the credentials cannot see as not found. const REPOSITORY_NOT_FOUND_LINE = /^fatal: repository '.+' not found$/i; -// Keep Git's lines, since hosts explain rejected credentials there, and add where credentials come from. +// Clients clamp clone errors to a few lines, so the explanation leads; Git's lines follow +// because hosts explain rejected credentials there. function cloneFailureDetail( reason: GitCommandFailureReason | undefined, stderrTail: ReadonlyArray, ): string { const output = stderrTail.join(" "); - if (reason === "authentication_failed") { - return `${output.length > 0 ? output : "Git could not authenticate with the remote."} ${CLONE_CREDENTIALS_NOTE}`; + const explanation = + reason === "authentication_failed" + ? "Git could not authenticate." + : stderrTail.some((line) => REPOSITORY_NOT_FOUND_LINE.test(line)) + ? "The repository was not found, or Git's credentials cannot access it." + : null; + if (explanation === null) { + return output.length > 0 ? output : "The repository could not be cloned."; } - if (stderrTail.some((line) => REPOSITORY_NOT_FOUND_LINE.test(line))) { - return `${output} Hosts also report a private repository as not found when the credentials Git used cannot see it. ${CLONE_CREDENTIALS_NOTE}`; - } - return output.length > 0 ? output : "The repository could not be cloned."; + return `${explanation} ${CLONE_CREDENTIALS_NOTE}${output.length > 0 ? ` ${output}` : ""}`; } /** @@ -420,11 +424,11 @@ export const make = Effect.gen(function* () { new SourceControlRepositoryError({ operation: "cloneRepository", provider: sshError.provider, - detail: `${ - httpsError.detail.endsWith(CLONE_CREDENTIALS_NOTE) - ? sshError.detail.replace(` ${CLONE_CREDENTIALS_NOTE}`, "") - : sshError.detail - } Cloning over HTTPS instead also failed: ${httpsError.detail}`, + detail: `${sshError.detail} Cloning over HTTPS instead also failed: ${ + sshError.detail.includes(CLONE_CREDENTIALS_NOTE) + ? httpsError.detail.replace(` ${CLONE_CREDENTIALS_NOTE}`, "") + : httpsError.detail + }`, cause: httpsError.cause, }), ), diff --git a/docs/user/source-control.md b/docs/user/source-control.md index 980c0025c71a3..4b28d0154c202 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -41,8 +41,8 @@ If `gh` is signed in to several accounts or hosts, expand **GitHub** in the same the account each host uses or turn a host off. A saved token or `GH_TOKEN` takes precedence over that choice; a host turned off stays off either way. -Cloning runs Git on the server, so it uses the server's Git credentials, not the token -above. Run `gh auth setup-git` there, or configure another credential helper or an SSH key. +Cloning runs Git on the server, so it uses the server's Git credentials, not a token saved +in Settings. Run `gh auth setup-git` there, or configure another credential helper or an SSH key. For GitHub Enterprise, sign in with `gh auth login --hostname YOUR_HOST`. T3 Code treats a custom server name as GitHub once it has a credential for that host. From a088d1fdd165411e365beb82c712b5f9e8e3931f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 20:49:06 +0000 Subject: [PATCH 5/6] fix: lead the combined SSH and HTTPS clone error with the credential note Review follow-up. When SSH fails on an untrusted host key and the HTTPS retry has no credentials, only the HTTPS half carried the note, so it sat after the SSH text where clients clamp it away. The combined message now states the note once, first. --- .../SourceControlRepositoryService.test.ts | 27 ++++++++++++++++--- .../SourceControlRepositoryService.ts | 15 +++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 9b02a7346b3f2..29da8ba083ffb 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -303,18 +303,21 @@ function rejectingSsh( const url = input.args.at(-2)!; urls.push(url); if (url.startsWith("https://") && !httpsFails) return processOutput(); + const https = url.startsWith("https://"); yield* ( input.progress?.onStderrLine?.( - url.startsWith("https://") + https ? "fatal: could not read Username for 'https://github.com': terminal prompts disabled" - : "git@github.com: Permission denied (publickey).", + : reason === "host_key_unverified" + ? "Host key verification failed." + : "git@github.com: Permission denied (publickey).", ) ?? Effect.void ); return yield* new GitCommandError({ operation: input.operation, command: "git", cwd: input.cwd, - reason, + reason: https ? "authentication_failed" : reason, detail: "Git command exited with a non-zero status.", }); }), @@ -362,7 +365,23 @@ it.effect("reports both failures when the HTTPS retry fails too", () => assert.strictEqual(urls.length, 2); assert.strictEqual( error.detail, - "Git could not authenticate. Clones use this environment's Git credentials, not a token saved in Settings. git@github.com: Permission denied (publickey). Cloning over HTTPS instead also failed: Git could not authenticate. fatal: could not read Username for 'https://github.com': terminal prompts disabled", + "Clones use this environment's Git credentials, not a token saved in Settings. Git could not authenticate. git@github.com: Permission denied (publickey). Cloning over HTTPS instead also failed: Git could not authenticate. fatal: could not read Username for 'https://github.com': terminal prompts disabled", + ); + }), +); + +it.effect("leads with the credential note when only the HTTPS retry lacks credentials", () => + Effect.gen(function* () { + const urls: Array = []; + const error = yield* Effect.flip( + cloneInTemporaryDirectory(rejectingSsh(urls, true, "host_key_unverified"), { + remoteUrl: CLONE_URLS.sshUrl, + }), + ); + + assert.strictEqual( + error.detail, + "Clones use this environment's Git credentials, not a token saved in Settings. Host key verification failed. Cloning over HTTPS instead also failed: Git could not authenticate. fatal: could not read Username for 'https://github.com': terminal prompts disabled", ); }), ); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index a670626d4cf40..ba27650908548 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -189,6 +189,15 @@ function cloneFailureDetail( return `${explanation} ${CLONE_CREDENTIALS_NOTE}${output.length > 0 ? ` ${output}` : ""}`; } +// The note leads once even when only the HTTPS attempt carries it. +function combinedCloneFailureDetail(sshDetail: string, httpsDetail: string): string { + const withoutNote = (detail: string) => detail.replace(` ${CLONE_CREDENTIALS_NOTE}`, ""); + const combined = `${withoutNote(sshDetail)} Cloning over HTTPS instead also failed: ${withoutNote(httpsDetail)}`; + return sshDetail.includes(CLONE_CREDENTIALS_NOTE) || httpsDetail.includes(CLONE_CREDENTIALS_NOTE) + ? `${CLONE_CREDENTIALS_NOTE} ${combined}` + : combined; +} + /** * SSH rejection can be retried over HTTPS when the host's credential helper is available. * Explicit SSH requests must not fall back; unknown URL shapes must not be guessed. @@ -424,11 +433,7 @@ export const make = Effect.gen(function* () { new SourceControlRepositoryError({ operation: "cloneRepository", provider: sshError.provider, - detail: `${sshError.detail} Cloning over HTTPS instead also failed: ${ - sshError.detail.includes(CLONE_CREDENTIALS_NOTE) - ? httpsError.detail.replace(` ${CLONE_CREDENTIALS_NOTE}`, "") - : httpsError.detail - }`, + detail: combinedCloneFailureDetail(sshError.detail, httpsError.detail), cause: httpsError.cause, }), ), From b3a835714c07ff095091422a305bea6fcd236c0b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 21:34:37 +0000 Subject: [PATCH 6/6] test(server): drop the duplicate authentication clone case Co-Authored-By: Claude Opus 5.5 --- .../sourceControl/SourceControlRepositoryService.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 29da8ba083ffb..e0b3df7d85ad9 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -392,14 +392,6 @@ it.effect.each([ "authentication_failed", ["fatal: could not read Username for 'https://github.com': terminal prompts disabled"], ], - [ - "the host rejects the stored credentials", - "authentication_failed", - [ - "remote: HTTP Basic: Access denied. The provided password or token is incorrect.", - "fatal: Authentication failed for 'https://gitlab.com/group/repo.git/'", - ], - ], [ "the remote hides the repository", "remote_unreachable",