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 6.2 kB View raw
1import { execFileSync } from 'node:child_process' 2import { afterEach, beforeEach, describe, expect, it } from 'vitest' 3import { RemoteRejectedError } from '../../server/utils/git-wire/errors' 4import { ReceivePackSession } from '../../server/utils/git-wire/receive-pack' 5import { ZERO_SHA } from '../../server/utils/git-wire/refs' 6import { fakeGithubFetch, GitFixture, localReceivePackFactory } from '../utils/git-wire' 7import { fetchPack } from '../../server/utils/git-wire/upload-pack' 8 9async function* fromBuffer(b: Buffer): AsyncGenerator<Buffer> { 10 yield b 11} 12 13async function push(factory: ReturnType<typeof localReceivePackFactory>, updates: Parameters<ReceivePackSession['push']>[0], pack: AsyncIterable<Buffer> | null) { 14 const session = await ReceivePackSession.open(factory) 15 await session.push(updates, pack) 16} 17 18async function drain(gen: AsyncGenerator<Buffer>): Promise<Buffer> { 19 const parts: Buffer[] = [] 20 for await (const c of gen) parts.push(c) 21 return Buffer.concat(parts) 22} 23 24describe('receive-pack (against real git-receive-pack)', () => { 25 let fx: GitFixture 26 let realFetch: typeof globalThis.fetch 27 28 beforeEach(() => { 29 fx = new GitFixture() 30 realFetch = globalThis.fetch 31 }) 32 33 afterEach(() => { 34 globalThis.fetch = realFetch 35 fx.cleanup() 36 }) 37 38 /** Build a pack on disk for `want` and return it as a single buffer. */ 39 async function packFor(ghBare: string, want: string, haves: string[]): Promise<Buffer> { 40 globalThis.fetch = fakeGithubFetch(new Map([['owner/repo', ghBare]])) as unknown as typeof globalThis.fetch 41 const { pack } = await fetchPack({ repoFullName: 'owner/repo', token: 't', want, haves, maxBytes: 1 << 30 }) 42 return drain(pack) 43 } 44 45 it('pushes a new ref into an empty knot repo', async () => { 46 const gh = fx.initBare('gh.git') 47 const work = fx.initWork('work') 48 const sha = fx.commit(work, 'a.txt', 'hello') 49 fx.pushTo(work, gh, 'HEAD:refs/heads/main') 50 const knot = fx.initBare('knot.git') 51 52 const pack = await packFor(gh, sha, []) 53 await push( 54 localReceivePackFactory(knot), 55 [{ ref: 'refs/heads/main', old: ZERO_SHA, next: sha }], 56 fromBuffer(pack), 57 ) 58 59 expect(fx.revParse(knot, 'refs/heads/main')).toBe(sha) 60 }) 61 62 it('fast-forwards an existing ref with a thin incremental pack', async () => { 63 const gh = fx.initBare('gh.git') 64 const work = fx.initWork('work') 65 const first = fx.commit(work, 'a.txt', 'one') 66 fx.pushTo(work, gh, 'HEAD:refs/heads/main') 67 const knot = fx.initBare('knot.git') 68 await push(localReceivePackFactory(knot), [{ ref: 'refs/heads/main', old: ZERO_SHA, next: first }], fromBuffer(await packFor(gh, first, []))) 69 70 const second = fx.commit(work, 'b.txt', 'two') 71 fx.pushTo(work, gh, 'HEAD:refs/heads/main') 72 await push(localReceivePackFactory(knot), [{ ref: 'refs/heads/main', old: first, next: second }], fromBuffer(await packFor(gh, second, [first]))) 73 74 expect(fx.revParse(knot, 'refs/heads/main')).toBe(second) 75 }) 76 77 it('rejects a stale compare-and-swap as stale-old-sha', async () => { 78 const gh = fx.initBare('gh.git') 79 const work = fx.initWork('work') 80 const first = fx.commit(work, 'a.txt', 'one') 81 fx.pushTo(work, gh, 'HEAD:refs/heads/main') 82 const knot = fx.initBare('knot.git') 83 await push(localReceivePackFactory(knot), [{ ref: 'refs/heads/main', old: ZERO_SHA, next: first }], fromBuffer(await packFor(gh, first, []))) 84 85 const second = fx.commit(work, 'b.txt', 'two') 86 fx.pushTo(work, gh, 'HEAD:refs/heads/main') 87 88 // Claim the knot is still empty when it actually points at `first`. 89 await expect( 90 push(localReceivePackFactory(knot), [{ ref: 'refs/heads/main', old: ZERO_SHA, next: second }], fromBuffer(await packFor(gh, second, [first]))), 91 ).rejects.toMatchObject({ constructor: RemoteRejectedError, reason: 'stale-old-sha' }) 92 }) 93 94 it('deletes a ref with no pack', async () => { 95 const gh = fx.initBare('gh.git') 96 const work = fx.initWork('work') 97 const sha = fx.commit(work, 'a.txt', 'hello') 98 fx.pushTo(work, gh, 'HEAD:refs/heads/main') 99 const knot = fx.initBare('knot.git') 100 await push(localReceivePackFactory(knot), [{ ref: 'refs/heads/main', old: ZERO_SHA, next: sha }], fromBuffer(await packFor(gh, sha, []))) 101 102 await push(localReceivePackFactory(knot), [{ ref: 'refs/heads/main', old: sha, next: ZERO_SHA }], null) 103 104 expect(() => execFileSync('git', ['rev-parse', 'refs/heads/main'], { cwd: knot })).toThrow(/unknown revision|ambiguous argument|fatal/) 105 }) 106 107 it('kills a stalled session once the watchdog fires', async () => { 108 // A factory whose child accepts the connection but never advertises: the 109 // open() read would block forever without the watchdog. 110 const stalled = () => { 111 let resolveDone: (code: number | null) => void 112 const done = new Promise<number | null>(r => { resolveDone = r }) 113 // eslint-disable-next-line require-yield -- models a stalled stream that blocks until killed and never emits 114 async function* neverYields(): AsyncGenerator<Buffer> { 115 await done 116 } 117 return { 118 stdin: { write: (_d: unknown, cb?: (e?: Error) => void) => cb?.(), end: () => {} } as unknown as NodeJS.WritableStream, 119 stdout: neverYields(), 120 stderr: () => '', 121 kill: () => resolveDone(null), 122 done, 123 } 124 } 125 await expect(ReceivePackSession.open(stalled, 50)).rejects.toThrow(/end of stream|advertisement/) 126 }) 127 128 it('pushes an annotated tag', async () => { 129 const gh = fx.initBare('gh.git') 130 const work = fx.initWork('work') 131 fx.commit(work, 'a.txt', 'hello') 132 fx.pushTo(work, gh, 'HEAD:refs/heads/main') 133 fx.git(['tag', '-a', 'v1', '-m', 'release'], work) 134 const tagSha = fx.git(['rev-parse', 'refs/tags/v1'], work) 135 fx.pushTo(work, gh, 'refs/tags/v1:refs/tags/v1') 136 const knot = fx.initBare('knot.git') 137 138 await push( 139 localReceivePackFactory(knot), 140 [{ ref: 'refs/tags/v1', old: ZERO_SHA, next: tagSha }], 141 fromBuffer(await packFor(gh, tagSha, [])), 142 ) 143 144 expect(fx.revParse(knot, 'refs/tags/v1')).toBe(tagSha) 145 expect(fx.git(['cat-file', '-t', 'refs/tags/v1'], knot)).toBe('tag') 146 }) 147})