// Writing a pack and a bundle, read back by the parsers that read git's own. // A round trip is the whole test: a pack this writes has to index, and a // bundle it writes has to parse, exactly as one git produced does. import { describe, expect, it } from 'vitest'; import { parseBundle } from '../src/bundle.js'; import { indexPacks, objectId, parseCommit, parseTree, } from '../src/objects.js'; import { writeBundle, writePack } from '../src/pack-write.js'; const encoder = new TextEncoder(); /** @param {string} type @param {Uint8Array} data */ const object = (type, data) => ({ type, data, sha: objectId(type, data) }); /** * One object's bytes out of an index, for a test that has just packed it and * knows it is there. * @param {Map} objects @param {string} sha */ const at = (objects, sha) => /** @type {{data: Uint8Array}} */ (objects.get(sha)).data; const blob = object('blob', encoder.encode('hello\n')); /** * A tree holding one file, in git's own entry format. * @param {string} name @param {string} mode @param {string} sha */ function treeOf(name, mode, sha) { const prefix = encoder.encode(`${mode} ${name}\0`); const raw = Uint8Array.from( (sha.match(/../g) ?? []).map((/** @type {string} */ pair) => Number.parseInt(pair, 16), ), ); const data = new Uint8Array(prefix.length + raw.length); data.set(prefix, 0); data.set(raw, prefix.length); return object('tree', data); } const tree = treeOf('greeting.txt', '100644', blob.sha); const commit = object( 'commit', encoder.encode( [ `tree ${tree.sha}`, 'author Bob 1756000000 +0000', 'committer Bob 1756000000 +0000', '', 'say hello', '', ].join('\n'), ), ); describe('writePack', () => { it('writes a pack the object index reads back whole', async () => { const pack = writePack([commit, tree, blob]); const objects = await indexPacks([pack]); expect(objects.get(commit.sha)?.type).toBe('commit'); expect(objects.get(tree.sha)?.type).toBe('tree'); expect(new TextDecoder().decode(objects.get(blob.sha)?.data)).toBe( 'hello\n', ); expect(parseCommit(at(objects, commit.sha)).tree).toBe(tree.sha); expect(parseTree(at(objects, tree.sha))[0].sha).toBe(blob.sha); }); it('carries a size past the four bits the first byte holds', async () => { // 4096 bytes needs three header bytes, which is the continuation the // one-file case above never exercises. const big = object('blob', new Uint8Array(4096).fill(0x61)); const objects = await indexPacks([writePack([big])]); expect(objects.get(big.sha)?.data.length).toBe(4096); }); it('writes a pack of nothing that still indexes', async () => { expect((await indexPacks([writePack([])])).size).toBe(0); }); it('refuses an object type a pack has no code for', () => { expect(() => writePack([{ type: 'ofs-delta', data: blob.data }])).toThrow( /cannot pack/, ); }); }); describe('writeBundle', () => { it('writes a bundle the parser reads back, pack and all', async () => { const pack = writePack([commit, tree, blob]); const parsed = parseBundle( writeBundle({ refs: [{ sha: commit.sha, name: 'refs/heads/main' }], prerequisites: ['c'.repeat(40)], pack, }), ); expect(parsed.version).toBe(2); expect(parsed.refs).toEqual([{ sha: commit.sha, name: 'refs/heads/main' }]); expect(parsed.prerequisites).toEqual(['c'.repeat(40)]); expect((await indexPacks([parsed.pack])).get(commit.sha)?.type).toBe( 'commit', ); }); it('writes a bundle with no prerequisites, which is a full one', () => { const parsed = parseBundle( writeBundle({ refs: [{ sha: commit.sha, name: 'refs/heads/main' }], pack: writePack([commit, tree, blob]), }), ); expect(parsed.prerequisites).toEqual([]); }); });