mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

at main 4.7 kB View raw
1import { type ChildProcessWithoutNullStreams, execFileSync, spawn, spawnSync } from 'node:child_process' 2import { mkdtempSync, rmSync } from 'node:fs' 3import os from 'node:os' 4import path from 'node:path' 5import type { ReceivePackFactory } from '../../server/utils/git-wire/receive-pack' 6import { encodePktLine, flushPkt } from '../../server/utils/git-wire/pkt-line' 7 8/** 9 * Local git fixtures for wire-protocol tests. No network, no ssh: we drive the 10 * same `git-upload-pack` / `git-receive-pack` binaries GitHub and the knot run 11 * server-side, so the bytes on the pipe are real protocol output. 12 */ 13export class GitFixture { 14 readonly dir: string 15 16 constructor() { 17 this.dir = mkdtempSync(path.join(os.tmpdir(), 'gitwire-test-')) 18 } 19 20 git(args: string[], cwd = this.dir): string { 21 return execFileSync('git', args, { 22 cwd, 23 encoding: 'utf8', 24 env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' }, 25 }).trim() 26 } 27 28 initBare(name: string): string { 29 const repo = path.join(this.dir, name) 30 this.git(['init', '-q', '--bare', repo]) 31 this.git(['symbolic-ref', 'HEAD', 'refs/heads/__synchub_placeholder'], repo) 32 return repo 33 } 34 35 /** Create a non-bare work repo with one commit on `main`, return its path. */ 36 initWork(name: string): string { 37 const repo = path.join(this.dir, name) 38 this.git(['init', '-q', '-b', 'main', repo]) 39 this.git(['config', 'user.email', 't@example.com'], repo) 40 this.git(['config', 'user.name', 'Test'], repo) 41 return repo 42 } 43 44 commit(workRepo: string, file: string, content: string): string { 45 execFileSync('bash', ['-c', `printf %s ${JSON.stringify(content)} > ${JSON.stringify(path.join(workRepo, file))}`]) 46 this.git(['add', '.'], workRepo) 47 this.git(['commit', '-q', '-m', `add ${file}`], workRepo) 48 return this.git(['rev-parse', 'HEAD'], workRepo) 49 } 50 51 pushTo(workRepo: string, bareRepo: string, refspec: string): void { 52 this.git(['push', '-q', bareRepo, refspec], workRepo) 53 } 54 55 revParse(repo: string, ref: string): string { 56 return this.git(['rev-parse', ref], repo) 57 } 58 59 cleanup(): void { 60 rmSync(this.dir, { recursive: true, force: true }) 61 } 62} 63 64/** 65 * Stand in for GitHub's `git-upload-pack` HTTP endpoint by running the binary 66 * in `--stateless-rpc` mode against a local bare repo. Mirrors the request / 67 * response framing the real endpoint uses, so `upload-pack.ts` can be pointed 68 * at it through a patched `fetch`. 69 */ 70export function fakeGithubFetch(repos: Map<string, string>) { 71 return async function fetchImpl(input: string | URL, init?: RequestInit): Promise<Response> { 72 const url = typeof input === 'string' ? input : input.toString() 73 const match = url.match(/github\.com\/(.+?)\.git\/(info\/refs|git-upload-pack)/) 74 if (!match) throw new Error(`fakeGithubFetch: unexpected url ${url}`) 75 const repoPath = repos.get(match[1]!) 76 if (!repoPath) return new Response(null, { status: 404, statusText: 'Not Found' }) 77 78 if (match[2] === 'info/refs') { 79 const adv = execFileSync('git-upload-pack', ['--stateless-rpc', '--advertise-refs', repoPath]) 80 // The HTTP transport prepends the service banner + flush; the binary does not. 81 const banner = Buffer.concat([ 82 encodePktLine('# service=git-upload-pack\n'), 83 flushPkt, 84 ]) 85 return new Response(new Uint8Array(Buffer.concat([banner, adv])), { status: 200 }) 86 } 87 88 const reqBody = Buffer.from(await new Response(init!.body as BodyInit).arrayBuffer()) 89 const proc = spawnSync('git-upload-pack', ['--stateless-rpc', repoPath], { input: reqBody, maxBuffer: 1 << 30 }) 90 if (proc.status !== 0) { 91 throw new Error(`git-upload-pack exited ${proc.status}: ${proc.stderr.toString()}`) 92 } 93 return new Response(new Uint8Array(proc.stdout), { status: 200 }) 94 } 95} 96 97const STDERR_CAP = 16 * 1024 98 99/** 100 * A `ReceivePackFactory` that spawns the real `git-receive-pack` binary 101 * against a local bare repo, bypassing ssh. The stdio protocol is identical 102 * to what the knot speaks. 103 */ 104export function localReceivePackFactory(bareRepo: string): ReceivePackFactory { 105 return () => { 106 const child: ChildProcessWithoutNullStreams = spawn('git-receive-pack', [bareRepo], { 107 stdio: ['pipe', 'pipe', 'pipe'], 108 }) 109 let stderrBuf = Buffer.alloc(0) 110 child.stderr.on('data', (chunk: Buffer) => { 111 stderrBuf = Buffer.concat([stderrBuf, chunk]).subarray(-STDERR_CAP) 112 }) 113 const done = new Promise<number | null>(resolve => child.on('close', code => resolve(code))) 114 return { 115 stdin: child.stdin, 116 stdout: child.stdout, 117 stderr: () => stderrBuf.toString('utf8'), 118 kill: () => child.kill('SIGKILL'), 119 done, 120 } 121 } 122}