/** * Wire-format primitives behind the smart HTTP layer, checked against real * git: the SHA-1 against node:crypto, and pack merging against git's own * index-pack, which is the consumer the merged bytes must satisfy. */ import { execFile, execFileSync } from 'node:child_process'; import { createHash, randomBytes } from 'node:crypto'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { parseBundle } from '../src/bundle.js'; import { emptyPack, mergePackStream, mergePacks, splitPack, } from '../src/pack.js'; import { concatBytes, FLUSH, parsePkts, pktLine, pktText } from '../src/pkt.js'; import { Sha1 } from '../src/sha1.js'; const execFileAsync = promisify(execFile); describe('sha1', () => { it('matches node:crypto across block boundaries', () => { for (const size of [0, 1, 55, 56, 57, 63, 64, 65, 127, 1000, 100_000]) { const data = randomBytes(size); const expected = createHash('sha1').update(data).digest('hex'); const actual = Buffer.from(new Sha1().update(data).digest()).toString( 'hex', ); expect(actual, `size ${size}`).toBe(expected); } }); it('is incremental', () => { const data = randomBytes(10_000); const hash = new Sha1(); for (let i = 0; i < data.length; i += 7) { hash.update(data.subarray(i, i + 7)); } expect(Buffer.from(hash.digest()).toString('hex')).toBe( createHash('sha1').update(data).digest('hex'), ); }); }); describe('pkt-line', () => { it('round-trips lines and flushes', () => { const stream = concatBytes([ pktLine('want abc\n'), FLUSH, pktLine('done\n'), pktLine(new Uint8Array([1, 2, 3])), ]); const pkts = parsePkts(stream); expect(pkts).toHaveLength(4); expect(pkts[0].type).toBe('line'); expect(pktText(/** @type {{data: Uint8Array}} */ (pkts[0]).data)).toBe( 'want abc', ); expect(pkts[1].type).toBe('flush'); expect(pktText(/** @type {{data: Uint8Array}} */ (pkts[2]).data)).toBe( 'done', ); expect(/** @type {{data: Uint8Array}} */ (pkts[3]).data).toEqual( new Uint8Array([1, 2, 3]), ); }); it('rejects truncated frames', () => { expect(() => parsePkts(pktLine('hello').subarray(0, 6))).toThrow(); }); }); describe('pack merging against real git', () => { /** @type {string} */ let dir = ''; const env = { .../** @type {Record} */ (process.env), GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@example.com', GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@example.com', }; /** * @param {string} cwd * @param {string[]} args */ const runGit = async (cwd, args) => { const { stdout } = await execFileAsync('git', args, { cwd, env }); return stdout.trim(); }; beforeAll(() => { dir = mkdtempSync(join(tmpdir(), 'pdsjs-git-wire-')); }); afterAll(() => { rmSync(dir, { recursive: true, force: true }); }); it('merges chained bundle packs into one git accepts', async () => { const repo = join(dir, 'repo'); await runGit(dir, ['init', '-b', 'main', repo]); writeFileSync(join(repo, 'a.txt'), 'first\n'); writeFileSync(join(repo, 'blob.bin'), randomBytes(2048)); await runGit(repo, ['add', '.']); await runGit(repo, ['commit', '-m', 'one']); const shaOne = await runGit(repo, ['rev-parse', 'HEAD']); await runGit(repo, ['bundle', 'create', join(dir, 'b1.bundle'), 'main']); writeFileSync(join(repo, 'b.txt'), 'second\n'); await runGit(repo, ['add', '.']); await runGit(repo, ['commit', '-m', 'two']); const shaTwo = await runGit(repo, ['rev-parse', 'HEAD']); await runGit(repo, [ 'bundle', 'create', join(dir, 'b2.bundle'), `^${shaOne}`, 'main', ]); const bundleOne = parseBundle(readFileSync(join(dir, 'b1.bundle'))); const bundleTwo = parseBundle(readFileSync(join(dir, 'b2.bundle'))); expect(bundleOne.refs[0].name).toBe('refs/heads/main'); expect(bundleTwo.prerequisites).toContain(shaOne); const merged = mergePacks([bundleOne.pack, bundleTwo.pack]); expect(splitPack(merged).count).toBe( splitPack(bundleOne.pack).count + splitPack(bundleTwo.pack).count, ); // The proof: a fresh repository indexes the merged pack and finds both // commits fully connected. const target = join(dir, 'target'); await runGit(dir, ['init', '-b', 'main', target]); execFileSync('git', ['index-pack', '--stdin'], { cwd: target, env, input: Buffer.from(merged), maxBuffer: 64 * 1024 * 1024, }); await runGit(target, ['cat-file', '-e', `${shaOne}^{commit}`]); await runGit(target, ['cat-file', '-e', `${shaTwo}^{commit}`]); // Full connectivity from the tip, not just object presence await runGit(target, ['rev-list', '--objects', shaTwo]); }); it('streams the same bytes it buffers, holding one read at a time', async () => { const packs = [ parseBundle(readFileSync(join(dir, 'b1.bundle'))).pack, parseBundle(readFileSync(join(dir, 'b2.bundle'))).pack, ]; const READ = 512; let largestRead = 0; const sources = packs.map((pack) => ({ count: splitPack(pack).count, length: pack.length, /** * @param {number} start * @param {number} end */ read: async function* (start, end) { for (let offset = start; offset < end; offset += READ) { const slice = pack.subarray(offset, Math.min(end, offset + READ)); largestRead = Math.max(largestRead, slice.length); yield slice; } }, })); /** @type {Uint8Array[]} */ const chunks = []; for await (const chunk of mergePackStream(sources)) chunks.push(chunk); expect(concatBytes(chunks)).toEqual(mergePacks(packs)); expect(largestRead).toBe(READ); }); it('streams a lone pack through unchanged', async () => { const pack = parseBundle(readFileSync(join(dir, 'b1.bundle'))).pack; /** @type {Uint8Array[]} */ const chunks = []; const stream = mergePackStream([ { count: splitPack(pack).count, length: pack.length, /** * @param {number} start * @param {number} end */ read: async function* (start, end) { yield pack.subarray(start, end); }, }, ]); for await (const chunk of stream) chunks.push(chunk); expect(concatBytes(chunks)).toEqual(new Uint8Array(pack)); }); it('streams an empty pack for an empty source list', async () => { /** @type {Uint8Array[]} */ const chunks = []; for await (const chunk of mergePackStream([])) chunks.push(chunk); expect(concatBytes(chunks)).toEqual(emptyPack()); }); it('produces an empty pack git accepts', async () => { const target = join(dir, 'empty-target'); await runGit(dir, ['init', '-b', 'main', target]); execFileSync('git', ['index-pack', '--stdin'], { cwd: target, env, input: Buffer.from(emptyPack()), }); }); });