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