/** * Git bundle file parsing: a text header (format marker, optional * capabilities, prerequisites, refs) terminated by a blank line, followed by * a complete packfile. */ const decoder = new TextDecoder(); /** * @typedef {Object} ParsedBundle * @property {2|3} version * @property {Array<{sha: string, name: string}>} refs * @property {string[]} prerequisites * @property {Record} capabilities * @property {Uint8Array} pack - the complete packfile region */ /** * @param {Uint8Array} bytes * @returns {ParsedBundle} */ export function parseBundle(bytes) { /** @type {Array<{sha: string, name: string}>} */ const refs = []; /** @type {string[]} */ const prerequisites = []; /** @type {Record} */ const capabilities = {}; /** @type {2|3|null} */ let version = null; let offset = 0; let lineStart = 0; let sawSignature = false; while (offset < bytes.length) { if (bytes[offset] !== 0x0a) { offset++; continue; } const line = decoder.decode(bytes.subarray(lineStart, offset)); offset++; lineStart = offset; if (!sawSignature) { if (line === '# v2 git bundle') version = 2; else if (line === '# v3 git bundle') version = 3; else throw new Error('not a git bundle'); sawSignature = true; continue; } if (line === '') break; if (line.startsWith('@')) { const eq = line.indexOf('='); if (eq === -1) capabilities[line.slice(1)] = ''; else capabilities[line.slice(1, eq)] = line.slice(eq + 1); continue; } if (line.startsWith('-')) { prerequisites.push(line.slice(1).split(' ')[0]); continue; } const space = line.indexOf(' '); if (space === -1) throw new Error('malformed bundle ref line'); refs.push({ sha: line.slice(0, space), name: line.slice(space + 1) }); } if (version === null) throw new Error('not a git bundle'); return { version, refs, prerequisites, capabilities, pack: bytes.subarray(lineStart), }; }