Something went wrong. Try again.
The `gh` equivalent for tangled.org
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394import { spawnSync, type SpawnSyncOptions } from 'node:child_process';
// Running git, and reading what it knows: remotes, branches, config.
export class GitError extends Error { readonly status: number | null; readonly stderr: string;
constructor(args: string[], status: number | null, stderr: string) { super(`git ${args.join(' ')} failed${stderr ? `: ${stderr.trim()}` : ''}`); this.name = 'GitError'; this.status = status; this.stderr = stderr; }}
/** Run git and return its stdout, trimmed. Throws on a non-zero exit. */export function git(args: string[], options: SpawnSyncOptions = {}): string { const result = spawnSync('git', args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, ...options }); if (result.error) throw new Error(`could not run git: ${result.error.message}`); if (result.status !== 0) throw new GitError(args, result.status, String(result.stderr ?? '')); return String(result.stdout ?? '').replace(/\n$/, '');}
/** Run git, and return undefined rather than throw. */export function gitMaybe(args: string[], options: SpawnSyncOptions = {}): string | undefined { try { return git(args, options); } catch { return undefined; }}
/** Run git attached to the terminal; returns its exit status. */export function gitInteractive(args: string[], options: SpawnSyncOptions = {}): number { const result = spawnSync('git', args, { stdio: 'inherit', ...options }); if (result.error) throw new Error(`could not run git: ${result.error.message}`); return result.status ?? 1;}
export function insideWorkTree(cwd?: string): boolean { return gitMaybe(['rev-parse', '--is-inside-work-tree'], cwd ? { cwd } : {}) === 'true';}
export function currentBranch(cwd?: string): string | undefined { return gitMaybe(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd ? { cwd } : {});}
export interface Remote { name: string; fetch: string; push: string;}
export function remotes(cwd?: string): Remote[] { const output = gitMaybe(['remote', '-v'], cwd ? { cwd } : {}); if (!output) return []; const found = new Map<string, Remote>(); for (const line of output.split('\n')) { const match = /^(\S+)\s+(\S+)\s+\((fetch|push)\)$/.exec(line); if (!match) continue; const [, name, url, kind] = match; const remote = found.get(name!) ?? { name: name!, fetch: url!, push: url! }; if (kind === 'fetch') remote.fetch = url!; else remote.push = url!; found.set(name!, remote); } // The usual suspects first, like gh: upstream, then origin, then the rest. const rank = (name: string) => name === 'upstream' ? 0 : name === 'origin' ? 1 : 2; return [...found.values()].sort((a, b) => rank(a.name) - rank(b.name));}
export function configGet(key: string, { global = false, cwd }: { global?: boolean; cwd?: string } = {}): string | undefined { return gitMaybe(['config', ...(global ? ['--global'] : []), '--get', key], cwd ? { cwd } : {});}
export function configGetAll(key: string, { global = false, cwd }: { global?: boolean; cwd?: string } = {}): string[] { const value = gitMaybe(['config', ...(global ? ['--global'] : []), '--get-all', key], cwd ? { cwd } : {}); return value === undefined ? [] : value.split('\n');}
/** Whether HEAD has commits not on `upstream`. */export function hasCommitsAhead(base: string, head = 'HEAD', cwd?: string): boolean { const count = gitMaybe(['rev-list', '--count', `${base}..${head}`], cwd ? { cwd } : {}); return Number(count) > 0;}
/** Run git with `input` on stdin and its output on the terminal; returns its exit status. */export function gitFeed(args: string[], input: string, options: SpawnSyncOptions = {}): number { const result = spawnSync('git', args, { input, stdio: ['pipe', 'inherit', 'inherit'], ...options }); if (result.error) throw new Error(`could not run git: ${result.error.message}`); return result.status ?? 1;}