/** * Writing a packfile and a bundle, for a caller that has objects in hand and * no git to shell out to. * * The node helper takes the pack git hands it over the wire and never builds * one. A server merging a pull request has no such pack: it holds the commit * it just made and the objects the branch it is merging brought with it, and * a push is a bundle plus a record write. * * Every object is stored whole. A delta would be smaller, and finding a good * base is the whole cost of packing; a merge writes one commit and the trees * under it, so the saving is not worth the code that finds it. A repack on the * server side is what compacts a chain. * * Workers-clean: pako and a pure-JS SHA-1, no node imports. */ import pako from 'pako'; import { Sha1 } from './sha1.js'; /** Pack object type codes, in the order the format numbers them. */ const TYPE_CODES = { commit: 1, tree: 2, blob: 3, tag: 4 }; const encoder = new TextEncoder(); /** * One object's header: its type and its inflated size, seven bits at a time. * The first byte carries four size bits after the type, and the top bit says * another byte follows. * @param {number} type * @param {number} size * @returns {number[]} */ function objectHeader(type, size) { const bytes = [(type << 4) | (size & 0x0f)]; let rest = Math.floor(size / 16); while (rest > 0) { bytes[bytes.length - 1] |= 0x80; bytes.push(rest & 0x7f); rest >>>= 7; } return bytes; } /** * A packfile holding every object given, none of them deltified. * @param {Array<{type: string, data: Uint8Array}>} objects * @returns {Uint8Array} */ export function writePack(objects) { /** @type {Uint8Array[]} */ const parts = []; const head = new Uint8Array(12); head.set(encoder.encode('PACK'), 0); new DataView(head.buffer).setUint32(4, 2); new DataView(head.buffer).setUint32(8, objects.length); parts.push(head); for (const object of objects) { const code = TYPE_CODES[/** @type {keyof typeof TYPE_CODES} */ (object.type)]; if (!code) throw new Error(`cannot pack a ${object.type}`); parts.push(Uint8Array.from(objectHeader(code, object.data.length))); parts.push(pako.deflate(object.data)); } const body = concat(parts); const digest = new Sha1().update(body).digest(); const pack = new Uint8Array(body.length + 20); pack.set(body, 0); pack.set(digest, body.length); return pack; } /** * A v2 bundle: the refs it carries, the objects it assumes the reader already * has, and the pack. * * A prerequisite is what makes a bundle incremental. The reader must hold that * object already, which for a chain means an earlier bundle carried it. * @param {Object} bundle * @param {Array<{sha: string, name: string}>} bundle.refs * @param {string[]} [bundle.prerequisites] * @param {Uint8Array} bundle.pack * @returns {Uint8Array} */ export function writeBundle({ refs, prerequisites = [], pack }) { const lines = ['# v2 git bundle']; for (const sha of prerequisites) lines.push(`-${sha}`); for (const ref of refs) lines.push(`${ref.sha} ${ref.name}`); lines.push('', ''); return concat([encoder.encode(lines.join('\n')), pack]); } /** @param {Uint8Array[]} parts */ function concat(parts) { const size = parts.reduce((total, part) => total + part.length, 0); const all = new Uint8Array(size); let at = 0; for (const part of parts) { all.set(part, at); at += part.length; } return all; }