mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

1import { execa, type Options } from 'execa' 2 3/** 4 * Thin wrapper over `execa` for invoking the system `git` binary with 5 * predictable defaults. 6 * 7 * - Forces non-interactive mode so a misconfigured ssh setup never hangs 8 * waiting for a passphrase or `yes/no` prompt. 9 * - Captures stderr so callers can produce useful error messages. 10 * - Adds a default 60s timeout; callers can override via `options.timeout`. 11 */ 12export async function git(args: string[], options: Options = {}): Promise<{ stdout: string, stderr: string }> { 13 const result = await execa('git', args, { 14 timeout: 60_000, 15 ...options, 16 env: { 17 // Belt and braces against interactive prompts. `GIT_TERMINAL_PROMPT=0` 18 // makes git fail rather than hang if it would otherwise ask for input 19 // (e.g. credentials). 20 GIT_TERMINAL_PROMPT: '0', 21 // Don't pick up the running user's ssh config / known_hosts. The caller 22 // supplies a complete GIT_SSH_COMMAND for ssh transports. 23 GIT_CONFIG_NOSYSTEM: '1', 24 ...options.env, 25 }, 26 // Buffer (default) is fine for small operations; for very large fetches 27 // we'd want to stream stderr instead. 28 reject: true, 29 all: true, 30 }) 31 return { stdout: String(result.stdout), stderr: String(result.stderr) } 32} 33 34/** 35 * Recognised remote rejection patterns from the knot when a repo no longer 36 * exists or our key has been revoked. Surfaces as a typed error so the 37 * worker can mark the mapping as terminally failed rather than retry forever. 38 */ 39export class RemoteRejectedPushError extends Error { 40 constructor(message: string, public readonly reason: 'repo-gone' | 'auth-rejected' | 'other') { 41 super(message) 42 this.name = 'RemoteRejectedPushError' 43 } 44} 45 46export function classifyPushFailure(stderr: string): RemoteRejectedPushError | null { 47 const lc = stderr.toLowerCase() 48 if (lc.includes('repository not found') || lc.includes('does not exist') || lc.includes('does not appear to be a git repository')) { 49 return new RemoteRejectedPushError(stderr.trim(), 'repo-gone') 50 } 51 if (lc.includes('permission denied') || lc.includes('publickey') && lc.includes('denied')) { 52 return new RemoteRejectedPushError(stderr.trim(), 'auth-rejected') 53 } 54 return null 55}