100 lines · 2.6 KB
1 /**
2 * Git pack file generator.
3 *
4 * Generates a pack file (v2) from a set of git objects.
5 * Used by git-upload-pack to serve clones and fetches.
6 *
7 * Pack format:
8 * - Header: "PACK" + version 2 (4 bytes) + object count (4 bytes)
9 * - Objects: repeated { type+size header + zlib-compressed content }
10 * - Trailer: 20-byte SHA-1 of the entire pack
11 */
12
13 import { type GitObjectType, stringToPackType } from './object.js';
14 import { deflate } from './zlib.js';
15 import { sha1, hexDecode } from './sha1.js';
16
17 const encoder = new TextEncoder();
18
19 export interface PackInput {
20 type: GitObjectType;
21 content: Uint8Array;
22 }
23
24 /** Write a 4-byte big-endian unsigned integer. */
25 function writeUint32BE(value: number): Uint8Array {
26 const buf = new Uint8Array(4);
27 buf[0] = (value >>> 24) & 0xff;
28 buf[1] = (value >>> 16) & 0xff;
29 buf[2] = (value >>> 8) & 0xff;
30 buf[3] = value & 0xff;
31 return buf;
32 }
33
34 /**
35 * Encode a pack object header (type + size) in variable-length format.
36 * First byte: bits 6-4 = type, bits 3-0 = size (low 4 bits).
37 * Subsequent bytes: 7 bits of size each, high bit = continuation.
38 */
39 function encodePackObjectHeader(type: number, size: number): Uint8Array {
40 const bytes: number[] = [];
41 let byte = (type << 4) | (size & 0x0f);
42 size >>= 4;
43
44 if (size > 0) {
45 byte |= 0x80;
46 }
47 bytes.push(byte);
48
49 while (size > 0) {
50 byte = size & 0x7f;
51 size >>= 7;
52 if (size > 0) byte |= 0x80;
53 bytes.push(byte);
54 }
55
56 return new Uint8Array(bytes);
57 }
58
59 /**
60 * Generate a pack file from a list of git objects.
61 * Returns the complete pack file as a Uint8Array.
62 */
63 export async function generatePack(objects: PackInput[]): Promise<Uint8Array> {
64 const parts: Uint8Array[] = [];
65
66 // Header
67 parts.push(encoder.encode('PACK'));
68 parts.push(writeUint32BE(2)); // version 2
69 parts.push(writeUint32BE(objects.length));
70
71 // Objects
72 for (const obj of objects) {
73 const typeNum = stringToPackType(obj.type);
74 const header = encodePackObjectHeader(typeNum, obj.content.length);
75 const compressed = await deflate(obj.content);
76 parts.push(header);
77 parts.push(compressed);
78 }
79
80 // Compute total size for concatenation (without trailer)
81 const totalLen = parts.reduce((sum, p) => sum + p.length, 0);
82 const pack = new Uint8Array(totalLen);
83 let offset = 0;
84 for (const part of parts) {
85 pack.set(part, offset);
86 offset += part.length;
87 }
88
89 // Trailer: SHA-1 of everything so far
90 const checksum = await sha1(pack);
91 const checksumBytes = hexDecode(checksum);
92
93 // Final pack = data + 20-byte checksum
94 const result = new Uint8Array(pack.length + 20);
95 result.set(pack, 0);
96 result.set(checksumBytes, pack.length);
97
98 return result;
99 }
100