mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

1import { 2 type ReceivePackFactory, 3 ReceivePackSession, 4 type RefUpdate, 5 ssh2ReceivePackFactory, 6} from './git-wire/receive-pack' 7import { ZERO_SHA } from './git-wire/refs' 8import { fetchPack } from './git-wire/upload-pack' 9import { loadSshKeyForInstall } from './ssh-cmd' 10import { sshEndpointForKnot } from './sync-push-host' 11 12const DEFAULT_MAX_PACK_BYTES = 1024 * 1024 * 1024 13/** Cap haves so a repo with thousands of refs can't bloat the negotiation. */ 14const MAX_HAVES = 256 15 16function maxPackBytes(): number { 17 const raw = process.env.NUXT_MAX_PACK_BYTES 18 if (!raw) return DEFAULT_MAX_PACK_BYTES 19 const n = Number.parseInt(raw, 10) 20 return Number.isNaN(n) || n <= 0 ? DEFAULT_MAX_PACK_BYTES : n 21} 22 23async function sshFactory(installationId: number, knot: string, repoDid: string): Promise<ReceivePackFactory> { 24 const privateKey = await loadSshKeyForInstall(installationId) 25 const { host, port } = sshEndpointForKnot(knot) 26 // ssh:// path form: leading slash, the knot resolves the repo by DID. 27 return ssh2ReceivePackFactory({ host, port, repoPath: `/${repoDid}`, privateKey }) 28} 29 30export interface SplicePushParams { 31 installationId: number 32 repoFullName: string 33 knot: string 34 repoDid: string 35 /** Fully-qualified ref, e.g. `refs/heads/main`. */ 36 ref: string 37 /** The SHA to land on the knot. */ 38 want: string 39 /** GitHub installation token authorising the fetch. */ 40 token: string 41} 42 43export interface SplicePushResult { 44 status: 'synced' | 'already-synced' 45 sha: string 46} 47 48/** 49 * Stream a single ref update from GitHub to the knot without materialising a 50 * repository: 51 * 52 * 1. open receive-pack, read the knot's tips; 53 * 2. if the knot's tip for `ref` already equals `want`, no-op; 54 * 3. fetch a thin pack from GitHub with the knot's tips as haves; 55 * 4. send the compare-and-swap command and pipe the pack straight through; 56 * 5. read report-status. 57 * 58 * Steps 1 and 3 share one ssh session: it sits idle for the duration of the 59 * GitHub round-trip (receive-pack waits indefinitely for commands), which 60 * keeps the knot's advertised tip as the authoritative compare-and-swap base. 61 */ 62export async function splicePush(params: SplicePushParams): Promise<SplicePushResult> { 63 const factory = await sshFactory(params.installationId, params.knot, params.repoDid) 64 return runSplice(factory, params) 65} 66 67/** The fetch + push exchange over an open session. Split out for the wire test. */ 68export async function runSplice( 69 factory: ReceivePackFactory, 70 params: { repoFullName: string, ref: string, want: string, token: string }, 71): Promise<SplicePushResult> { 72 const session = await ReceivePackSession.open(factory) 73 let pushStarted = false 74 try { 75 const old = session.tips.get(params.ref) ?? ZERO_SHA 76 if (old === params.want) { 77 await session.close() 78 return { status: 'already-synced', sha: params.want } 79 } 80 81 const haves = [...new Set(session.tips.values())] 82 .filter(sha => sha !== ZERO_SHA) 83 .slice(0, MAX_HAVES) 84 85 const { pack } = await fetchPack({ 86 repoFullName: params.repoFullName, 87 token: params.token, 88 want: params.want, 89 haves, 90 maxBytes: maxPackBytes(), 91 }) 92 93 const update: RefUpdate = { ref: params.ref, old, next: params.want } 94 pushStarted = true 95 await session.push([update], pack) 96 return { status: 'synced', sha: params.want } 97 } 98 finally { 99 // `push` tears the session down itself; only close here if we threw before 100 // reaching it (e.g. the byte cap fired inside fetchPack's stream). 101 if (!pushStarted) await session.close() 102 } 103} 104 105export interface SpliceDeleteResult { 106 status: 'synced' | 'already-absent' 107} 108 109/** 110 * Delete a ref on the knot. No GitHub leg and no pack: read the knot's 111 * advertisement, and if the ref is absent we're already done (idempotent). 112 * Otherwise send a delete command with the advertised value as the 113 * compare-and-swap base. 114 */ 115export async function spliceDelete(params: { 116 installationId: number 117 knot: string 118 repoDid: string 119 ref: string 120}): Promise<SpliceDeleteResult> { 121 const factory = await sshFactory(params.installationId, params.knot, params.repoDid) 122 return runSpliceDelete(factory, params.ref) 123} 124 125/** The delete exchange over an open session. Split out for the wire test. */ 126export async function runSpliceDelete(factory: ReceivePackFactory, ref: string): Promise<SpliceDeleteResult> { 127 const session = await ReceivePackSession.open(factory) 128 const old = session.tips.get(ref) 129 if (!old || old === ZERO_SHA) { 130 await session.close() 131 return { status: 'already-absent' } 132 } 133 // push() owns teardown for the success and rejection paths. 134 await session.push([{ ref, old, next: ZERO_SHA }], null) 135 return { status: 'synced' } 136}