mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

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 return repo 32 } 33 34 /** Create a non-bare work repo with one commit on `main`, return its path. */ 35 initWork(name: string): string { 36 const repo = path.join(this.dir, name) 37 this.git(['init', '-q', '-b', 'main', repo]) 38 this.git(['config', 'user.email', 't@example.com'], repo) 39 this.git(['config', 'user.name', 'Test'], repo) 40 return repo 41 } 42 43 commit(workRepo: string, file: string, content: string): string { 44 execFileSync('bash', ['-c', `printf %s ${JSON.stringify(content)} > ${JSON.stringify(path.join(workRepo, file))}`]) 45 this.git(['add', '.'], workRepo) 46 this.git(['commit', '-q', '-m', `add ${file}`], workRepo) 47 return this.git(['rev-parse', 'HEAD'], workRepo) 48 } 49 50 pushTo(workRepo: string, bareRepo: string, refspec: string): void { 51 this.git(['push', '-q', bareRepo, refspec], workRepo) 52 } 53 54 revParse(repo: string, ref: string): string { 55 return this.git(['rev-parse', ref], repo) 56 } 57 58 cleanup(): void { 59 rmSync(this.dir, { recursive: true, force: true }) 60 } 61} 62 63/** 64 * Stand in for GitHub's `git-upload-pack` HTTP endpoint by running the binary 65 * in `--stateless-rpc` mode against a local bare repo. Mirrors the request / 66 * response framing the real endpoint uses, so `upload-pack.ts` can be pointed 67 * at it through a patched `fetch`. 68 */ 69export function fakeGithubFetch(repos: Map<string, string>) { 70 return async function fetchImpl(input: string | URL, init?: RequestInit): Promise<Response> { 71 const url = typeof input === 'string' ? input : input.toString() 72 const match = url.match(/github\.com\/(.+?)\.git\/(info\/refs|git-upload-pack)/) 73 if (!match) throw new Error(`fakeGithubFetch: unexpected url ${url}`) 74 const repoPath = repos.get(match[1]) 75 if (!repoPath) return new Response(null, { status: 404, statusText: 'Not Found' }) 76 77 if (match[2] === 'info/refs') { 78 const adv = execFileSync('git-upload-pack', ['--stateless-rpc', '--advertise-refs', repoPath]) 79 // The HTTP transport prepends the service banner + flush; the binary does not. 80 const banner = Buffer.concat([ 81 encodePktLine('# service=git-upload-pack\n'), 82 flushPkt, 83 ]) 84 return new Response(new Uint8Array(Buffer.concat([banner, adv])), { status: 200 }) 85 } 86 87 const reqBody = Buffer.from(await new Response(init!.body as BodyInit).arrayBuffer()) 88 const proc = spawnSync('git-upload-pack', ['--stateless-rpc', repoPath], { input: reqBody, maxBuffer: 1 << 30 }) 89 if (proc.status !== 0) { 90 throw new Error(`git-upload-pack exited ${proc.status}: ${proc.stderr.toString()}`) 91 } 92 return new Response(new Uint8Array(proc.stdout), { status: 200 }) 93 } 94} 95 96const STDERR_CAP = 16 * 1024 97 98/** 99 * A `ReceivePackFactory` that spawns the real `git-receive-pack` binary 100 * against a local bare repo, bypassing ssh. The stdio protocol is identical 101 * to what the knot speaks. 102 */ 103export function localReceivePackFactory(bareRepo: string): ReceivePackFactory { 104 return () => { 105 const child: ChildProcessWithoutNullStreams = spawn('git-receive-pack', [bareRepo], { 106 stdio: ['pipe', 'pipe', 'pipe'], 107 }) 108 let stderrBuf = Buffer.alloc(0) 109 child.stderr.on('data', (chunk: Buffer) => { 110 stderrBuf = Buffer.concat([stderrBuf, chunk]).subarray(-STDERR_CAP) 111 }) 112 const done = new Promise<number | null>(resolve => child.on('close', code => resolve(code))) 113 return { 114 stdin: child.stdin, 115 stdout: child.stdout, 116 stderr: () => stderrBuf.toString('utf8'), 117 kill: () => child.kill('SIGKILL'), 118 done, 119 } 120 } 121}