fix: Use HTTP header auth for clone, bypassing credential helper
Some checks failed
Build and Release / Create Release (push) Successful in 2s
CI / Lint (push) Successful in 40s
CI / Build (${{ matrix.os }}) (macos-latest) (push) Has been cancelled
CI / Build (${{ matrix.os }}) (ubuntu-latest) (push) Has been cancelled
CI / Build (${{ matrix.os }}) (windows-latest) (push) Has been cancelled
CI / Test (${{ matrix.os }}) (ubuntu-latest) (push) Has been cancelled
CI / Test (${{ matrix.os }}) (windows-latest) (push) Has been cancelled
CI / Test (${{ matrix.os }}) (macos-latest) (push) Has been cancelled
Build and Release / Build Windows (${{ matrix.arch }}) (arm64) (push) Successful in 8h3m34s
Build and Release / Build Linux (${{ matrix.arch }}) (x64) (push) Successful in 5m36s
Build and Release / Build Windows (${{ matrix.arch }}) (x64) (push) Successful in 9h4m46s
Build and Release / Build macOS (${{ matrix.arch }}) (arm64) (push) Successful in 7m49s
Build and Release / Build macOS (${{ matrix.arch }}) (x64) (push) Successful in 8m8s
Some checks failed
Build and Release / Create Release (push) Successful in 2s
CI / Lint (push) Successful in 40s
CI / Build (${{ matrix.os }}) (macos-latest) (push) Has been cancelled
CI / Build (${{ matrix.os }}) (ubuntu-latest) (push) Has been cancelled
CI / Build (${{ matrix.os }}) (windows-latest) (push) Has been cancelled
CI / Test (${{ matrix.os }}) (ubuntu-latest) (push) Has been cancelled
CI / Test (${{ matrix.os }}) (windows-latest) (push) Has been cancelled
CI / Test (${{ matrix.os }}) (macos-latest) (push) Has been cancelled
Build and Release / Build Windows (${{ matrix.arch }}) (arm64) (push) Successful in 8h3m34s
Build and Release / Build Linux (${{ matrix.arch }}) (x64) (push) Successful in 5m36s
Build and Release / Build Windows (${{ matrix.arch }}) (x64) (push) Successful in 9h4m46s
Build and Release / Build macOS (${{ matrix.arch }}) (arm64) (push) Successful in 7m49s
Build and Release / Build macOS (${{ matrix.arch }}) (x64) (push) Successful in 8m8s
Instead of relying on the credential helper system which is unreliable on Windows (cached credentials in Windows Credential Manager take precedence), pass the auth token directly as an HTTP header using git's http.extraHeader config option. This approach: - Bypasses the credential helper system entirely for HTTPS clone - Works on all platforms (Windows, macOS, Linux) - Supports both OAuth tokens and Personal Access Tokens - Does not affect SSH authentication (different protocol) Changes: - clone.ts: Add getAuthConfigArgs() to build -c http.extraHeader args - clone-options.ts: Add account field to CloneOptions - clone-repository.tsx: Look up account and pass through clone chain - dispatcher.ts: Accept account in clone options - app-store.ts: Pass account through to cloningRepositoriesStore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"productName": "GitCaddy",
|
||||
"bundleID": "com.gitcaddy.app",
|
||||
"companyName": "MarketAlly",
|
||||
"version": "4.0.20",
|
||||
"version": "4.0.21",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -4,38 +4,38 @@ import { CloneOptions } from '../../models/clone-options'
|
||||
import { CloneProgressParser, executionOptionsWithProgress } from '../progress'
|
||||
import { getDefaultBranch } from '../helpers/default-branch'
|
||||
import { envForRemoteOperation } from './environment'
|
||||
import { rejectCredential } from './credential'
|
||||
|
||||
/**
|
||||
* On Windows, proactively clear any cached credentials for the target host
|
||||
* from Windows Credential Manager before cloning. This prevents git from
|
||||
* using stale/incorrect cached credentials instead of our credential helper.
|
||||
* Build git config args for HTTP header authentication.
|
||||
* This bypasses the credential helper system entirely by passing
|
||||
* the auth token directly as an HTTP header.
|
||||
*
|
||||
* Works for both OAuth tokens and Personal Access Tokens.
|
||||
* Only applies to HTTPS URLs - SSH uses a different auth mechanism.
|
||||
*/
|
||||
async function clearCachedCredentialsForHost(url: string): Promise<void> {
|
||||
if (process.platform !== 'win32') {
|
||||
return
|
||||
function getAuthConfigArgs(url: string, options: CloneOptions): string[] {
|
||||
const account = options.account
|
||||
if (!account || !account.token) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url)
|
||||
const cred = new Map<string, string>()
|
||||
cred.set('protocol', parsedUrl.protocol.replace(':', ''))
|
||||
cred.set('host', parsedUrl.host)
|
||||
|
||||
log.info(
|
||||
`[clone] Pre-emptively clearing cached credentials for ${parsedUrl.host}`
|
||||
)
|
||||
// Only add auth header for HTTP/HTTPS URLs
|
||||
if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
|
||||
log.debug('[clone] SSH URL detected, skipping HTTP auth header')
|
||||
return []
|
||||
}
|
||||
|
||||
// Try to reject/clear any cached credential for this host
|
||||
// This uses git credential reject which tells Windows Credential Manager
|
||||
// to remove any stored credential for this host
|
||||
await rejectCredential(cred, process.cwd()).catch(e => {
|
||||
// Ignore errors - there may not be a cached credential to clear
|
||||
log.debug(`[clone] No cached credential to clear for ${parsedUrl.host}`)
|
||||
})
|
||||
log.info(`[clone] Using HTTP header auth for ${parsedUrl.host}`)
|
||||
|
||||
// Use http.extraHeader to pass auth directly
|
||||
// This works for Gitea, GitHub, GitLab, etc.
|
||||
return ['-c', `http.extraHeader=Authorization: token ${account.token}`]
|
||||
} catch (e) {
|
||||
// Ignore URL parsing errors
|
||||
log.debug(`[clone] Could not parse URL for credential clearing: ${e}`)
|
||||
log.debug(`[clone] Could not parse URL for auth: ${e}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,6 @@ export async function clone(
|
||||
options: CloneOptions,
|
||||
progressCallback?: (progress: ICloneProgress) => void
|
||||
): Promise<void> {
|
||||
// On Windows, clear any cached credentials for this host BEFORE cloning
|
||||
// This prevents git from using stale credentials from Windows Credential Manager
|
||||
await clearCachedCredentialsForHost(url)
|
||||
|
||||
const env = {
|
||||
...(await envForRemoteOperation(url)),
|
||||
GIT_CLONE_PROTECTION_ACTIVE: 'false',
|
||||
@@ -75,7 +71,11 @@ export async function clone(
|
||||
|
||||
const defaultBranch = options.defaultBranch ?? (await getDefaultBranch())
|
||||
|
||||
// Build args with auth header if account provided
|
||||
const authArgs = getAuthConfigArgs(url, options)
|
||||
|
||||
const args = [
|
||||
...authArgs,
|
||||
'-c',
|
||||
`init.defaultBranch=${defaultBranch}`,
|
||||
'clone',
|
||||
|
||||
@@ -5346,12 +5346,21 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||
public _clone(
|
||||
url: string,
|
||||
path: string,
|
||||
options: { branch?: string; defaultBranch?: string } = {}
|
||||
options: {
|
||||
branch?: string
|
||||
defaultBranch?: string
|
||||
account?: Account | null
|
||||
} = {}
|
||||
): {
|
||||
promise: Promise<boolean>
|
||||
repository: CloningRepository
|
||||
} {
|
||||
const promise = this.cloningRepositoriesStore.clone(url, path, options)
|
||||
const cloneOptions = {
|
||||
branch: options.branch,
|
||||
defaultBranch: options.defaultBranch,
|
||||
account: options.account ?? undefined,
|
||||
}
|
||||
const promise = this.cloningRepositoriesStore.clone(url, path, cloneOptions)
|
||||
const repository = this.cloningRepositoriesStore.repositories.find(
|
||||
r => r.url === url && r.path === path
|
||||
)!
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Account } from './account'
|
||||
|
||||
/** Additional arguments to provide when cloning a repository */
|
||||
export type CloneOptions = {
|
||||
/** The branch to checkout after the clone has completed. */
|
||||
readonly branch?: string
|
||||
/** The default branch name in case we're cloning an empty repository. */
|
||||
readonly defaultBranch?: string
|
||||
/** The account to use for authentication (for HTTPS URLs). */
|
||||
readonly account?: Account
|
||||
}
|
||||
|
||||
@@ -767,7 +767,7 @@ class CloneRepositoryComponent extends React.Component<
|
||||
this.setState({ loading: true })
|
||||
|
||||
const cloneInfo = await this.resolveCloneInfo()
|
||||
const { path } = this.getSelectedTabState()
|
||||
const { path, url: originalUrl } = this.getSelectedTabState()
|
||||
|
||||
if (path == null) {
|
||||
const error = new Error(`Directory could not be created at this path.`)
|
||||
@@ -787,9 +787,15 @@ class CloneRepositoryComponent extends React.Component<
|
||||
|
||||
const { url, defaultBranch } = cloneInfo
|
||||
|
||||
// Look up the account for HTTP header auth
|
||||
const account = await findAccountForRemoteURL(
|
||||
originalUrl,
|
||||
this.props.accounts
|
||||
)
|
||||
|
||||
this.props.dispatcher.closeFoldout(FoldoutType.Repository)
|
||||
try {
|
||||
this.cloneImpl(url.trim(), path, defaultBranch)
|
||||
this.cloneImpl(url.trim(), path, defaultBranch, account)
|
||||
} catch (e) {
|
||||
log.error(`CloneRepository: clone failed to complete to ${path}`, e)
|
||||
this.setState({ loading: false })
|
||||
@@ -797,8 +803,13 @@ class CloneRepositoryComponent extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
private cloneImpl(url: string, path: string, defaultBranch?: string) {
|
||||
this.props.dispatcher.clone(url, path, { defaultBranch })
|
||||
private cloneImpl(
|
||||
url: string,
|
||||
path: string,
|
||||
defaultBranch?: string,
|
||||
account?: Account | null
|
||||
) {
|
||||
this.props.dispatcher.clone(url, path, { defaultBranch, account })
|
||||
this.props.onDismissed()
|
||||
|
||||
setDefaultDir(Path.resolve(path, '..'))
|
||||
|
||||
@@ -821,7 +821,11 @@ export class Dispatcher {
|
||||
public async clone(
|
||||
url: string,
|
||||
path: string,
|
||||
options?: { branch?: string; defaultBranch?: string }
|
||||
options?: {
|
||||
branch?: string
|
||||
defaultBranch?: string
|
||||
account?: Account | null
|
||||
}
|
||||
): Promise<Repository | null> {
|
||||
return this.appStore._completeOpenInDesktop(async () => {
|
||||
const { promise, repository } = this.appStore._clone(url, path, options)
|
||||
|
||||
Reference in New Issue
Block a user