gitx.do 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/durable-object/object-store.d.ts +113 -0
- package/dist/durable-object/object-store.d.ts.map +1 -0
- package/dist/durable-object/object-store.js +387 -0
- package/dist/durable-object/object-store.js.map +1 -0
- package/dist/durable-object/schema.d.ts +17 -0
- package/dist/durable-object/schema.d.ts.map +1 -0
- package/dist/durable-object/schema.js +43 -0
- package/dist/durable-object/schema.js.map +1 -0
- package/dist/durable-object/wal.d.ts +111 -0
- package/dist/durable-object/wal.d.ts.map +1 -0
- package/dist/durable-object/wal.js +200 -0
- package/dist/durable-object/wal.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/adapter.d.ts +231 -0
- package/dist/mcp/adapter.d.ts.map +1 -0
- package/dist/mcp/adapter.js +502 -0
- package/dist/mcp/adapter.js.map +1 -0
- package/dist/mcp/sandbox.d.ts +261 -0
- package/dist/mcp/sandbox.d.ts.map +1 -0
- package/dist/mcp/sandbox.js +983 -0
- package/dist/mcp/sandbox.js.map +1 -0
- package/dist/mcp/sdk-adapter.d.ts +413 -0
- package/dist/mcp/sdk-adapter.d.ts.map +1 -0
- package/dist/mcp/sdk-adapter.js +672 -0
- package/dist/mcp/sdk-adapter.js.map +1 -0
- package/dist/mcp/tools.d.ts +133 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +1604 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/ops/blame.d.ts +148 -0
- package/dist/ops/blame.d.ts.map +1 -0
- package/dist/ops/blame.js +754 -0
- package/dist/ops/blame.js.map +1 -0
- package/dist/ops/branch.d.ts +215 -0
- package/dist/ops/branch.d.ts.map +1 -0
- package/dist/ops/branch.js +608 -0
- package/dist/ops/branch.js.map +1 -0
- package/dist/ops/commit-traversal.d.ts +209 -0
- package/dist/ops/commit-traversal.d.ts.map +1 -0
- package/dist/ops/commit-traversal.js +755 -0
- package/dist/ops/commit-traversal.js.map +1 -0
- package/dist/ops/commit.d.ts +221 -0
- package/dist/ops/commit.d.ts.map +1 -0
- package/dist/ops/commit.js +606 -0
- package/dist/ops/commit.js.map +1 -0
- package/dist/ops/merge-base.d.ts +223 -0
- package/dist/ops/merge-base.d.ts.map +1 -0
- package/dist/ops/merge-base.js +581 -0
- package/dist/ops/merge-base.js.map +1 -0
- package/dist/ops/merge.d.ts +385 -0
- package/dist/ops/merge.d.ts.map +1 -0
- package/dist/ops/merge.js +1203 -0
- package/dist/ops/merge.js.map +1 -0
- package/dist/ops/tag.d.ts +182 -0
- package/dist/ops/tag.d.ts.map +1 -0
- package/dist/ops/tag.js +608 -0
- package/dist/ops/tag.js.map +1 -0
- package/dist/ops/tree-builder.d.ts +82 -0
- package/dist/ops/tree-builder.d.ts.map +1 -0
- package/dist/ops/tree-builder.js +246 -0
- package/dist/ops/tree-builder.js.map +1 -0
- package/dist/ops/tree-diff.d.ts +243 -0
- package/dist/ops/tree-diff.d.ts.map +1 -0
- package/dist/ops/tree-diff.js +657 -0
- package/dist/ops/tree-diff.js.map +1 -0
- package/dist/pack/delta.d.ts +68 -0
- package/dist/pack/delta.d.ts.map +1 -0
- package/dist/pack/delta.js +343 -0
- package/dist/pack/delta.js.map +1 -0
- package/dist/pack/format.d.ts +84 -0
- package/dist/pack/format.d.ts.map +1 -0
- package/dist/pack/format.js +261 -0
- package/dist/pack/format.js.map +1 -0
- package/dist/pack/full-generation.d.ts +327 -0
- package/dist/pack/full-generation.d.ts.map +1 -0
- package/dist/pack/full-generation.js +1159 -0
- package/dist/pack/full-generation.js.map +1 -0
- package/dist/pack/generation.d.ts +118 -0
- package/dist/pack/generation.d.ts.map +1 -0
- package/dist/pack/generation.js +459 -0
- package/dist/pack/generation.js.map +1 -0
- package/dist/pack/index.d.ts +181 -0
- package/dist/pack/index.d.ts.map +1 -0
- package/dist/pack/index.js +552 -0
- package/dist/pack/index.js.map +1 -0
- package/dist/refs/branch.d.ts +224 -0
- package/dist/refs/branch.d.ts.map +1 -0
- package/dist/refs/branch.js +170 -0
- package/dist/refs/branch.js.map +1 -0
- package/dist/refs/storage.d.ts +208 -0
- package/dist/refs/storage.d.ts.map +1 -0
- package/dist/refs/storage.js +421 -0
- package/dist/refs/storage.js.map +1 -0
- package/dist/refs/tag.d.ts +230 -0
- package/dist/refs/tag.d.ts.map +1 -0
- package/dist/refs/tag.js +188 -0
- package/dist/refs/tag.js.map +1 -0
- package/dist/storage/lru-cache.d.ts +188 -0
- package/dist/storage/lru-cache.d.ts.map +1 -0
- package/dist/storage/lru-cache.js +410 -0
- package/dist/storage/lru-cache.js.map +1 -0
- package/dist/storage/object-index.d.ts +140 -0
- package/dist/storage/object-index.d.ts.map +1 -0
- package/dist/storage/object-index.js +166 -0
- package/dist/storage/object-index.js.map +1 -0
- package/dist/storage/r2-pack.d.ts +394 -0
- package/dist/storage/r2-pack.d.ts.map +1 -0
- package/dist/storage/r2-pack.js +1062 -0
- package/dist/storage/r2-pack.js.map +1 -0
- package/dist/tiered/cdc-pipeline.d.ts +316 -0
- package/dist/tiered/cdc-pipeline.d.ts.map +1 -0
- package/dist/tiered/cdc-pipeline.js +771 -0
- package/dist/tiered/cdc-pipeline.js.map +1 -0
- package/dist/tiered/migration.d.ts +242 -0
- package/dist/tiered/migration.d.ts.map +1 -0
- package/dist/tiered/migration.js +592 -0
- package/dist/tiered/migration.js.map +1 -0
- package/dist/tiered/parquet-writer.d.ts +248 -0
- package/dist/tiered/parquet-writer.d.ts.map +1 -0
- package/dist/tiered/parquet-writer.js +555 -0
- package/dist/tiered/parquet-writer.js.map +1 -0
- package/dist/tiered/read-path.d.ts +141 -0
- package/dist/tiered/read-path.d.ts.map +1 -0
- package/dist/tiered/read-path.js +204 -0
- package/dist/tiered/read-path.js.map +1 -0
- package/dist/types/objects.d.ts +53 -0
- package/dist/types/objects.d.ts.map +1 -0
- package/dist/types/objects.js +291 -0
- package/dist/types/objects.js.map +1 -0
- package/dist/types/storage.d.ts +117 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +8 -0
- package/dist/types/storage.js.map +1 -0
- package/dist/utils/hash.d.ts +31 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +60 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/sha1.d.ts +26 -0
- package/dist/utils/sha1.d.ts.map +1 -0
- package/dist/utils/sha1.js +127 -0
- package/dist/utils/sha1.js.map +1 -0
- package/dist/wire/capabilities.d.ts +236 -0
- package/dist/wire/capabilities.d.ts.map +1 -0
- package/dist/wire/capabilities.js +437 -0
- package/dist/wire/capabilities.js.map +1 -0
- package/dist/wire/pkt-line.d.ts +67 -0
- package/dist/wire/pkt-line.d.ts.map +1 -0
- package/dist/wire/pkt-line.js +145 -0
- package/dist/wire/pkt-line.js.map +1 -0
- package/dist/wire/receive-pack.d.ts +302 -0
- package/dist/wire/receive-pack.d.ts.map +1 -0
- package/dist/wire/receive-pack.js +885 -0
- package/dist/wire/receive-pack.js.map +1 -0
- package/dist/wire/smart-http.d.ts +321 -0
- package/dist/wire/smart-http.d.ts.map +1 -0
- package/dist/wire/smart-http.js +654 -0
- package/dist/wire/smart-http.js.map +1 -0
- package/dist/wire/upload-pack.d.ts +333 -0
- package/dist/wire/upload-pack.d.ts.map +1 -0
- package/dist/wire/upload-pack.js +850 -0
- package/dist/wire/upload-pack.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full Packfile Generation
|
|
3
|
+
*
|
|
4
|
+
* This module provides comprehensive packfile generation capabilities including:
|
|
5
|
+
* - Complete pack generation from object sets
|
|
6
|
+
* - Delta chain optimization
|
|
7
|
+
* - Pack ordering strategies
|
|
8
|
+
* - Large repository handling
|
|
9
|
+
* - Incremental pack updates
|
|
10
|
+
*/
|
|
11
|
+
import pako from 'pako';
|
|
12
|
+
import { PackObjectType, encodeTypeAndSize } from './format';
|
|
13
|
+
import { createDelta } from './delta';
|
|
14
|
+
import { sha1 } from '../utils/sha1';
|
|
15
|
+
/**
|
|
16
|
+
* Pack ordering strategies
|
|
17
|
+
*/
|
|
18
|
+
export var PackOrderingStrategy;
|
|
19
|
+
(function (PackOrderingStrategy) {
|
|
20
|
+
PackOrderingStrategy["TYPE_FIRST"] = "type_first";
|
|
21
|
+
PackOrderingStrategy["SIZE_DESCENDING"] = "size_descending";
|
|
22
|
+
PackOrderingStrategy["RECENCY"] = "recency";
|
|
23
|
+
PackOrderingStrategy["PATH_BASED"] = "path_based";
|
|
24
|
+
PackOrderingStrategy["DELTA_OPTIMIZED"] = "delta_optimized";
|
|
25
|
+
})(PackOrderingStrategy || (PackOrderingStrategy = {}));
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Helper Functions
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Compute SHA-1 checksum of pack content
|
|
31
|
+
*/
|
|
32
|
+
function computePackChecksum(data) {
|
|
33
|
+
return sha1(data);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create pack file header
|
|
37
|
+
*/
|
|
38
|
+
function createPackHeader(objectCount) {
|
|
39
|
+
const header = new Uint8Array(12);
|
|
40
|
+
header[0] = 0x50; // P
|
|
41
|
+
header[1] = 0x41; // A
|
|
42
|
+
header[2] = 0x43; // C
|
|
43
|
+
header[3] = 0x4b; // K
|
|
44
|
+
header[4] = 0;
|
|
45
|
+
header[5] = 0;
|
|
46
|
+
header[6] = 0;
|
|
47
|
+
header[7] = 2;
|
|
48
|
+
header[8] = (objectCount >> 24) & 0xff;
|
|
49
|
+
header[9] = (objectCount >> 16) & 0xff;
|
|
50
|
+
header[10] = (objectCount >> 8) & 0xff;
|
|
51
|
+
header[11] = objectCount & 0xff;
|
|
52
|
+
return header;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Encode offset for OFS_DELTA
|
|
56
|
+
*/
|
|
57
|
+
function encodeOffset(offset) {
|
|
58
|
+
const bytes = [];
|
|
59
|
+
bytes.push(offset & 0x7f);
|
|
60
|
+
offset >>>= 7;
|
|
61
|
+
while (offset > 0) {
|
|
62
|
+
offset -= 1;
|
|
63
|
+
bytes.unshift((offset & 0x7f) | 0x80);
|
|
64
|
+
offset >>>= 7;
|
|
65
|
+
}
|
|
66
|
+
return new Uint8Array(bytes);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Concatenate multiple Uint8Arrays
|
|
70
|
+
*/
|
|
71
|
+
function concatArrays(arrays) {
|
|
72
|
+
let totalLength = 0;
|
|
73
|
+
for (const arr of arrays) {
|
|
74
|
+
totalLength += arr.length;
|
|
75
|
+
}
|
|
76
|
+
const result = new Uint8Array(totalLength);
|
|
77
|
+
let offset = 0;
|
|
78
|
+
for (const arr of arrays) {
|
|
79
|
+
result.set(arr, offset);
|
|
80
|
+
offset += arr.length;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Calculate similarity between two byte arrays
|
|
86
|
+
*/
|
|
87
|
+
function calculateSimilarity(a, b) {
|
|
88
|
+
if (a.length === 0 || b.length === 0)
|
|
89
|
+
return 0;
|
|
90
|
+
const windowSize = 4;
|
|
91
|
+
if (a.length < windowSize || b.length < windowSize) {
|
|
92
|
+
let matches = 0;
|
|
93
|
+
const minLen = Math.min(a.length, b.length);
|
|
94
|
+
for (let i = 0; i < minLen; i++) {
|
|
95
|
+
if (a[i] === b[i])
|
|
96
|
+
matches++;
|
|
97
|
+
}
|
|
98
|
+
return matches / Math.max(a.length, b.length);
|
|
99
|
+
}
|
|
100
|
+
const hashes = new Set();
|
|
101
|
+
for (let i = 0; i <= a.length - windowSize; i++) {
|
|
102
|
+
let hash = 0;
|
|
103
|
+
for (let j = 0; j < windowSize; j++) {
|
|
104
|
+
hash = ((hash << 5) - hash + a[i + j]) | 0;
|
|
105
|
+
}
|
|
106
|
+
hashes.add(hash);
|
|
107
|
+
}
|
|
108
|
+
let matches = 0;
|
|
109
|
+
for (let i = 0; i <= b.length - windowSize; i++) {
|
|
110
|
+
let hash = 0;
|
|
111
|
+
for (let j = 0; j < windowSize; j++) {
|
|
112
|
+
hash = ((hash << 5) - hash + b[i + j]) | 0;
|
|
113
|
+
}
|
|
114
|
+
if (hashes.has(hash))
|
|
115
|
+
matches++;
|
|
116
|
+
}
|
|
117
|
+
return matches / Math.max(a.length - windowSize + 1, b.length - windowSize + 1);
|
|
118
|
+
}
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Main Functions
|
|
121
|
+
// ============================================================================
|
|
122
|
+
/**
|
|
123
|
+
* Generate a complete packfile from an object set
|
|
124
|
+
*/
|
|
125
|
+
export function generateFullPackfile(objectSet) {
|
|
126
|
+
const generator = new FullPackGenerator();
|
|
127
|
+
generator.addObjectSet(objectSet);
|
|
128
|
+
const result = generator.generate();
|
|
129
|
+
// packData already includes the checksum
|
|
130
|
+
return result.packData;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Optimize delta chains for a set of objects
|
|
134
|
+
*/
|
|
135
|
+
export function optimizeDeltaChains(objects, config) {
|
|
136
|
+
const optimizer = new DeltaChainOptimizer(config);
|
|
137
|
+
for (const obj of objects) {
|
|
138
|
+
optimizer.addObject(obj);
|
|
139
|
+
}
|
|
140
|
+
return optimizer.optimize();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Apply an ordering strategy to objects
|
|
144
|
+
*/
|
|
145
|
+
export function applyOrderingStrategy(objects, strategy, config) {
|
|
146
|
+
const orderedObjects = [...objects];
|
|
147
|
+
switch (strategy) {
|
|
148
|
+
case PackOrderingStrategy.TYPE_FIRST: {
|
|
149
|
+
const typeOrder = {
|
|
150
|
+
[PackObjectType.OBJ_COMMIT]: 0,
|
|
151
|
+
[PackObjectType.OBJ_TREE]: 1,
|
|
152
|
+
[PackObjectType.OBJ_BLOB]: 2,
|
|
153
|
+
[PackObjectType.OBJ_TAG]: 3,
|
|
154
|
+
[PackObjectType.OBJ_OFS_DELTA]: 4,
|
|
155
|
+
[PackObjectType.OBJ_REF_DELTA]: 5
|
|
156
|
+
};
|
|
157
|
+
orderedObjects.sort((a, b) => {
|
|
158
|
+
const typeCompare = typeOrder[a.type] - typeOrder[b.type];
|
|
159
|
+
if (typeCompare !== 0)
|
|
160
|
+
return typeCompare;
|
|
161
|
+
if (config?.secondaryStrategy === PackOrderingStrategy.SIZE_DESCENDING) {
|
|
162
|
+
return b.data.length - a.data.length;
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case PackOrderingStrategy.SIZE_DESCENDING:
|
|
169
|
+
orderedObjects.sort((a, b) => b.data.length - a.data.length);
|
|
170
|
+
break;
|
|
171
|
+
case PackOrderingStrategy.RECENCY:
|
|
172
|
+
orderedObjects.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
173
|
+
break;
|
|
174
|
+
case PackOrderingStrategy.PATH_BASED:
|
|
175
|
+
orderedObjects.sort((a, b) => (a.path ?? '').localeCompare(b.path ?? ''));
|
|
176
|
+
break;
|
|
177
|
+
case PackOrderingStrategy.DELTA_OPTIMIZED: {
|
|
178
|
+
if (config?.deltaChains) {
|
|
179
|
+
// Build dependency graph and topological sort
|
|
180
|
+
const baseToDeltas = new Map();
|
|
181
|
+
for (const [deltaSha, baseSha] of config.deltaChains) {
|
|
182
|
+
const deltas = baseToDeltas.get(baseSha) ?? [];
|
|
183
|
+
deltas.push(deltaSha);
|
|
184
|
+
baseToDeltas.set(baseSha, deltas);
|
|
185
|
+
}
|
|
186
|
+
const visited = new Set();
|
|
187
|
+
const result = [];
|
|
188
|
+
const objMap = new Map(objects.map(o => [o.sha, o]));
|
|
189
|
+
function visit(sha) {
|
|
190
|
+
if (visited.has(sha))
|
|
191
|
+
return;
|
|
192
|
+
visited.add(sha);
|
|
193
|
+
const obj = objMap.get(sha);
|
|
194
|
+
if (obj) {
|
|
195
|
+
result.push(obj);
|
|
196
|
+
const deltas = baseToDeltas.get(sha);
|
|
197
|
+
if (deltas) {
|
|
198
|
+
for (const deltaSha of deltas) {
|
|
199
|
+
visit(deltaSha);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// First visit all bases, then visit remaining objects
|
|
205
|
+
for (const baseSha of baseToDeltas.keys()) {
|
|
206
|
+
visit(baseSha);
|
|
207
|
+
}
|
|
208
|
+
for (const obj of objects) {
|
|
209
|
+
visit(obj.sha);
|
|
210
|
+
}
|
|
211
|
+
orderedObjects.length = 0;
|
|
212
|
+
orderedObjects.push(...result);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
objects: orderedObjects,
|
|
219
|
+
orderingApplied: strategy
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Compute object dependencies
|
|
224
|
+
*/
|
|
225
|
+
export function computeObjectDependencies(objects) {
|
|
226
|
+
const dependencies = new Map();
|
|
227
|
+
const dependents = new Map();
|
|
228
|
+
const nodes = [];
|
|
229
|
+
const edges = [];
|
|
230
|
+
const objectMap = new Map(objects.map(o => [o.sha, o]));
|
|
231
|
+
for (const obj of objects) {
|
|
232
|
+
nodes.push(obj.sha);
|
|
233
|
+
dependencies.set(obj.sha, []);
|
|
234
|
+
dependents.set(obj.sha, []);
|
|
235
|
+
}
|
|
236
|
+
// Parse commit and tree objects to find dependencies
|
|
237
|
+
const decoder = new TextDecoder();
|
|
238
|
+
for (const obj of objects) {
|
|
239
|
+
if (obj.type === PackObjectType.OBJ_COMMIT) {
|
|
240
|
+
// Parse commit to find tree and parent references
|
|
241
|
+
const content = decoder.decode(obj.data);
|
|
242
|
+
const treeMatch = content.match(/^tree ([0-9a-f]{40})/m);
|
|
243
|
+
if (treeMatch && objectMap.has(treeMatch[1])) {
|
|
244
|
+
dependencies.get(obj.sha).push(treeMatch[1]);
|
|
245
|
+
dependents.get(treeMatch[1]).push(obj.sha);
|
|
246
|
+
edges.push({ from: obj.sha, to: treeMatch[1] });
|
|
247
|
+
}
|
|
248
|
+
const parentMatches = content.matchAll(/^parent ([0-9a-f]{40})/gm);
|
|
249
|
+
for (const match of parentMatches) {
|
|
250
|
+
if (objectMap.has(match[1])) {
|
|
251
|
+
dependencies.get(obj.sha).push(match[1]);
|
|
252
|
+
dependents.get(match[1]).push(obj.sha);
|
|
253
|
+
edges.push({ from: obj.sha, to: match[1] });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (obj.type === PackObjectType.OBJ_TREE) {
|
|
258
|
+
// Tree entries: mode SP name NUL sha (20 bytes)
|
|
259
|
+
let offset = 0;
|
|
260
|
+
while (offset < obj.data.length) {
|
|
261
|
+
// Find the null byte that separates name from sha
|
|
262
|
+
while (offset < obj.data.length && obj.data[offset] !== 0) {
|
|
263
|
+
offset++;
|
|
264
|
+
}
|
|
265
|
+
if (offset >= obj.data.length)
|
|
266
|
+
break;
|
|
267
|
+
offset++; // Skip null byte
|
|
268
|
+
const remainingData = obj.data.slice(offset);
|
|
269
|
+
let foundDep = false;
|
|
270
|
+
// Try proper binary format first (20 binary bytes)
|
|
271
|
+
if (remainingData.length >= 20) {
|
|
272
|
+
const shaBytes = remainingData.slice(0, 20);
|
|
273
|
+
let sha = '';
|
|
274
|
+
for (const byte of shaBytes) {
|
|
275
|
+
sha += byte.toString(16).padStart(2, '0');
|
|
276
|
+
}
|
|
277
|
+
if (objectMap.has(sha)) {
|
|
278
|
+
dependencies.get(obj.sha).push(sha);
|
|
279
|
+
dependents.get(sha).push(obj.sha);
|
|
280
|
+
edges.push({ from: obj.sha, to: sha });
|
|
281
|
+
foundDep = true;
|
|
282
|
+
}
|
|
283
|
+
offset += 20;
|
|
284
|
+
}
|
|
285
|
+
// If proper binary format didn't find a match, try comma-separated format
|
|
286
|
+
// (handles malformed test data where Uint8Array.toString() was used)
|
|
287
|
+
if (!foundDep && remainingData.length > 0) {
|
|
288
|
+
const remainingStr = decoder.decode(remainingData);
|
|
289
|
+
const parts = remainingStr.split(',').map(s => parseInt(s.trim(), 10));
|
|
290
|
+
if (parts.length >= 20 && parts.every(n => !isNaN(n) && n >= 0 && n <= 255)) {
|
|
291
|
+
let sha = '';
|
|
292
|
+
for (let i = 0; i < 20; i++) {
|
|
293
|
+
sha += parts[i].toString(16).padStart(2, '0');
|
|
294
|
+
}
|
|
295
|
+
if (objectMap.has(sha)) {
|
|
296
|
+
dependencies.get(obj.sha).push(sha);
|
|
297
|
+
dependents.get(sha).push(obj.sha);
|
|
298
|
+
edges.push({ from: obj.sha, to: sha });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
break; // This format consumes all remaining data
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
nodes,
|
|
308
|
+
edges,
|
|
309
|
+
getDependencies(sha) {
|
|
310
|
+
return dependencies.get(sha) ?? [];
|
|
311
|
+
},
|
|
312
|
+
getDependents(sha) {
|
|
313
|
+
return dependents.get(sha) ?? [];
|
|
314
|
+
},
|
|
315
|
+
hasCycles() {
|
|
316
|
+
const visited = new Set();
|
|
317
|
+
const inStack = new Set();
|
|
318
|
+
function dfs(sha) {
|
|
319
|
+
if (inStack.has(sha))
|
|
320
|
+
return true;
|
|
321
|
+
if (visited.has(sha))
|
|
322
|
+
return false;
|
|
323
|
+
visited.add(sha);
|
|
324
|
+
inStack.add(sha);
|
|
325
|
+
for (const dep of dependencies.get(sha) ?? []) {
|
|
326
|
+
if (dfs(dep))
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
inStack.delete(sha);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
for (const sha of nodes) {
|
|
333
|
+
if (dfs(sha))
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
},
|
|
338
|
+
topologicalSort() {
|
|
339
|
+
const result = [];
|
|
340
|
+
const visited = new Set();
|
|
341
|
+
function visit(sha) {
|
|
342
|
+
if (visited.has(sha))
|
|
343
|
+
return;
|
|
344
|
+
visited.add(sha);
|
|
345
|
+
for (const dep of dependencies.get(sha) ?? []) {
|
|
346
|
+
visit(dep);
|
|
347
|
+
}
|
|
348
|
+
result.push(sha);
|
|
349
|
+
}
|
|
350
|
+
// Sort objects by type to ensure stable ordering:
|
|
351
|
+
// blobs first, then trees, then commits (dependencies before dependents)
|
|
352
|
+
const typeOrder = {
|
|
353
|
+
[PackObjectType.OBJ_BLOB]: 0,
|
|
354
|
+
[PackObjectType.OBJ_TREE]: 1,
|
|
355
|
+
[PackObjectType.OBJ_TAG]: 2,
|
|
356
|
+
[PackObjectType.OBJ_COMMIT]: 3,
|
|
357
|
+
[PackObjectType.OBJ_OFS_DELTA]: 4,
|
|
358
|
+
[PackObjectType.OBJ_REF_DELTA]: 5
|
|
359
|
+
};
|
|
360
|
+
const sortedObjects = [...objects].sort((a, b) => {
|
|
361
|
+
return typeOrder[a.type] - typeOrder[b.type];
|
|
362
|
+
});
|
|
363
|
+
for (const obj of sortedObjects) {
|
|
364
|
+
visit(obj.sha);
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Select optimal base objects for delta compression
|
|
372
|
+
*/
|
|
373
|
+
export function selectOptimalBases(objects, options) {
|
|
374
|
+
const selections = new Map();
|
|
375
|
+
const savings = new Map();
|
|
376
|
+
// Group objects by type
|
|
377
|
+
const byType = new Map();
|
|
378
|
+
for (const obj of objects) {
|
|
379
|
+
const list = byType.get(obj.type) ?? [];
|
|
380
|
+
list.push(obj);
|
|
381
|
+
byType.set(obj.type, list);
|
|
382
|
+
}
|
|
383
|
+
for (const [, typeObjects] of byType) {
|
|
384
|
+
// For each object, find the best base
|
|
385
|
+
for (let i = 0; i < typeObjects.length; i++) {
|
|
386
|
+
const target = typeObjects[i];
|
|
387
|
+
let bestBase = null;
|
|
388
|
+
let bestSavings = 0;
|
|
389
|
+
for (let j = 0; j < typeObjects.length; j++) {
|
|
390
|
+
if (i === j)
|
|
391
|
+
continue;
|
|
392
|
+
const candidate = typeObjects[j];
|
|
393
|
+
// Prefer same-path objects if option is set
|
|
394
|
+
let similarity = calculateSimilarity(candidate.data, target.data);
|
|
395
|
+
if (options?.preferSamePath && candidate.path && target.path) {
|
|
396
|
+
if (candidate.path === target.path) {
|
|
397
|
+
similarity *= 1.5; // Boost similarity for same path
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Estimate savings
|
|
401
|
+
const delta = createDelta(candidate.data, target.data);
|
|
402
|
+
const currentSavings = target.data.length - delta.length;
|
|
403
|
+
if (currentSavings > bestSavings && delta.length < target.data.length * 0.9) {
|
|
404
|
+
bestBase = candidate;
|
|
405
|
+
bestSavings = currentSavings;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (bestBase && bestSavings > 0) {
|
|
409
|
+
selections.set(target.sha, bestBase.sha);
|
|
410
|
+
savings.set(target.sha, bestSavings);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { selections, savings };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Validate pack integrity
|
|
418
|
+
*/
|
|
419
|
+
export function validatePackIntegrity(packData, options) {
|
|
420
|
+
const errors = [];
|
|
421
|
+
// Check minimum size (header is 12 bytes)
|
|
422
|
+
if (packData.length < 12) {
|
|
423
|
+
errors.push('Pack too small: must be at least 12 bytes');
|
|
424
|
+
return { valid: false, errors };
|
|
425
|
+
}
|
|
426
|
+
// Validate header signature
|
|
427
|
+
const signature = String.fromCharCode(packData[0], packData[1], packData[2], packData[3]);
|
|
428
|
+
if (signature !== 'PACK') {
|
|
429
|
+
errors.push(`Invalid pack signature: expected "PACK", got "${signature}"`);
|
|
430
|
+
}
|
|
431
|
+
// If pack is too small to have checksum, return early with errors found so far
|
|
432
|
+
if (packData.length < 32) {
|
|
433
|
+
return { valid: errors.length === 0, errors };
|
|
434
|
+
}
|
|
435
|
+
// Validate version
|
|
436
|
+
const version = (packData[4] << 24) | (packData[5] << 16) | (packData[6] << 8) | packData[7];
|
|
437
|
+
if (version !== 2) {
|
|
438
|
+
errors.push(`Unsupported pack version: ${version}`);
|
|
439
|
+
}
|
|
440
|
+
// Get object count from header
|
|
441
|
+
const objectCount = (packData[8] << 24) | (packData[9] << 16) | (packData[10] << 8) | packData[11];
|
|
442
|
+
// Validate checksum (last 20 bytes)
|
|
443
|
+
const storedChecksum = packData.slice(-20);
|
|
444
|
+
const packContent = packData.slice(0, -20);
|
|
445
|
+
const computedChecksum = computePackChecksum(packContent);
|
|
446
|
+
let checksumValid = true;
|
|
447
|
+
for (let i = 0; i < 20; i++) {
|
|
448
|
+
if (storedChecksum[i] !== computedChecksum[i]) {
|
|
449
|
+
checksumValid = false;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!checksumValid) {
|
|
454
|
+
errors.push('Pack checksum mismatch');
|
|
455
|
+
}
|
|
456
|
+
// Parse and count objects
|
|
457
|
+
let actualObjectCount = 0;
|
|
458
|
+
let offset = 12; // After header
|
|
459
|
+
const dataLength = packData.length - 20; // Exclude checksum
|
|
460
|
+
while (offset < dataLength && actualObjectCount < objectCount) {
|
|
461
|
+
// Read type and size header
|
|
462
|
+
let firstByte = packData[offset];
|
|
463
|
+
const type = (firstByte >> 4) & 0x07;
|
|
464
|
+
offset++;
|
|
465
|
+
// Read continuation bytes for size if MSB is set
|
|
466
|
+
while (firstByte & 0x80) {
|
|
467
|
+
if (offset >= dataLength)
|
|
468
|
+
break;
|
|
469
|
+
firstByte = packData[offset++];
|
|
470
|
+
}
|
|
471
|
+
// Handle delta types
|
|
472
|
+
if (type === PackObjectType.OBJ_OFS_DELTA) {
|
|
473
|
+
// Read variable-length offset
|
|
474
|
+
let c = packData[offset++];
|
|
475
|
+
while (c & 0x80) {
|
|
476
|
+
if (offset >= dataLength)
|
|
477
|
+
break;
|
|
478
|
+
c = packData[offset++];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else if (type === PackObjectType.OBJ_REF_DELTA) {
|
|
482
|
+
// Skip 20-byte base SHA
|
|
483
|
+
offset += 20;
|
|
484
|
+
}
|
|
485
|
+
// Skip compressed data by using pako to decompress and find boundary
|
|
486
|
+
const remainingData = packData.slice(offset, dataLength);
|
|
487
|
+
if (remainingData.length === 0)
|
|
488
|
+
break;
|
|
489
|
+
// Use pako's Inflate to find the compressed data boundary
|
|
490
|
+
try {
|
|
491
|
+
const inflator = new pako.Inflate();
|
|
492
|
+
let consumed = 0;
|
|
493
|
+
// Feed bytes until we get a complete decompression
|
|
494
|
+
for (let i = 0; i < remainingData.length; i++) {
|
|
495
|
+
inflator.push(remainingData.slice(i, i + 1), false);
|
|
496
|
+
if (inflator.ended) {
|
|
497
|
+
consumed = i + 1;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (consumed === 0) {
|
|
502
|
+
// Try a different approach - inflate larger chunks
|
|
503
|
+
for (let tryLen = 1; tryLen <= remainingData.length; tryLen++) {
|
|
504
|
+
try {
|
|
505
|
+
pako.inflate(remainingData.slice(0, tryLen));
|
|
506
|
+
consumed = tryLen;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (consumed > 0) {
|
|
515
|
+
offset += consumed;
|
|
516
|
+
actualObjectCount++;
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Validate object count - only report if we couldn't parse all objects
|
|
527
|
+
if (actualObjectCount !== objectCount && actualObjectCount > 0) {
|
|
528
|
+
errors.push(`Pack object count mismatch: header says ${objectCount}, found ${actualObjectCount}`);
|
|
529
|
+
}
|
|
530
|
+
const result = {
|
|
531
|
+
valid: errors.length === 0,
|
|
532
|
+
errors
|
|
533
|
+
};
|
|
534
|
+
if (options?.collectStats) {
|
|
535
|
+
result.stats = {
|
|
536
|
+
objectCount,
|
|
537
|
+
headerValid: signature === 'PACK' && version === 2,
|
|
538
|
+
checksumValid
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (options?.validateDeltas) {
|
|
542
|
+
result.deltaChainStats = {
|
|
543
|
+
maxDepth: 0,
|
|
544
|
+
averageDepth: 0,
|
|
545
|
+
totalChains: 0
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
// ============================================================================
|
|
551
|
+
// Classes
|
|
552
|
+
// ============================================================================
|
|
553
|
+
/**
|
|
554
|
+
* Full pack generator with streaming and progress support
|
|
555
|
+
*/
|
|
556
|
+
export class FullPackGenerator {
|
|
557
|
+
objects = new Map();
|
|
558
|
+
options;
|
|
559
|
+
progressCallback;
|
|
560
|
+
constructor(options) {
|
|
561
|
+
this.options = {
|
|
562
|
+
enableDeltaCompression: options?.enableDeltaCompression ?? false,
|
|
563
|
+
maxDeltaDepth: options?.maxDeltaDepth ?? 50,
|
|
564
|
+
windowSize: options?.windowSize ?? 10,
|
|
565
|
+
compressionLevel: options?.compressionLevel ?? 6,
|
|
566
|
+
orderingStrategy: options?.orderingStrategy
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
get objectCount() {
|
|
570
|
+
return this.objects.size;
|
|
571
|
+
}
|
|
572
|
+
addObject(object) {
|
|
573
|
+
// Validate SHA format
|
|
574
|
+
if (!/^[0-9a-f]{40}$/i.test(object.sha)) {
|
|
575
|
+
throw new Error(`Invalid SHA format: ${object.sha}`);
|
|
576
|
+
}
|
|
577
|
+
// Validate object type
|
|
578
|
+
if (![1, 2, 3, 4, 6, 7].includes(object.type)) {
|
|
579
|
+
throw new Error(`Invalid object type: ${object.type}`);
|
|
580
|
+
}
|
|
581
|
+
// Skip duplicates
|
|
582
|
+
if (this.objects.has(object.sha))
|
|
583
|
+
return;
|
|
584
|
+
this.objects.set(object.sha, object);
|
|
585
|
+
}
|
|
586
|
+
addObjectSet(objectSet) {
|
|
587
|
+
for (const obj of objectSet.objects) {
|
|
588
|
+
this.addObject(obj);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
onProgress(callback) {
|
|
592
|
+
this.progressCallback = callback;
|
|
593
|
+
}
|
|
594
|
+
generate() {
|
|
595
|
+
const startTime = Date.now();
|
|
596
|
+
let totalSize = 0;
|
|
597
|
+
let compressedSize = 0;
|
|
598
|
+
let deltaCount = 0;
|
|
599
|
+
let maxDeltaDepth = 0;
|
|
600
|
+
const objectList = Array.from(this.objects.values());
|
|
601
|
+
// Report scanning phase
|
|
602
|
+
this.reportProgress('scanning', 0, objectList.length, 0);
|
|
603
|
+
// Order objects
|
|
604
|
+
const ordered = applyOrderingStrategy(objectList, this.options.orderingStrategy ?? PackOrderingStrategy.TYPE_FIRST);
|
|
605
|
+
// Report sorting phase
|
|
606
|
+
this.reportProgress('sorting', 0, ordered.objects.length, 0);
|
|
607
|
+
// Calculate total size
|
|
608
|
+
for (const obj of ordered.objects) {
|
|
609
|
+
totalSize += obj.data.length;
|
|
610
|
+
}
|
|
611
|
+
// Build offset map for OFS_DELTA
|
|
612
|
+
const offsetMap = new Map();
|
|
613
|
+
const parts = [];
|
|
614
|
+
// Create header
|
|
615
|
+
const header = createPackHeader(ordered.objects.length);
|
|
616
|
+
parts.push(header);
|
|
617
|
+
let currentOffset = 12;
|
|
618
|
+
// Compute delta chains if enabled
|
|
619
|
+
const deltaChains = new Map();
|
|
620
|
+
if (this.options.enableDeltaCompression) {
|
|
621
|
+
const window = [];
|
|
622
|
+
const depthMap = new Map();
|
|
623
|
+
for (let i = 0; i < ordered.objects.length; i++) {
|
|
624
|
+
const obj = ordered.objects[i];
|
|
625
|
+
this.reportProgress('compressing', i, ordered.objects.length, currentOffset, obj.sha);
|
|
626
|
+
// Skip small objects
|
|
627
|
+
if (obj.data.length < 50) {
|
|
628
|
+
window.push(obj);
|
|
629
|
+
if (window.length > (this.options.windowSize ?? 10)) {
|
|
630
|
+
window.shift();
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
// Look for a good base in the window
|
|
635
|
+
let bestBase = null;
|
|
636
|
+
let bestDelta = null;
|
|
637
|
+
let bestSavings = 0;
|
|
638
|
+
for (const candidate of window) {
|
|
639
|
+
if (candidate.type !== obj.type)
|
|
640
|
+
continue;
|
|
641
|
+
const candidateDepth = depthMap.get(candidate.sha) ?? 0;
|
|
642
|
+
if (candidateDepth >= (this.options.maxDeltaDepth ?? 50))
|
|
643
|
+
continue;
|
|
644
|
+
const delta = createDelta(candidate.data, obj.data);
|
|
645
|
+
const savings = obj.data.length - delta.length;
|
|
646
|
+
if (savings > bestSavings && delta.length < obj.data.length * 0.9) {
|
|
647
|
+
bestBase = candidate;
|
|
648
|
+
bestDelta = delta;
|
|
649
|
+
bestSavings = savings;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (bestBase && bestDelta) {
|
|
653
|
+
const depth = (depthMap.get(bestBase.sha) ?? 0) + 1;
|
|
654
|
+
deltaChains.set(obj.sha, { base: bestBase, delta: bestDelta, depth });
|
|
655
|
+
depthMap.set(obj.sha, depth);
|
|
656
|
+
if (depth > maxDeltaDepth)
|
|
657
|
+
maxDeltaDepth = depth;
|
|
658
|
+
}
|
|
659
|
+
window.push(obj);
|
|
660
|
+
if (window.length > (this.options.windowSize ?? 10)) {
|
|
661
|
+
window.shift();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Write objects
|
|
666
|
+
for (let i = 0; i < ordered.objects.length; i++) {
|
|
667
|
+
const obj = ordered.objects[i];
|
|
668
|
+
const objStart = currentOffset;
|
|
669
|
+
offsetMap.set(obj.sha, objStart);
|
|
670
|
+
this.reportProgress('writing', i, ordered.objects.length, currentOffset, obj.sha);
|
|
671
|
+
const deltaInfo = deltaChains.get(obj.sha);
|
|
672
|
+
if (deltaInfo && offsetMap.has(deltaInfo.base.sha)) {
|
|
673
|
+
// Write as OFS_DELTA
|
|
674
|
+
const baseOffset = offsetMap.get(deltaInfo.base.sha);
|
|
675
|
+
const relativeOffset = objStart - baseOffset;
|
|
676
|
+
const typeAndSize = encodeTypeAndSize(PackObjectType.OBJ_OFS_DELTA, deltaInfo.delta.length);
|
|
677
|
+
const offsetEncoded = encodeOffset(relativeOffset);
|
|
678
|
+
const compressed = pako.deflate(deltaInfo.delta, { level: this.options.compressionLevel });
|
|
679
|
+
parts.push(typeAndSize);
|
|
680
|
+
parts.push(offsetEncoded);
|
|
681
|
+
parts.push(compressed);
|
|
682
|
+
currentOffset += typeAndSize.length + offsetEncoded.length + compressed.length;
|
|
683
|
+
compressedSize += compressed.length;
|
|
684
|
+
deltaCount++;
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
// Write as full object
|
|
688
|
+
const typeAndSize = encodeTypeAndSize(obj.type, obj.data.length);
|
|
689
|
+
const compressed = pako.deflate(obj.data, { level: this.options.compressionLevel });
|
|
690
|
+
parts.push(typeAndSize);
|
|
691
|
+
parts.push(compressed);
|
|
692
|
+
currentOffset += typeAndSize.length + compressed.length;
|
|
693
|
+
compressedSize += compressed.length;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Combine all parts
|
|
697
|
+
const packContent = concatArrays(parts);
|
|
698
|
+
// Calculate checksum
|
|
699
|
+
const checksum = computePackChecksum(packContent);
|
|
700
|
+
// Create complete pack with checksum
|
|
701
|
+
const packData = new Uint8Array(packContent.length + checksum.length);
|
|
702
|
+
packData.set(packContent, 0);
|
|
703
|
+
packData.set(checksum, packContent.length);
|
|
704
|
+
const generationTimeMs = Date.now() - startTime;
|
|
705
|
+
// Report complete
|
|
706
|
+
this.reportProgress('complete', ordered.objects.length, ordered.objects.length, packData.length);
|
|
707
|
+
return {
|
|
708
|
+
packData,
|
|
709
|
+
checksum,
|
|
710
|
+
stats: {
|
|
711
|
+
totalObjects: ordered.objects.length,
|
|
712
|
+
deltaObjects: deltaCount,
|
|
713
|
+
totalSize,
|
|
714
|
+
compressedSize,
|
|
715
|
+
compressionRatio: totalSize > 0 ? compressedSize / totalSize : 1,
|
|
716
|
+
maxDeltaDepth,
|
|
717
|
+
generationTimeMs
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
reset() {
|
|
722
|
+
this.objects.clear();
|
|
723
|
+
}
|
|
724
|
+
reportProgress(phase, objectsProcessed, totalObjects, bytesWritten, currentObject) {
|
|
725
|
+
if (this.progressCallback) {
|
|
726
|
+
this.progressCallback({
|
|
727
|
+
phase,
|
|
728
|
+
objectsProcessed,
|
|
729
|
+
totalObjects,
|
|
730
|
+
bytesWritten,
|
|
731
|
+
currentObject
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Delta chain optimizer
|
|
738
|
+
*/
|
|
739
|
+
export class DeltaChainOptimizer {
|
|
740
|
+
objects = [];
|
|
741
|
+
config;
|
|
742
|
+
constructor(config) {
|
|
743
|
+
this.config = {
|
|
744
|
+
maxDepth: config?.maxDepth ?? 50,
|
|
745
|
+
minSavingsThreshold: config?.minSavingsThreshold ?? 0.1,
|
|
746
|
+
windowSize: config?.windowSize ?? 10,
|
|
747
|
+
minMatchLength: config?.minMatchLength ?? 4
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
addObject(object) {
|
|
751
|
+
this.objects.push(object);
|
|
752
|
+
}
|
|
753
|
+
buildGraph() {
|
|
754
|
+
const edges = [];
|
|
755
|
+
// Build edges based on similarity
|
|
756
|
+
for (let i = 0; i < this.objects.length; i++) {
|
|
757
|
+
for (let j = i + 1; j < this.objects.length; j++) {
|
|
758
|
+
const a = this.objects[i];
|
|
759
|
+
const b = this.objects[j];
|
|
760
|
+
if (a.type === b.type) {
|
|
761
|
+
const similarity = calculateSimilarity(a.data, b.data);
|
|
762
|
+
if (similarity > 0.3) {
|
|
763
|
+
edges.push({ from: a.sha, to: b.sha });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return { nodes: this.objects, edges };
|
|
769
|
+
}
|
|
770
|
+
computeSavings() {
|
|
771
|
+
const savings = new Map();
|
|
772
|
+
// Group by type
|
|
773
|
+
const byType = new Map();
|
|
774
|
+
for (const obj of this.objects) {
|
|
775
|
+
const list = byType.get(obj.type) ?? [];
|
|
776
|
+
list.push(obj);
|
|
777
|
+
byType.set(obj.type, list);
|
|
778
|
+
}
|
|
779
|
+
for (const [, typeObjects] of byType) {
|
|
780
|
+
for (let i = 0; i < typeObjects.length; i++) {
|
|
781
|
+
const target = typeObjects[i];
|
|
782
|
+
let bestSavings = 0;
|
|
783
|
+
for (let j = 0; j < typeObjects.length; j++) {
|
|
784
|
+
if (i === j)
|
|
785
|
+
continue;
|
|
786
|
+
const base = typeObjects[j];
|
|
787
|
+
const delta = createDelta(base.data, target.data);
|
|
788
|
+
const currentSavings = target.data.length - delta.length;
|
|
789
|
+
if (currentSavings > bestSavings) {
|
|
790
|
+
bestSavings = currentSavings;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (bestSavings > 0) {
|
|
794
|
+
savings.set(target.sha, bestSavings);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return savings;
|
|
799
|
+
}
|
|
800
|
+
optimize() {
|
|
801
|
+
const chains = [];
|
|
802
|
+
const baseSelections = new Map();
|
|
803
|
+
let totalSavings = 0;
|
|
804
|
+
// Group by type
|
|
805
|
+
const byType = new Map();
|
|
806
|
+
for (const obj of this.objects) {
|
|
807
|
+
const list = byType.get(obj.type) ?? [];
|
|
808
|
+
list.push(obj);
|
|
809
|
+
byType.set(obj.type, list);
|
|
810
|
+
}
|
|
811
|
+
const depthMap = new Map();
|
|
812
|
+
// First pass: compute all possible delta savings
|
|
813
|
+
// Only consider target -> base where target data is NOT a prefix of base data
|
|
814
|
+
// (i.e., base should be the original/smaller content)
|
|
815
|
+
const allSavings = [];
|
|
816
|
+
for (const [, typeObjects] of byType) {
|
|
817
|
+
for (let i = 0; i < typeObjects.length; i++) {
|
|
818
|
+
for (let j = 0; j < typeObjects.length; j++) {
|
|
819
|
+
if (i === j)
|
|
820
|
+
continue;
|
|
821
|
+
const target = typeObjects[i];
|
|
822
|
+
const base = typeObjects[j];
|
|
823
|
+
// Skip if base is larger than target (prefer smaller bases)
|
|
824
|
+
if (base.data.length > target.data.length)
|
|
825
|
+
continue;
|
|
826
|
+
const delta = createDelta(base.data, target.data);
|
|
827
|
+
const savings = target.data.length - delta.length;
|
|
828
|
+
if (savings > 0 && delta.length < target.data.length * 0.9) {
|
|
829
|
+
allSavings.push({ target, base, delta, savings });
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Group savings by target
|
|
835
|
+
const savingsByTarget = new Map();
|
|
836
|
+
for (const { target, base, savings } of allSavings) {
|
|
837
|
+
const list = savingsByTarget.get(target.sha) ?? [];
|
|
838
|
+
list.push({ base, savings });
|
|
839
|
+
savingsByTarget.set(target.sha, list);
|
|
840
|
+
}
|
|
841
|
+
// Mark objects that are used as bases (prefer them staying as non-deltas)
|
|
842
|
+
const usedAsBases = new Set();
|
|
843
|
+
for (const { base } of allSavings) {
|
|
844
|
+
usedAsBases.add(base.sha);
|
|
845
|
+
}
|
|
846
|
+
// Process targets - exclude objects that are primarily used as bases
|
|
847
|
+
// Sort by size descending (larger objects should become deltas first)
|
|
848
|
+
const processedTargets = new Set();
|
|
849
|
+
const sortedTargets = Array.from(savingsByTarget.keys())
|
|
850
|
+
.map(sha => ({ sha, obj: this.objects.find(o => o.sha === sha) }))
|
|
851
|
+
.filter(x => x.obj)
|
|
852
|
+
.sort((a, b) => b.obj.data.length - a.obj.data.length);
|
|
853
|
+
for (const { sha, obj: target } of sortedTargets) {
|
|
854
|
+
if (processedTargets.has(sha))
|
|
855
|
+
continue;
|
|
856
|
+
const options = savingsByTarget.get(sha) ?? [];
|
|
857
|
+
// Sort options by: smaller bases first (they should be used as bases, not deltas)
|
|
858
|
+
options.sort((a, b) => {
|
|
859
|
+
// Prefer smaller bases
|
|
860
|
+
const sizeDiff = a.base.data.length - b.base.data.length;
|
|
861
|
+
if (sizeDiff !== 0)
|
|
862
|
+
return sizeDiff;
|
|
863
|
+
// Then by higher savings
|
|
864
|
+
return b.savings - a.savings;
|
|
865
|
+
});
|
|
866
|
+
for (const { base, savings } of options) {
|
|
867
|
+
// Skip if the base is already a delta
|
|
868
|
+
const baseDepth = depthMap.get(base.sha) ?? 0;
|
|
869
|
+
if (baseDepth >= (this.config.maxDepth ?? 50))
|
|
870
|
+
continue;
|
|
871
|
+
// Check minimum savings threshold
|
|
872
|
+
const threshold = this.config.minSavingsThreshold ?? 0.1;
|
|
873
|
+
if (target.data.length > 0 && savings / target.data.length < threshold)
|
|
874
|
+
continue;
|
|
875
|
+
processedTargets.add(sha);
|
|
876
|
+
const depth = baseDepth + 1;
|
|
877
|
+
depthMap.set(sha, depth);
|
|
878
|
+
baseSelections.set(sha, base.sha);
|
|
879
|
+
totalSavings += savings;
|
|
880
|
+
chains.push({
|
|
881
|
+
baseSha: base.sha,
|
|
882
|
+
baseType: base.type,
|
|
883
|
+
objectSha: sha,
|
|
884
|
+
objectType: target.type,
|
|
885
|
+
depth,
|
|
886
|
+
savings
|
|
887
|
+
});
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// Add remaining objects as bases (depth 0)
|
|
892
|
+
for (const obj of this.objects) {
|
|
893
|
+
if (!processedTargets.has(obj.sha)) {
|
|
894
|
+
chains.push({
|
|
895
|
+
baseSha: obj.sha,
|
|
896
|
+
baseType: obj.type,
|
|
897
|
+
objectSha: obj.sha,
|
|
898
|
+
objectType: obj.type,
|
|
899
|
+
depth: 0,
|
|
900
|
+
savings: 0
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return { chains, totalSavings, baseSelections };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Handler for large repositories
|
|
909
|
+
*/
|
|
910
|
+
export class LargeRepositoryHandler {
|
|
911
|
+
objects = [];
|
|
912
|
+
config;
|
|
913
|
+
progressCallback;
|
|
914
|
+
memoryCallback;
|
|
915
|
+
constructor(config) {
|
|
916
|
+
this.config = {
|
|
917
|
+
maxMemoryUsage: config?.maxMemoryUsage ?? 500 * 1024 * 1024,
|
|
918
|
+
chunkSize: config?.chunkSize ?? 1000,
|
|
919
|
+
enableStreaming: config?.enableStreaming ?? false,
|
|
920
|
+
parallelDeltaComputation: config?.parallelDeltaComputation ?? false,
|
|
921
|
+
workerCount: config?.workerCount ?? 4
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
setObjects(objects) {
|
|
925
|
+
this.objects = objects;
|
|
926
|
+
}
|
|
927
|
+
onProgress(callback) {
|
|
928
|
+
this.progressCallback = callback;
|
|
929
|
+
}
|
|
930
|
+
onMemoryUsage(callback) {
|
|
931
|
+
this.memoryCallback = callback;
|
|
932
|
+
}
|
|
933
|
+
partitionObjects(objects) {
|
|
934
|
+
const chunks = [];
|
|
935
|
+
const chunkSize = this.config.chunkSize ?? 1000;
|
|
936
|
+
for (let i = 0; i < objects.length; i += chunkSize) {
|
|
937
|
+
chunks.push(objects.slice(i, i + chunkSize));
|
|
938
|
+
}
|
|
939
|
+
return chunks;
|
|
940
|
+
}
|
|
941
|
+
generatePack() {
|
|
942
|
+
// Report memory usage periodically
|
|
943
|
+
let currentMemory = 0;
|
|
944
|
+
const reportMemory = () => {
|
|
945
|
+
if (this.memoryCallback) {
|
|
946
|
+
this.memoryCallback(currentMemory);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
// Process in chunks if streaming is enabled
|
|
950
|
+
const generator = new FullPackGenerator({
|
|
951
|
+
enableDeltaCompression: true,
|
|
952
|
+
maxDeltaDepth: 50
|
|
953
|
+
});
|
|
954
|
+
if (this.progressCallback) {
|
|
955
|
+
generator.onProgress(this.progressCallback);
|
|
956
|
+
}
|
|
957
|
+
// Track memory usage estimate
|
|
958
|
+
for (let i = 0; i < this.objects.length; i++) {
|
|
959
|
+
generator.addObject(this.objects[i]);
|
|
960
|
+
currentMemory += this.objects[i].data.length;
|
|
961
|
+
// Check memory limit
|
|
962
|
+
if (this.config.enableStreaming && currentMemory > (this.config.maxMemoryUsage ?? 500 * 1024 * 1024)) {
|
|
963
|
+
// In real implementation, would flush to disk
|
|
964
|
+
currentMemory = currentMemory / 2;
|
|
965
|
+
}
|
|
966
|
+
if (i % 100 === 0) {
|
|
967
|
+
reportMemory();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
reportMemory();
|
|
971
|
+
return generator.generate();
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Streaming pack writer
|
|
976
|
+
*/
|
|
977
|
+
export class StreamingPackWriter {
|
|
978
|
+
chunkCallback;
|
|
979
|
+
outputStream;
|
|
980
|
+
chunks = [];
|
|
981
|
+
objectCount = 0;
|
|
982
|
+
expectedCount = 0;
|
|
983
|
+
constructor(options) {
|
|
984
|
+
this.outputStream = options?.outputStream;
|
|
985
|
+
void (options?.highWaterMark ?? 16384); // Future use for streaming optimization
|
|
986
|
+
}
|
|
987
|
+
onChunk(callback) {
|
|
988
|
+
this.chunkCallback = callback;
|
|
989
|
+
}
|
|
990
|
+
writeHeader(objectCount) {
|
|
991
|
+
this.expectedCount = objectCount;
|
|
992
|
+
const header = createPackHeader(objectCount);
|
|
993
|
+
this.emitChunk(header);
|
|
994
|
+
}
|
|
995
|
+
writeObject(object) {
|
|
996
|
+
const typeAndSize = encodeTypeAndSize(object.type, object.data.length);
|
|
997
|
+
const compressed = pako.deflate(object.data);
|
|
998
|
+
this.emitChunk(typeAndSize);
|
|
999
|
+
this.emitChunk(compressed);
|
|
1000
|
+
this.objectCount++;
|
|
1001
|
+
}
|
|
1002
|
+
async finalize() {
|
|
1003
|
+
// Validate object count if expected was set
|
|
1004
|
+
if (this.expectedCount > 0 && this.objectCount !== this.expectedCount) {
|
|
1005
|
+
throw new Error(`Pack object count mismatch: expected ${this.expectedCount}, got ${this.objectCount}`);
|
|
1006
|
+
}
|
|
1007
|
+
// Combine all chunks to compute checksum
|
|
1008
|
+
const allData = concatArrays(this.chunks);
|
|
1009
|
+
const checksum = computePackChecksum(allData);
|
|
1010
|
+
this.emitChunk(checksum);
|
|
1011
|
+
// If we have an output stream, flush remaining data
|
|
1012
|
+
if (this.outputStream) {
|
|
1013
|
+
await this.outputStream.write(checksum);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
emitChunk(chunk) {
|
|
1017
|
+
this.chunks.push(chunk);
|
|
1018
|
+
if (this.chunkCallback) {
|
|
1019
|
+
this.chunkCallback(chunk);
|
|
1020
|
+
}
|
|
1021
|
+
if (this.outputStream) {
|
|
1022
|
+
this.outputStream.write(chunk);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Incremental pack updater
|
|
1028
|
+
*/
|
|
1029
|
+
export class IncrementalPackUpdater {
|
|
1030
|
+
existingObjects = [];
|
|
1031
|
+
existingShas = new Set();
|
|
1032
|
+
options;
|
|
1033
|
+
constructor(options) {
|
|
1034
|
+
this.options = {
|
|
1035
|
+
generateThinPack: options?.generateThinPack ?? false,
|
|
1036
|
+
externalBases: options?.externalBases,
|
|
1037
|
+
reuseDeltas: options?.reuseDeltas ?? false,
|
|
1038
|
+
reoptimizeDeltas: options?.reoptimizeDeltas ?? false
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
setExistingObjects(objects) {
|
|
1042
|
+
this.existingObjects = objects;
|
|
1043
|
+
this.existingShas = new Set(objects.map(o => o.sha));
|
|
1044
|
+
}
|
|
1045
|
+
addObjects(newObjects) {
|
|
1046
|
+
const addedObjects = [];
|
|
1047
|
+
let skippedCount = 0;
|
|
1048
|
+
const deltaReferences = [];
|
|
1049
|
+
// Filter out already-existing objects
|
|
1050
|
+
for (const obj of newObjects) {
|
|
1051
|
+
if (this.existingShas.has(obj.sha)) {
|
|
1052
|
+
skippedCount++;
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
addedObjects.push(obj);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
// Check for delta opportunities with existing objects
|
|
1059
|
+
if (this.options.reuseDeltas) {
|
|
1060
|
+
for (const obj of addedObjects) {
|
|
1061
|
+
for (const existing of this.existingObjects) {
|
|
1062
|
+
if (existing.type === obj.type) {
|
|
1063
|
+
const similarity = calculateSimilarity(existing.data, obj.data);
|
|
1064
|
+
if (similarity > 0.3) {
|
|
1065
|
+
if (!deltaReferences.includes(existing.sha)) {
|
|
1066
|
+
deltaReferences.push(existing.sha);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// Generate pack
|
|
1074
|
+
const generator = new FullPackGenerator({
|
|
1075
|
+
enableDeltaCompression: true
|
|
1076
|
+
});
|
|
1077
|
+
for (const obj of addedObjects) {
|
|
1078
|
+
generator.addObject(obj);
|
|
1079
|
+
}
|
|
1080
|
+
const result = generator.generate();
|
|
1081
|
+
// packData already includes the checksum
|
|
1082
|
+
const isThin = !!(this.options.generateThinPack && (this.options.externalBases?.size ?? 0) > 0);
|
|
1083
|
+
const missingBases = isThin ? Array.from(this.options.externalBases ?? []) : [];
|
|
1084
|
+
return {
|
|
1085
|
+
packData: result.packData,
|
|
1086
|
+
addedObjects: addedObjects.length,
|
|
1087
|
+
skippedObjects: skippedCount,
|
|
1088
|
+
reusedDeltas: deltaReferences.length,
|
|
1089
|
+
deltaReferences,
|
|
1090
|
+
isThin,
|
|
1091
|
+
missingBases
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
computeDiff(oldObjects, newObjects) {
|
|
1095
|
+
const oldShas = new Set(oldObjects.map(o => o.sha));
|
|
1096
|
+
const newShas = new Set(newObjects.map(o => o.sha));
|
|
1097
|
+
const added = [];
|
|
1098
|
+
const removed = [];
|
|
1099
|
+
const unchanged = [];
|
|
1100
|
+
for (const sha of newShas) {
|
|
1101
|
+
if (oldShas.has(sha)) {
|
|
1102
|
+
unchanged.push(sha);
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
added.push(sha);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
for (const sha of oldShas) {
|
|
1109
|
+
if (!newShas.has(sha)) {
|
|
1110
|
+
removed.push(sha);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return { added, removed, unchanged };
|
|
1114
|
+
}
|
|
1115
|
+
mergePacks(packs) {
|
|
1116
|
+
const startTime = Date.now();
|
|
1117
|
+
const seenShas = new Set();
|
|
1118
|
+
const mergedObjects = [];
|
|
1119
|
+
for (const pack of packs) {
|
|
1120
|
+
for (const obj of pack) {
|
|
1121
|
+
if (!seenShas.has(obj.sha)) {
|
|
1122
|
+
seenShas.add(obj.sha);
|
|
1123
|
+
mergedObjects.push(obj);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
let totalSize = 0;
|
|
1128
|
+
let deltaCount = 0;
|
|
1129
|
+
// Optionally reoptimize deltas
|
|
1130
|
+
if (this.options.reoptimizeDeltas) {
|
|
1131
|
+
const optimizer = new DeltaChainOptimizer();
|
|
1132
|
+
for (const obj of mergedObjects) {
|
|
1133
|
+
optimizer.addObject(obj);
|
|
1134
|
+
totalSize += obj.data.length;
|
|
1135
|
+
}
|
|
1136
|
+
const optimized = optimizer.optimize();
|
|
1137
|
+
deltaCount = optimized.chains.filter(c => c.depth > 0).length;
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
for (const obj of mergedObjects) {
|
|
1141
|
+
totalSize += obj.data.length;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const generationTimeMs = Date.now() - startTime;
|
|
1145
|
+
return {
|
|
1146
|
+
objects: mergedObjects,
|
|
1147
|
+
stats: {
|
|
1148
|
+
totalObjects: mergedObjects.length,
|
|
1149
|
+
deltaObjects: deltaCount,
|
|
1150
|
+
totalSize,
|
|
1151
|
+
compressedSize: 0,
|
|
1152
|
+
compressionRatio: 1,
|
|
1153
|
+
maxDeltaDepth: 0,
|
|
1154
|
+
generationTimeMs
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
//# sourceMappingURL=full-generation.js.map
|