feat: add repository git credential fixer

This commit is contained in:
Agent committed 2026-10-18 10:55:21 +00:00
1 parent 25cea75eb5
commit c6e042db86
7 files changed
+120 -10

No files matched your search

+46 -3
View File
@@ -297,9 +297,13 @@ export async function listRepos(account, { query, owner, max = 300, signal } = {
// ---- git operations ---------------------------------------------------------
export function credentialHelperCmd() {
return `!'${process.execPath.replace(/'/g, "'\\''")}' '${HELPER_PATH.replace(/'/g, "'\\''")}'`
}
/** git -c options that make git ask our helper for credentials of known accounts. */
export function credentialArgs() {
const helper = `!'${process.execPath.replace(/'/g, "'\\''")}' '${HELPER_PATH.replace(/'/g, "'\\''")}'`
const helper = credentialHelperCmd()
return ['-c', 'credential.helper=', '-c', `credential.helper=${helper}`]
}
@@ -347,8 +351,7 @@ export async function cloneRepo({ url, parent, name, branch, depth, signal }) {
}
// Keep the repo's own pushes/fetches authenticated through the helper.
if (account) {
await run('git', ['-C', target, 'config', '--local', '--add', 'credential.helper', ''])
await run('git', ['-C', target, 'config', '--local', '--add', 'credential.helper', credentialArgs()[3].slice('credential.helper='.length)])
await fixRepoConfig(target)
}
return { path: target, url: effective, account: account ? account.id : null }
}
@@ -421,3 +424,43 @@ export async function resolveRepo({ repo, account: accountId, cwd, remote = 'ori
if (!account) throw new ForgeError(`no authorized account for host ${parsed.host}; available: ${accounts.map((a) => `${a.id} (${a.host})`).join(', ')}`, 400)
return { account, owner: parsed.owner, name: parsed.name }
}
/** Configure or update a local repository to use the git-forge credential helper. */
export async function fixRepoConfig(repoPath) {
const top = await gitIn(repoPath, ['rev-parse', '--show-toplevel'])
if (top.code !== 0) throw new ForgeError(`not a git repository: ${repoPath}`, 400)
const root = top.stdout.trim()
const values = (result) => {
if (!result || result.code !== 0 || result.stdout === '') return []
const text = String(result.stdout)
return (text.endsWith('\n') ? text.slice(0, -1) : text).split('\n')
}
const existing = await gitIn(root, ['config', '--local', '--get-all', 'credential.helper'])
const previous = values(existing)
const cmd = credentialHelperCmd()
const unset = await gitIn(root, ['config', '--local', '--unset-all', 'credential.helper'])
// git config exits with 5 when there was no value to unset; that is expected.
if (unset.code !== 0 && unset.code !== 5) {
throw new ForgeError(`could not update local git credentials in ${root}: ${unset.stderr || 'failed to clear credential.helper'}`, 422)
}
const reset = await gitIn(root, ['config', '--local', '--add', 'credential.helper', ''])
if (reset.code !== 0) {
throw new ForgeError(`could not update local git credentials in ${root}: ${reset.stderr || 'failed to reset credential.helper'}`, 422)
}
const add = await gitIn(root, ['config', '--local', '--add', 'credential.helper', cmd])
if (add.code !== 0) {
throw new ForgeError(`could not update local git credentials in ${root}: ${add.stderr || 'failed to install credential.helper'}`, 422)
}
const check = await gitIn(root, ['config', '--local', '--get-all', 'credential.helper'])
if (check.code !== 0) throw new ForgeError(`could not verify local git credentials in ${root}`, 422)
return {
path: root,
fixed: true,
helper: cmd,
previous,
current: values(check),
}
}