@catmint-fs/git 0.0.0-prealpha.0
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 +339 -0
- package/README.md +209 -0
- package/dist/index.d.ts +615 -0
- package/dist/index.js +3871 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3871 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/utils.ts
|
|
12
|
+
async function sha1(data) {
|
|
13
|
+
if (typeof globalThis.crypto?.subtle?.digest === "function") {
|
|
14
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-1", data);
|
|
15
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
16
|
+
}
|
|
17
|
+
const { createHash } = await import("crypto");
|
|
18
|
+
return createHash("sha1").update(data).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
async function sha1Bytes(data) {
|
|
21
|
+
if (typeof globalThis.crypto?.subtle?.digest === "function") {
|
|
22
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-1", data);
|
|
23
|
+
return new Uint8Array(hashBuffer);
|
|
24
|
+
}
|
|
25
|
+
const { createHash } = await import("crypto");
|
|
26
|
+
const buf = createHash("sha1").update(data).digest();
|
|
27
|
+
return new Uint8Array(buf);
|
|
28
|
+
}
|
|
29
|
+
function hexToBytes(hex) {
|
|
30
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
31
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
32
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
33
|
+
}
|
|
34
|
+
return bytes;
|
|
35
|
+
}
|
|
36
|
+
function bytesToHex(bytes) {
|
|
37
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
38
|
+
}
|
|
39
|
+
function encode(str) {
|
|
40
|
+
return encoder.encode(str);
|
|
41
|
+
}
|
|
42
|
+
function decode(bytes) {
|
|
43
|
+
return decoder.decode(bytes);
|
|
44
|
+
}
|
|
45
|
+
function concat(...arrays) {
|
|
46
|
+
let totalLen = 0;
|
|
47
|
+
for (const a of arrays) totalLen += a.length;
|
|
48
|
+
const result = new Uint8Array(totalLen);
|
|
49
|
+
let offset = 0;
|
|
50
|
+
for (const a of arrays) {
|
|
51
|
+
result.set(a, offset);
|
|
52
|
+
offset += a.length;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function normalizePath(path) {
|
|
57
|
+
let p = path.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
58
|
+
p = p.replace(/\/+/g, "/");
|
|
59
|
+
return p;
|
|
60
|
+
}
|
|
61
|
+
var encoder, decoder;
|
|
62
|
+
var init_utils = __esm({
|
|
63
|
+
"src/utils.ts"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
encoder = new TextEncoder();
|
|
66
|
+
decoder = new TextDecoder();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// src/pack/read.ts
|
|
71
|
+
var read_exports = {};
|
|
72
|
+
__export(read_exports, {
|
|
73
|
+
PackReader: () => PackReader
|
|
74
|
+
});
|
|
75
|
+
import pako from "pako";
|
|
76
|
+
function readVarint(data, pos) {
|
|
77
|
+
let value = 0;
|
|
78
|
+
let shift = 0;
|
|
79
|
+
let byte;
|
|
80
|
+
do {
|
|
81
|
+
byte = data[pos++];
|
|
82
|
+
value |= (byte & 127) << shift;
|
|
83
|
+
shift += 7;
|
|
84
|
+
} while (byte & 128);
|
|
85
|
+
return [value, pos];
|
|
86
|
+
}
|
|
87
|
+
function applyDelta(base, delta) {
|
|
88
|
+
let pos = 0;
|
|
89
|
+
[, pos] = readVarint(delta, pos);
|
|
90
|
+
let targetSize;
|
|
91
|
+
[targetSize, pos] = readVarint(delta, pos);
|
|
92
|
+
const result = new Uint8Array(targetSize);
|
|
93
|
+
let resultPos = 0;
|
|
94
|
+
while (pos < delta.length) {
|
|
95
|
+
const cmd = delta[pos++];
|
|
96
|
+
if (cmd & 128) {
|
|
97
|
+
let copyOffset = 0;
|
|
98
|
+
let copySize = 0;
|
|
99
|
+
if (cmd & 1) copyOffset = delta[pos++];
|
|
100
|
+
if (cmd & 2) copyOffset |= delta[pos++] << 8;
|
|
101
|
+
if (cmd & 4) copyOffset |= delta[pos++] << 16;
|
|
102
|
+
if (cmd & 8) copyOffset |= delta[pos++] << 24;
|
|
103
|
+
if (cmd & 16) copySize = delta[pos++];
|
|
104
|
+
if (cmd & 32) copySize |= delta[pos++] << 8;
|
|
105
|
+
if (cmd & 64) copySize |= delta[pos++] << 16;
|
|
106
|
+
if (copySize === 0) copySize = 65536;
|
|
107
|
+
result.set(base.subarray(copyOffset, copyOffset + copySize), resultPos);
|
|
108
|
+
resultPos += copySize;
|
|
109
|
+
} else if (cmd > 0) {
|
|
110
|
+
result.set(delta.subarray(pos, pos + cmd), resultPos);
|
|
111
|
+
resultPos += cmd;
|
|
112
|
+
pos += cmd;
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error("Unexpected delta opcode 0");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
function inflateWithConsumed(data, pos) {
|
|
120
|
+
const inf = new pako.Inflate({ raw: true });
|
|
121
|
+
inf.push(data.subarray(pos + 2), true);
|
|
122
|
+
if (inf.err) {
|
|
123
|
+
throw new Error(`Inflate error: ${inf.msg}`);
|
|
124
|
+
}
|
|
125
|
+
const result = inf.result;
|
|
126
|
+
const strm = inf.strm;
|
|
127
|
+
const rawConsumed = strm?.total_in ?? data.length - pos - 6;
|
|
128
|
+
return [result, 2 + rawConsumed + 4];
|
|
129
|
+
}
|
|
130
|
+
function readUint32(data, offset) {
|
|
131
|
+
return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
|
|
132
|
+
}
|
|
133
|
+
var OBJ_COMMIT, OBJ_TREE, OBJ_BLOB, OBJ_TAG, OBJ_OFS_DELTA, OBJ_REF_DELTA, TYPE_MAP, PackReader;
|
|
134
|
+
var init_read = __esm({
|
|
135
|
+
"src/pack/read.ts"() {
|
|
136
|
+
"use strict";
|
|
137
|
+
init_utils();
|
|
138
|
+
OBJ_COMMIT = 1;
|
|
139
|
+
OBJ_TREE = 2;
|
|
140
|
+
OBJ_BLOB = 3;
|
|
141
|
+
OBJ_TAG = 4;
|
|
142
|
+
OBJ_OFS_DELTA = 6;
|
|
143
|
+
OBJ_REF_DELTA = 7;
|
|
144
|
+
TYPE_MAP = {
|
|
145
|
+
[OBJ_COMMIT]: "commit",
|
|
146
|
+
[OBJ_TREE]: "tree",
|
|
147
|
+
[OBJ_BLOB]: "blob",
|
|
148
|
+
[OBJ_TAG]: "tag"
|
|
149
|
+
};
|
|
150
|
+
PackReader = class {
|
|
151
|
+
constructor(layer, packPath, idxPath) {
|
|
152
|
+
this.layer = layer;
|
|
153
|
+
this.packPath = packPath;
|
|
154
|
+
this.idxPath = idxPath;
|
|
155
|
+
}
|
|
156
|
+
packData = null;
|
|
157
|
+
index = null;
|
|
158
|
+
async readObject(oid) {
|
|
159
|
+
await this.ensureLoaded();
|
|
160
|
+
if (!this.index) return null;
|
|
161
|
+
const offset = this.index.offsets.get(oid);
|
|
162
|
+
if (offset === void 0) return null;
|
|
163
|
+
return this.readObjectAtOffset(offset);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Extract all objects from the packfile without requiring an index.
|
|
167
|
+
* Parses the pack header to get the object count, then reads each object
|
|
168
|
+
* sequentially.
|
|
169
|
+
*/
|
|
170
|
+
async extractAll() {
|
|
171
|
+
if (!this.packData) {
|
|
172
|
+
this.packData = await this.layer.readFile(this.packPath);
|
|
173
|
+
}
|
|
174
|
+
const data = this.packData;
|
|
175
|
+
const objects = [];
|
|
176
|
+
if (data.length < 12) {
|
|
177
|
+
throw new Error("Pack data too short");
|
|
178
|
+
}
|
|
179
|
+
const version = readUint32(data, 4);
|
|
180
|
+
const objectCount = readUint32(data, 8);
|
|
181
|
+
let pos = 12;
|
|
182
|
+
for (let i = 0; i < objectCount; i++) {
|
|
183
|
+
const startPos = pos;
|
|
184
|
+
let byte = data[pos++];
|
|
185
|
+
const type = byte >> 4 & 7;
|
|
186
|
+
let size = byte & 15;
|
|
187
|
+
let shift = 4;
|
|
188
|
+
while (byte & 128) {
|
|
189
|
+
byte = data[pos++];
|
|
190
|
+
size |= (byte & 127) << shift;
|
|
191
|
+
shift += 7;
|
|
192
|
+
}
|
|
193
|
+
if (type === OBJ_OFS_DELTA) {
|
|
194
|
+
byte = data[pos++];
|
|
195
|
+
let negOffset = byte & 127;
|
|
196
|
+
while (byte & 128) {
|
|
197
|
+
byte = data[pos++];
|
|
198
|
+
negOffset = negOffset + 1 << 7 | byte & 127;
|
|
199
|
+
}
|
|
200
|
+
const baseOffset = startPos - negOffset;
|
|
201
|
+
const base = this.readObjectAtOffset(baseOffset);
|
|
202
|
+
const [deltaData, consumed] = inflateWithConsumed(data, pos);
|
|
203
|
+
pos += consumed;
|
|
204
|
+
const result = applyDelta(base.content, deltaData);
|
|
205
|
+
objects.push({ type: base.type, content: result });
|
|
206
|
+
} else if (type === OBJ_REF_DELTA) {
|
|
207
|
+
const baseSha = bytesToHex(data.subarray(pos, pos + 20));
|
|
208
|
+
pos += 20;
|
|
209
|
+
let baseObj;
|
|
210
|
+
if (this.index) {
|
|
211
|
+
const baseOffset = this.index.offsets.get(baseSha);
|
|
212
|
+
if (baseOffset !== void 0) {
|
|
213
|
+
baseObj = this.readObjectAtOffset(baseOffset);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!baseObj) {
|
|
217
|
+
throw new Error(`Base object not found in pack: ${baseSha}`);
|
|
218
|
+
}
|
|
219
|
+
const [deltaData, consumed] = inflateWithConsumed(data, pos);
|
|
220
|
+
pos += consumed;
|
|
221
|
+
const result = applyDelta(baseObj.content, deltaData);
|
|
222
|
+
objects.push({ type: baseObj.type, content: result });
|
|
223
|
+
} else {
|
|
224
|
+
const typeName = TYPE_MAP[type];
|
|
225
|
+
if (!typeName) {
|
|
226
|
+
throw new Error(`Unknown pack object type: ${type}`);
|
|
227
|
+
}
|
|
228
|
+
const [content, consumed] = inflateWithConsumed(data, pos);
|
|
229
|
+
pos += consumed;
|
|
230
|
+
objects.push({ type: typeName, content });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return objects;
|
|
234
|
+
}
|
|
235
|
+
async ensureLoaded() {
|
|
236
|
+
if (this.packData && (this.index || !this.idxPath)) return;
|
|
237
|
+
this.packData = await this.layer.readFile(this.packPath);
|
|
238
|
+
if (this.idxPath) {
|
|
239
|
+
const idxData = await this.layer.readFile(this.idxPath);
|
|
240
|
+
this.index = this.parseIndex(idxData);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
parseIndex(data) {
|
|
244
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
245
|
+
if (data[0] !== 255 || data[1] !== 116 || data[2] !== 79 || data[3] !== 99) {
|
|
246
|
+
return { offsets };
|
|
247
|
+
}
|
|
248
|
+
const version = readUint32(data, 4);
|
|
249
|
+
if (version !== 2) return { offsets };
|
|
250
|
+
const fanoutStart = 8;
|
|
251
|
+
const totalObjects = readUint32(data, fanoutStart + 255 * 4);
|
|
252
|
+
const shaStart = fanoutStart + 256 * 4;
|
|
253
|
+
const crcStart = shaStart + totalObjects * 20;
|
|
254
|
+
const offsetStart = crcStart + totalObjects * 4;
|
|
255
|
+
for (let i = 0; i < totalObjects; i++) {
|
|
256
|
+
const shaBytes = data.subarray(shaStart + i * 20, shaStart + (i + 1) * 20);
|
|
257
|
+
const oid = bytesToHex(shaBytes);
|
|
258
|
+
const offset = readUint32(data, offsetStart + i * 4);
|
|
259
|
+
offsets.set(oid, offset);
|
|
260
|
+
}
|
|
261
|
+
return { offsets };
|
|
262
|
+
}
|
|
263
|
+
readObjectAtOffset(offset) {
|
|
264
|
+
const data = this.packData;
|
|
265
|
+
let pos = offset;
|
|
266
|
+
let byte = data[pos++];
|
|
267
|
+
const type = byte >> 4 & 7;
|
|
268
|
+
let size = byte & 15;
|
|
269
|
+
let shift = 4;
|
|
270
|
+
while (byte & 128) {
|
|
271
|
+
byte = data[pos++];
|
|
272
|
+
size |= (byte & 127) << shift;
|
|
273
|
+
shift += 7;
|
|
274
|
+
}
|
|
275
|
+
if (type === OBJ_OFS_DELTA) {
|
|
276
|
+
return this.readOfsDelta(pos, offset, size);
|
|
277
|
+
}
|
|
278
|
+
if (type === OBJ_REF_DELTA) {
|
|
279
|
+
return this.readRefDelta(pos, size);
|
|
280
|
+
}
|
|
281
|
+
const typeName = TYPE_MAP[type];
|
|
282
|
+
if (!typeName) {
|
|
283
|
+
throw new Error(`Unknown pack object type: ${type}`);
|
|
284
|
+
}
|
|
285
|
+
const content = pako.inflate(data.subarray(pos));
|
|
286
|
+
return { type: typeName, content };
|
|
287
|
+
}
|
|
288
|
+
readOfsDelta(pos, currentOffset, _size) {
|
|
289
|
+
const data = this.packData;
|
|
290
|
+
let byte = data[pos++];
|
|
291
|
+
let negOffset = byte & 127;
|
|
292
|
+
while (byte & 128) {
|
|
293
|
+
byte = data[pos++];
|
|
294
|
+
negOffset = negOffset + 1 << 7 | byte & 127;
|
|
295
|
+
}
|
|
296
|
+
const baseOffset = currentOffset - negOffset;
|
|
297
|
+
const base = this.readObjectAtOffset(baseOffset);
|
|
298
|
+
const deltaData = pako.inflate(data.subarray(pos));
|
|
299
|
+
const result = applyDelta(base.content, deltaData);
|
|
300
|
+
return { type: base.type, content: result };
|
|
301
|
+
}
|
|
302
|
+
readRefDelta(pos, _size) {
|
|
303
|
+
const data = this.packData;
|
|
304
|
+
const baseSha = bytesToHex(data.subarray(pos, pos + 20));
|
|
305
|
+
pos += 20;
|
|
306
|
+
const baseOffset = this.index.offsets.get(baseSha);
|
|
307
|
+
if (baseOffset === void 0) {
|
|
308
|
+
throw new Error(`Base object not found in pack: ${baseSha}`);
|
|
309
|
+
}
|
|
310
|
+
const base = this.readObjectAtOffset(baseOffset);
|
|
311
|
+
const deltaData = pako.inflate(data.subarray(pos));
|
|
312
|
+
const result = applyDelta(base.content, deltaData);
|
|
313
|
+
return { type: base.type, content: result };
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// src/object-db.ts
|
|
320
|
+
init_utils();
|
|
321
|
+
import pako2 from "pako";
|
|
322
|
+
|
|
323
|
+
// src/errors.ts
|
|
324
|
+
var GitError = class extends Error {
|
|
325
|
+
code;
|
|
326
|
+
constructor(code, message) {
|
|
327
|
+
super(message);
|
|
328
|
+
this.name = "GitError";
|
|
329
|
+
this.code = code;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/object-db.ts
|
|
334
|
+
init_read();
|
|
335
|
+
var ObjectDB = class {
|
|
336
|
+
constructor(layer, gitDir) {
|
|
337
|
+
this.layer = layer;
|
|
338
|
+
this.gitDir = gitDir;
|
|
339
|
+
}
|
|
340
|
+
// ---- Write objects -----------------------------------------------------
|
|
341
|
+
async writeObject(type, content) {
|
|
342
|
+
const header = encode(`${type} ${content.length}\0`);
|
|
343
|
+
const store = concat(header, content);
|
|
344
|
+
const oid = await sha1(store);
|
|
345
|
+
const dir = `${this.gitDir}/objects/${oid.substring(0, 2)}`;
|
|
346
|
+
const filePath = `${dir}/${oid.substring(2)}`;
|
|
347
|
+
if (await this.layer.exists(filePath)) {
|
|
348
|
+
return oid;
|
|
349
|
+
}
|
|
350
|
+
await this.layer.mkdir(dir, { recursive: true });
|
|
351
|
+
const compressed = pako2.deflate(store);
|
|
352
|
+
await this.layer.writeFile(filePath, compressed);
|
|
353
|
+
return oid;
|
|
354
|
+
}
|
|
355
|
+
// ---- Read objects ------------------------------------------------------
|
|
356
|
+
async readObject(oid) {
|
|
357
|
+
const dir = oid.substring(0, 2);
|
|
358
|
+
const file = oid.substring(2);
|
|
359
|
+
const loosePath = `${this.gitDir}/objects/${dir}/${file}`;
|
|
360
|
+
if (await this.layer.exists(loosePath)) {
|
|
361
|
+
const compressed = await this.layer.readFile(loosePath);
|
|
362
|
+
const data = pako2.inflate(compressed);
|
|
363
|
+
return this.parseRawObject(data, oid);
|
|
364
|
+
}
|
|
365
|
+
const packResult = await this.readFromPacks(oid);
|
|
366
|
+
if (packResult) return packResult;
|
|
367
|
+
throw new GitError("INVALID_OBJECT", `Object not found: ${oid}`);
|
|
368
|
+
}
|
|
369
|
+
async existsObject(oid) {
|
|
370
|
+
const dir = oid.substring(0, 2);
|
|
371
|
+
const file = oid.substring(2);
|
|
372
|
+
const loosePath = `${this.gitDir}/objects/${dir}/${file}`;
|
|
373
|
+
if (await this.layer.exists(loosePath)) return true;
|
|
374
|
+
try {
|
|
375
|
+
const packResult = await this.readFromPacks(oid);
|
|
376
|
+
return packResult !== null;
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// ---- Parse raw git object ----------------------------------------------
|
|
382
|
+
parseRawObject(data, oid) {
|
|
383
|
+
let nullIdx = -1;
|
|
384
|
+
for (let i = 0; i < data.length; i++) {
|
|
385
|
+
if (data[i] === 0) {
|
|
386
|
+
nullIdx = i;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (nullIdx < 0) {
|
|
391
|
+
throw new GitError("INVALID_OBJECT", `Malformed object: ${oid}`);
|
|
392
|
+
}
|
|
393
|
+
const header = decode(data.subarray(0, nullIdx));
|
|
394
|
+
const spaceIdx = header.indexOf(" ");
|
|
395
|
+
if (spaceIdx < 0) {
|
|
396
|
+
throw new GitError("INVALID_OBJECT", `Malformed object header: ${oid}`);
|
|
397
|
+
}
|
|
398
|
+
const type = header.substring(0, spaceIdx);
|
|
399
|
+
const content = data.subarray(nullIdx + 1);
|
|
400
|
+
return { type, content };
|
|
401
|
+
}
|
|
402
|
+
// ---- Packfile search ---------------------------------------------------
|
|
403
|
+
async readFromPacks(oid) {
|
|
404
|
+
const packDir = `${this.gitDir}/objects/pack`;
|
|
405
|
+
if (!await this.layer.exists(packDir)) return null;
|
|
406
|
+
const entries = await this.layer.readdir(packDir);
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
if (!entry.name.endsWith(".idx")) continue;
|
|
409
|
+
const baseName = entry.name.replace(/\.idx$/, "");
|
|
410
|
+
const idxPath = `${packDir}/${entry.name}`;
|
|
411
|
+
const packPath = `${packDir}/${baseName}.pack`;
|
|
412
|
+
if (!await this.layer.exists(packPath)) continue;
|
|
413
|
+
const reader = new PackReader(this.layer, packPath, idxPath);
|
|
414
|
+
const obj = await reader.readObject(oid);
|
|
415
|
+
if (obj) return obj;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
// ---- High-level object construction ------------------------------------
|
|
420
|
+
async writeBlob(content) {
|
|
421
|
+
return this.writeObject("blob", content);
|
|
422
|
+
}
|
|
423
|
+
async writeTree(entries) {
|
|
424
|
+
const sorted = [...entries].sort((a, b) => {
|
|
425
|
+
const aName = a.mode.startsWith("40") ? a.name + "/" : a.name;
|
|
426
|
+
const bName = b.mode.startsWith("40") ? b.name + "/" : b.name;
|
|
427
|
+
return aName < bName ? -1 : aName > bName ? 1 : 0;
|
|
428
|
+
});
|
|
429
|
+
const parts = [];
|
|
430
|
+
for (const entry of sorted) {
|
|
431
|
+
const modeName = encode(`${entry.mode} ${entry.name}\0`);
|
|
432
|
+
const shaBytes = hexToBytes(entry.oid);
|
|
433
|
+
parts.push(modeName, shaBytes);
|
|
434
|
+
}
|
|
435
|
+
return this.writeObject("tree", concat(...parts));
|
|
436
|
+
}
|
|
437
|
+
parseTree(content) {
|
|
438
|
+
const entries = [];
|
|
439
|
+
let offset = 0;
|
|
440
|
+
while (offset < content.length) {
|
|
441
|
+
let spaceIdx = offset;
|
|
442
|
+
while (spaceIdx < content.length && content[spaceIdx] !== 32) spaceIdx++;
|
|
443
|
+
const mode = decode(content.subarray(offset, spaceIdx));
|
|
444
|
+
let nullIdx = spaceIdx + 1;
|
|
445
|
+
while (nullIdx < content.length && content[nullIdx] !== 0) nullIdx++;
|
|
446
|
+
const name = decode(content.subarray(spaceIdx + 1, nullIdx));
|
|
447
|
+
const shaBytes = content.subarray(nullIdx + 1, nullIdx + 21);
|
|
448
|
+
const oid = bytesToHex(shaBytes);
|
|
449
|
+
entries.push({ mode, name, oid });
|
|
450
|
+
offset = nullIdx + 21;
|
|
451
|
+
}
|
|
452
|
+
return entries;
|
|
453
|
+
}
|
|
454
|
+
async writeCommit(tree, parents, author, committer, message) {
|
|
455
|
+
let content = `tree ${tree}
|
|
456
|
+
`;
|
|
457
|
+
for (const parent of parents) {
|
|
458
|
+
content += `parent ${parent}
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
content += `author ${formatIdentity(author)}
|
|
462
|
+
`;
|
|
463
|
+
content += `committer ${formatIdentity(committer)}
|
|
464
|
+
`;
|
|
465
|
+
content += `
|
|
466
|
+
${message}`;
|
|
467
|
+
return this.writeObject("commit", encode(content));
|
|
468
|
+
}
|
|
469
|
+
parseCommit(content) {
|
|
470
|
+
const text = decode(content);
|
|
471
|
+
const headerEnd = text.indexOf("\n\n");
|
|
472
|
+
const headerPart = headerEnd >= 0 ? text.substring(0, headerEnd) : text;
|
|
473
|
+
const message = headerEnd >= 0 ? text.substring(headerEnd + 2) : "";
|
|
474
|
+
const lines = headerPart.split("\n");
|
|
475
|
+
let tree = "";
|
|
476
|
+
const parents = [];
|
|
477
|
+
let author;
|
|
478
|
+
let committer;
|
|
479
|
+
for (const line of lines) {
|
|
480
|
+
if (line.startsWith("tree ")) {
|
|
481
|
+
tree = line.substring(5);
|
|
482
|
+
} else if (line.startsWith("parent ")) {
|
|
483
|
+
parents.push(line.substring(7));
|
|
484
|
+
} else if (line.startsWith("author ")) {
|
|
485
|
+
author = parseIdentity(line.substring(7));
|
|
486
|
+
} else if (line.startsWith("committer ")) {
|
|
487
|
+
committer = parseIdentity(line.substring(10));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (!author || !committer) {
|
|
491
|
+
throw new GitError(
|
|
492
|
+
"INVALID_OBJECT",
|
|
493
|
+
"Commit missing author or committer"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return { tree, parents, author, committer, message };
|
|
497
|
+
}
|
|
498
|
+
async writeTag(object, type, tagName, tagger, message) {
|
|
499
|
+
let content = `object ${object}
|
|
500
|
+
`;
|
|
501
|
+
content += `type ${type}
|
|
502
|
+
`;
|
|
503
|
+
content += `tag ${tagName}
|
|
504
|
+
`;
|
|
505
|
+
content += `tagger ${formatIdentity(tagger)}
|
|
506
|
+
`;
|
|
507
|
+
content += `
|
|
508
|
+
${message}`;
|
|
509
|
+
return this.writeObject("tag", encode(content));
|
|
510
|
+
}
|
|
511
|
+
parseTag(content) {
|
|
512
|
+
const text = decode(content);
|
|
513
|
+
const headerEnd = text.indexOf("\n\n");
|
|
514
|
+
const headerPart = headerEnd >= 0 ? text.substring(0, headerEnd) : text;
|
|
515
|
+
const message = headerEnd >= 0 ? text.substring(headerEnd + 2) : "";
|
|
516
|
+
const lines = headerPart.split("\n");
|
|
517
|
+
let object = "";
|
|
518
|
+
let type = "commit";
|
|
519
|
+
let tag = "";
|
|
520
|
+
let tagger;
|
|
521
|
+
for (const line of lines) {
|
|
522
|
+
if (line.startsWith("object ")) object = line.substring(7);
|
|
523
|
+
else if (line.startsWith("type ")) type = line.substring(5);
|
|
524
|
+
else if (line.startsWith("tag ")) tag = line.substring(4);
|
|
525
|
+
else if (line.startsWith("tagger ")) tagger = parseIdentity(line.substring(7));
|
|
526
|
+
}
|
|
527
|
+
if (!tagger) {
|
|
528
|
+
throw new GitError("INVALID_OBJECT", "Tag missing tagger");
|
|
529
|
+
}
|
|
530
|
+
return { object, type, tag, tagger, message };
|
|
531
|
+
}
|
|
532
|
+
// ---- Hash object without writing (for comparison) ----------------------
|
|
533
|
+
async hashObject(type, content) {
|
|
534
|
+
const header = encode(`${type} ${content.length}\0`);
|
|
535
|
+
const store = concat(header, content);
|
|
536
|
+
return sha1(store);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
function formatIdentity(id) {
|
|
540
|
+
const sign = id.timezoneOffset >= 0 ? "+" : "-";
|
|
541
|
+
const absOffset = Math.abs(id.timezoneOffset);
|
|
542
|
+
const hours = Math.floor(absOffset / 60).toString().padStart(2, "0");
|
|
543
|
+
const minutes = (absOffset % 60).toString().padStart(2, "0");
|
|
544
|
+
return `${id.name} <${id.email}> ${id.timestamp} ${sign}${hours}${minutes}`;
|
|
545
|
+
}
|
|
546
|
+
function parseIdentity(str) {
|
|
547
|
+
const emailStart = str.indexOf("<");
|
|
548
|
+
const emailEnd = str.indexOf(">");
|
|
549
|
+
if (emailStart < 0 || emailEnd < 0) {
|
|
550
|
+
throw new GitError("INVALID_OBJECT", `Invalid identity: ${str}`);
|
|
551
|
+
}
|
|
552
|
+
const name = str.substring(0, emailStart).trim();
|
|
553
|
+
const email = str.substring(emailStart + 1, emailEnd);
|
|
554
|
+
const rest = str.substring(emailEnd + 1).trim();
|
|
555
|
+
const parts = rest.split(" ");
|
|
556
|
+
const timestamp = parseInt(parts[0], 10);
|
|
557
|
+
const tz = parts[1] || "+0000";
|
|
558
|
+
const tzSign = tz.startsWith("-") ? -1 : 1;
|
|
559
|
+
const tzHours = parseInt(tz.substring(1, 3), 10);
|
|
560
|
+
const tzMinutes = parseInt(tz.substring(3, 5), 10);
|
|
561
|
+
const timezoneOffset = tzSign * (tzHours * 60 + tzMinutes);
|
|
562
|
+
return { name, email, timestamp, timezoneOffset };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/ref-store.ts
|
|
566
|
+
init_utils();
|
|
567
|
+
var RefStore = class {
|
|
568
|
+
constructor(layer, gitDir) {
|
|
569
|
+
this.layer = layer;
|
|
570
|
+
this.gitDir = gitDir;
|
|
571
|
+
}
|
|
572
|
+
// ---- Read refs ---------------------------------------------------------
|
|
573
|
+
async readRef(ref) {
|
|
574
|
+
const path = `${this.gitDir}/${ref}`;
|
|
575
|
+
if (!await this.layer.exists(path)) {
|
|
576
|
+
return this.readPackedRef(ref);
|
|
577
|
+
}
|
|
578
|
+
const content = decode(await this.layer.readFile(path)).trim();
|
|
579
|
+
if (content.startsWith("ref: ")) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
return content;
|
|
583
|
+
}
|
|
584
|
+
async readSymbolicRef(ref) {
|
|
585
|
+
const path = `${this.gitDir}/${ref}`;
|
|
586
|
+
if (!await this.layer.exists(path)) return null;
|
|
587
|
+
const content = decode(await this.layer.readFile(path)).trim();
|
|
588
|
+
if (content.startsWith("ref: ")) {
|
|
589
|
+
return content.substring(5);
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
async resolveRef(ref) {
|
|
594
|
+
return this.resolveRefInner(ref, 0);
|
|
595
|
+
}
|
|
596
|
+
async resolveRefInner(ref, depth) {
|
|
597
|
+
if (depth > 10) {
|
|
598
|
+
throw new GitError("INVALID_OBJECT", `Ref loop detected: ${ref}`);
|
|
599
|
+
}
|
|
600
|
+
if (/^[0-9a-f]{40}$/.test(ref)) {
|
|
601
|
+
return ref;
|
|
602
|
+
}
|
|
603
|
+
const path = `${this.gitDir}/${ref}`;
|
|
604
|
+
if (await this.layer.exists(path)) {
|
|
605
|
+
const content = decode(await this.layer.readFile(path)).trim();
|
|
606
|
+
if (content.startsWith("ref: ")) {
|
|
607
|
+
return this.resolveRefInner(content.substring(5), depth + 1);
|
|
608
|
+
}
|
|
609
|
+
if (/^[0-9a-f]{40}$/.test(content)) {
|
|
610
|
+
return content;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const packed = await this.readPackedRef(ref);
|
|
614
|
+
if (packed) return packed;
|
|
615
|
+
throw new GitError("NOT_FOUND", `Ref not found: ${ref}`);
|
|
616
|
+
}
|
|
617
|
+
// ---- Resolve short ref names ------------------------------------------
|
|
618
|
+
async expandRef(shortName) {
|
|
619
|
+
const candidates = [
|
|
620
|
+
shortName,
|
|
621
|
+
`refs/${shortName}`,
|
|
622
|
+
`refs/tags/${shortName}`,
|
|
623
|
+
`refs/heads/${shortName}`,
|
|
624
|
+
`refs/remotes/${shortName}`,
|
|
625
|
+
`refs/remotes/${shortName}/HEAD`
|
|
626
|
+
];
|
|
627
|
+
for (const candidate of candidates) {
|
|
628
|
+
try {
|
|
629
|
+
await this.resolveRef(candidate);
|
|
630
|
+
return candidate;
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (/^[0-9a-f]{40}$/.test(shortName)) {
|
|
635
|
+
return shortName;
|
|
636
|
+
}
|
|
637
|
+
throw new GitError("NOT_FOUND", `Ref not found: ${shortName}`);
|
|
638
|
+
}
|
|
639
|
+
// ---- Write refs --------------------------------------------------------
|
|
640
|
+
async writeRef(ref, oid) {
|
|
641
|
+
const path = `${this.gitDir}/${ref}`;
|
|
642
|
+
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
643
|
+
await this.layer.mkdir(dir, { recursive: true });
|
|
644
|
+
await this.layer.writeFile(path, encode(`${oid}
|
|
645
|
+
`));
|
|
646
|
+
}
|
|
647
|
+
async writeSymbolicRef(ref, target) {
|
|
648
|
+
const path = `${this.gitDir}/${ref}`;
|
|
649
|
+
await this.layer.writeFile(path, encode(`ref: ${target}
|
|
650
|
+
`));
|
|
651
|
+
}
|
|
652
|
+
async deleteRef(ref) {
|
|
653
|
+
const path = `${this.gitDir}/${ref}`;
|
|
654
|
+
if (await this.layer.exists(path)) {
|
|
655
|
+
await this.layer.rm(path);
|
|
656
|
+
}
|
|
657
|
+
await this.removeFromPackedRefs(ref);
|
|
658
|
+
}
|
|
659
|
+
// ---- List refs ---------------------------------------------------------
|
|
660
|
+
async listRefs(prefix = "refs/") {
|
|
661
|
+
const results = [];
|
|
662
|
+
await this.listLooseRefs(`${this.gitDir}/${prefix}`, prefix, results);
|
|
663
|
+
const packed = await this.readAllPackedRefs();
|
|
664
|
+
for (const [ref, oid] of packed) {
|
|
665
|
+
if (ref.startsWith(prefix)) {
|
|
666
|
+
if (!results.some((r) => r.name === ref)) {
|
|
667
|
+
results.push({ name: ref, oid });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
672
|
+
}
|
|
673
|
+
async listLooseRefs(dirPath, refPrefix, results) {
|
|
674
|
+
if (!await this.layer.exists(dirPath)) return;
|
|
675
|
+
const entries = await this.layer.readdir(dirPath);
|
|
676
|
+
for (const entry of entries) {
|
|
677
|
+
const refName = refPrefix + entry.name;
|
|
678
|
+
if (entry.isDirectory()) {
|
|
679
|
+
await this.listLooseRefs(`${dirPath}/${entry.name}`, `${refName}/`, results);
|
|
680
|
+
} else {
|
|
681
|
+
try {
|
|
682
|
+
const content = decode(
|
|
683
|
+
await this.layer.readFile(`${dirPath}/${entry.name}`)
|
|
684
|
+
).trim();
|
|
685
|
+
if (/^[0-9a-f]{40}$/.test(content)) {
|
|
686
|
+
results.push({ name: refName, oid: content });
|
|
687
|
+
}
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// ---- Packed refs -------------------------------------------------------
|
|
694
|
+
async readPackedRef(ref) {
|
|
695
|
+
const packed = await this.readAllPackedRefs();
|
|
696
|
+
return packed.get(ref) ?? null;
|
|
697
|
+
}
|
|
698
|
+
async readAllPackedRefs() {
|
|
699
|
+
const result = /* @__PURE__ */ new Map();
|
|
700
|
+
const path = `${this.gitDir}/packed-refs`;
|
|
701
|
+
if (!await this.layer.exists(path)) return result;
|
|
702
|
+
const content = decode(await this.layer.readFile(path));
|
|
703
|
+
const lines = content.split("\n");
|
|
704
|
+
for (const line of lines) {
|
|
705
|
+
if (line.startsWith("#") || line.startsWith("^") || !line.trim()) continue;
|
|
706
|
+
const parts = line.trim().split(" ");
|
|
707
|
+
if (parts.length >= 2 && /^[0-9a-f]{40}$/.test(parts[0])) {
|
|
708
|
+
result.set(parts[1], parts[0]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
713
|
+
async removeFromPackedRefs(ref) {
|
|
714
|
+
const path = `${this.gitDir}/packed-refs`;
|
|
715
|
+
if (!await this.layer.exists(path)) return;
|
|
716
|
+
const content = decode(await this.layer.readFile(path));
|
|
717
|
+
const lines = content.split("\n");
|
|
718
|
+
const filtered = lines.filter((line) => {
|
|
719
|
+
if (line.startsWith("#") || !line.trim()) return true;
|
|
720
|
+
const parts = line.trim().split(" ");
|
|
721
|
+
return parts.length < 2 || parts[1] !== ref;
|
|
722
|
+
});
|
|
723
|
+
await this.layer.writeFile(path, encode(filtered.join("\n")));
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// src/index-file.ts
|
|
728
|
+
init_utils();
|
|
729
|
+
var INDEX_SIGNATURE = 1145655875;
|
|
730
|
+
var INDEX_VERSION = 2;
|
|
731
|
+
var IndexFile = class {
|
|
732
|
+
constructor(layer, gitDir) {
|
|
733
|
+
this.layer = layer;
|
|
734
|
+
this.gitDir = gitDir;
|
|
735
|
+
}
|
|
736
|
+
entries = [];
|
|
737
|
+
// ---- Read index --------------------------------------------------------
|
|
738
|
+
async read() {
|
|
739
|
+
const path = `${this.gitDir}/index`;
|
|
740
|
+
if (!await this.layer.exists(path)) {
|
|
741
|
+
this.entries = [];
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const data = await this.layer.readFile(path);
|
|
745
|
+
this.parse(data);
|
|
746
|
+
}
|
|
747
|
+
parse(data) {
|
|
748
|
+
const entries = [];
|
|
749
|
+
let offset = 0;
|
|
750
|
+
const sig = readUint322(data, offset);
|
|
751
|
+
offset += 4;
|
|
752
|
+
if (sig !== INDEX_SIGNATURE) {
|
|
753
|
+
throw new Error("Invalid index signature");
|
|
754
|
+
}
|
|
755
|
+
const version = readUint322(data, offset);
|
|
756
|
+
offset += 4;
|
|
757
|
+
if (version !== INDEX_VERSION) {
|
|
758
|
+
throw new Error(`Unsupported index version: ${version}`);
|
|
759
|
+
}
|
|
760
|
+
const entryCount = readUint322(data, offset);
|
|
761
|
+
offset += 4;
|
|
762
|
+
for (let i = 0; i < entryCount; i++) {
|
|
763
|
+
const entryStart = offset;
|
|
764
|
+
const ctimeSeconds = readUint322(data, offset);
|
|
765
|
+
offset += 4;
|
|
766
|
+
const ctimeNanoseconds = readUint322(data, offset);
|
|
767
|
+
offset += 4;
|
|
768
|
+
const mtimeSeconds = readUint322(data, offset);
|
|
769
|
+
offset += 4;
|
|
770
|
+
const mtimeNanoseconds = readUint322(data, offset);
|
|
771
|
+
offset += 4;
|
|
772
|
+
const dev = readUint322(data, offset);
|
|
773
|
+
offset += 4;
|
|
774
|
+
const ino = readUint322(data, offset);
|
|
775
|
+
offset += 4;
|
|
776
|
+
const mode = readUint322(data, offset);
|
|
777
|
+
offset += 4;
|
|
778
|
+
const uid = readUint322(data, offset);
|
|
779
|
+
offset += 4;
|
|
780
|
+
const gid = readUint322(data, offset);
|
|
781
|
+
offset += 4;
|
|
782
|
+
const size = readUint322(data, offset);
|
|
783
|
+
offset += 4;
|
|
784
|
+
const oid = bytesToHex(data.subarray(offset, offset + 20));
|
|
785
|
+
offset += 20;
|
|
786
|
+
const flags = readUint16(data, offset);
|
|
787
|
+
offset += 2;
|
|
788
|
+
const nameLength = flags & 4095;
|
|
789
|
+
const stage = flags >> 12 & 3;
|
|
790
|
+
let pathEnd = offset;
|
|
791
|
+
while (pathEnd < data.length && data[pathEnd] !== 0) {
|
|
792
|
+
pathEnd++;
|
|
793
|
+
}
|
|
794
|
+
const path = decode(data.subarray(offset, pathEnd));
|
|
795
|
+
const entrySize = pathEnd - entryStart + 1;
|
|
796
|
+
const paddedSize = Math.ceil(entrySize / 8) * 8;
|
|
797
|
+
offset = entryStart + paddedSize;
|
|
798
|
+
entries.push({
|
|
799
|
+
ctimeSeconds,
|
|
800
|
+
ctimeNanoseconds,
|
|
801
|
+
mtimeSeconds,
|
|
802
|
+
mtimeNanoseconds,
|
|
803
|
+
dev,
|
|
804
|
+
ino,
|
|
805
|
+
mode,
|
|
806
|
+
uid,
|
|
807
|
+
gid,
|
|
808
|
+
size,
|
|
809
|
+
oid,
|
|
810
|
+
flags,
|
|
811
|
+
path,
|
|
812
|
+
stage
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
this.entries = entries;
|
|
816
|
+
}
|
|
817
|
+
// ---- Write index -------------------------------------------------------
|
|
818
|
+
async write() {
|
|
819
|
+
const data = await this.serialize();
|
|
820
|
+
await this.layer.writeFile(`${this.gitDir}/index`, data);
|
|
821
|
+
}
|
|
822
|
+
async serialize() {
|
|
823
|
+
const sorted = [...this.entries].sort(
|
|
824
|
+
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
825
|
+
);
|
|
826
|
+
const parts = [];
|
|
827
|
+
const header = new Uint8Array(12);
|
|
828
|
+
writeUint32(header, 0, INDEX_SIGNATURE);
|
|
829
|
+
writeUint32(header, 4, INDEX_VERSION);
|
|
830
|
+
writeUint32(header, 8, sorted.length);
|
|
831
|
+
parts.push(header);
|
|
832
|
+
for (const entry of sorted) {
|
|
833
|
+
const pathBytes = encode(entry.path);
|
|
834
|
+
const fixedSize = 62;
|
|
835
|
+
const entryDataSize = fixedSize + pathBytes.length + 1;
|
|
836
|
+
const paddedSize = Math.ceil(entryDataSize / 8) * 8;
|
|
837
|
+
const entryBuf = new Uint8Array(paddedSize);
|
|
838
|
+
let offset = 0;
|
|
839
|
+
writeUint32(entryBuf, offset, entry.ctimeSeconds);
|
|
840
|
+
offset += 4;
|
|
841
|
+
writeUint32(entryBuf, offset, entry.ctimeNanoseconds);
|
|
842
|
+
offset += 4;
|
|
843
|
+
writeUint32(entryBuf, offset, entry.mtimeSeconds);
|
|
844
|
+
offset += 4;
|
|
845
|
+
writeUint32(entryBuf, offset, entry.mtimeNanoseconds);
|
|
846
|
+
offset += 4;
|
|
847
|
+
writeUint32(entryBuf, offset, entry.dev);
|
|
848
|
+
offset += 4;
|
|
849
|
+
writeUint32(entryBuf, offset, entry.ino);
|
|
850
|
+
offset += 4;
|
|
851
|
+
writeUint32(entryBuf, offset, entry.mode);
|
|
852
|
+
offset += 4;
|
|
853
|
+
writeUint32(entryBuf, offset, entry.uid);
|
|
854
|
+
offset += 4;
|
|
855
|
+
writeUint32(entryBuf, offset, entry.gid);
|
|
856
|
+
offset += 4;
|
|
857
|
+
writeUint32(entryBuf, offset, entry.size);
|
|
858
|
+
offset += 4;
|
|
859
|
+
entryBuf.set(hexToBytes(entry.oid), offset);
|
|
860
|
+
offset += 20;
|
|
861
|
+
const nameLen = Math.min(pathBytes.length, 4095);
|
|
862
|
+
const flags = entry.stage << 12 | nameLen;
|
|
863
|
+
writeUint16(entryBuf, offset, flags);
|
|
864
|
+
offset += 2;
|
|
865
|
+
entryBuf.set(pathBytes, offset);
|
|
866
|
+
parts.push(entryBuf);
|
|
867
|
+
}
|
|
868
|
+
const content = concat(...parts);
|
|
869
|
+
const checksum = await sha1Bytes(content);
|
|
870
|
+
return concat(content, checksum);
|
|
871
|
+
}
|
|
872
|
+
// ---- Entry manipulation ------------------------------------------------
|
|
873
|
+
addEntry(entry) {
|
|
874
|
+
this.entries = this.entries.filter(
|
|
875
|
+
(e) => !(e.path === entry.path && e.stage === entry.stage)
|
|
876
|
+
);
|
|
877
|
+
this.entries.push(entry);
|
|
878
|
+
this.sortEntries();
|
|
879
|
+
}
|
|
880
|
+
removeEntry(path) {
|
|
881
|
+
this.entries = this.entries.filter((e) => e.path !== path);
|
|
882
|
+
}
|
|
883
|
+
removeEntriesUnder(dirPath) {
|
|
884
|
+
const prefix = dirPath.endsWith("/") ? dirPath : dirPath + "/";
|
|
885
|
+
this.entries = this.entries.filter((e) => !e.path.startsWith(prefix));
|
|
886
|
+
}
|
|
887
|
+
getEntry(path, stage = 0) {
|
|
888
|
+
return this.entries.find((e) => e.path === path && e.stage === stage);
|
|
889
|
+
}
|
|
890
|
+
hasConflicts() {
|
|
891
|
+
return this.entries.some((e) => e.stage !== 0);
|
|
892
|
+
}
|
|
893
|
+
getConflictedPaths() {
|
|
894
|
+
const paths = /* @__PURE__ */ new Set();
|
|
895
|
+
for (const e of this.entries) {
|
|
896
|
+
if (e.stage !== 0) paths.add(e.path);
|
|
897
|
+
}
|
|
898
|
+
return [...paths];
|
|
899
|
+
}
|
|
900
|
+
clearConflictEntries(path) {
|
|
901
|
+
this.entries = this.entries.filter(
|
|
902
|
+
(e) => e.path !== path || e.stage === 0
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
sortEntries() {
|
|
906
|
+
this.entries.sort((a, b) => {
|
|
907
|
+
if (a.path < b.path) return -1;
|
|
908
|
+
if (a.path > b.path) return 1;
|
|
909
|
+
return a.stage - b.stage;
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
function readUint322(data, offset) {
|
|
914
|
+
return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
|
|
915
|
+
}
|
|
916
|
+
function readUint16(data, offset) {
|
|
917
|
+
return (data[offset] << 8 | data[offset + 1]) >>> 0;
|
|
918
|
+
}
|
|
919
|
+
function writeUint32(data, offset, value) {
|
|
920
|
+
data[offset] = value >>> 24 & 255;
|
|
921
|
+
data[offset + 1] = value >>> 16 & 255;
|
|
922
|
+
data[offset + 2] = value >>> 8 & 255;
|
|
923
|
+
data[offset + 3] = value & 255;
|
|
924
|
+
}
|
|
925
|
+
function writeUint16(data, offset, value) {
|
|
926
|
+
data[offset] = value >>> 8 & 255;
|
|
927
|
+
data[offset + 1] = value & 255;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/config.ts
|
|
931
|
+
init_utils();
|
|
932
|
+
var GitConfig = class {
|
|
933
|
+
constructor(layer, gitDir) {
|
|
934
|
+
this.layer = layer;
|
|
935
|
+
this.gitDir = gitDir;
|
|
936
|
+
}
|
|
937
|
+
sections = [];
|
|
938
|
+
loaded = false;
|
|
939
|
+
// ---- Load / save -------------------------------------------------------
|
|
940
|
+
async load() {
|
|
941
|
+
const path = `${this.gitDir}/config`;
|
|
942
|
+
if (!await this.layer.exists(path)) {
|
|
943
|
+
this.sections = [];
|
|
944
|
+
this.loaded = true;
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const content = decode(await this.layer.readFile(path));
|
|
948
|
+
this.sections = this.parseConfig(content);
|
|
949
|
+
this.loaded = true;
|
|
950
|
+
}
|
|
951
|
+
async save() {
|
|
952
|
+
const content = this.serialize();
|
|
953
|
+
await this.layer.writeFile(`${this.gitDir}/config`, encode(content));
|
|
954
|
+
}
|
|
955
|
+
// ---- Get / set / delete ------------------------------------------------
|
|
956
|
+
get(key) {
|
|
957
|
+
const { section, subsection, name } = this.parseKey(key);
|
|
958
|
+
for (const s of this.sections) {
|
|
959
|
+
if (s.name.toLowerCase() === section.toLowerCase() && (subsection === void 0 ? s.subsection === void 0 : s.subsection === subsection)) {
|
|
960
|
+
for (const entry of s.entries) {
|
|
961
|
+
if (entry.key.toLowerCase() === name.toLowerCase()) {
|
|
962
|
+
return entry.value;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return void 0;
|
|
968
|
+
}
|
|
969
|
+
getAll(key) {
|
|
970
|
+
const { section, subsection, name } = this.parseKey(key);
|
|
971
|
+
const results = [];
|
|
972
|
+
for (const s of this.sections) {
|
|
973
|
+
if (s.name.toLowerCase() === section.toLowerCase() && (subsection === void 0 ? s.subsection === void 0 : s.subsection === subsection)) {
|
|
974
|
+
for (const entry of s.entries) {
|
|
975
|
+
if (entry.key.toLowerCase() === name.toLowerCase()) {
|
|
976
|
+
results.push(entry.value);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return results;
|
|
982
|
+
}
|
|
983
|
+
set(key, value) {
|
|
984
|
+
const { section, subsection, name } = this.parseKey(key);
|
|
985
|
+
for (const s of this.sections) {
|
|
986
|
+
if (s.name.toLowerCase() === section.toLowerCase() && (subsection === void 0 ? s.subsection === void 0 : s.subsection === subsection)) {
|
|
987
|
+
for (const entry of s.entries) {
|
|
988
|
+
if (entry.key.toLowerCase() === name.toLowerCase()) {
|
|
989
|
+
entry.value = value;
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
s.entries.push({ key: name, value });
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
this.sections.push({
|
|
998
|
+
name: section,
|
|
999
|
+
subsection,
|
|
1000
|
+
entries: [{ key: name, value }]
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
delete(key) {
|
|
1004
|
+
const { section, subsection, name } = this.parseKey(key);
|
|
1005
|
+
for (const s of this.sections) {
|
|
1006
|
+
if (s.name.toLowerCase() === section.toLowerCase() && (subsection === void 0 ? s.subsection === void 0 : s.subsection === subsection)) {
|
|
1007
|
+
const idx = s.entries.findIndex(
|
|
1008
|
+
(e) => e.key.toLowerCase() === name.toLowerCase()
|
|
1009
|
+
);
|
|
1010
|
+
if (idx >= 0) {
|
|
1011
|
+
s.entries.splice(idx, 1);
|
|
1012
|
+
if (s.entries.length === 0) {
|
|
1013
|
+
this.sections = this.sections.filter((x) => x !== s);
|
|
1014
|
+
}
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
deleteSection(sectionKey) {
|
|
1022
|
+
const parts = sectionKey.split(".");
|
|
1023
|
+
const section = parts[0];
|
|
1024
|
+
const subsection = parts.length > 1 ? parts[1] : void 0;
|
|
1025
|
+
const before = this.sections.length;
|
|
1026
|
+
this.sections = this.sections.filter((s) => {
|
|
1027
|
+
if (s.name.toLowerCase() !== section.toLowerCase()) return true;
|
|
1028
|
+
if (subsection === void 0) return s.subsection !== void 0;
|
|
1029
|
+
return s.subsection !== subsection;
|
|
1030
|
+
});
|
|
1031
|
+
return this.sections.length < before;
|
|
1032
|
+
}
|
|
1033
|
+
// ---- Parse / serialize -------------------------------------------------
|
|
1034
|
+
parseConfig(content) {
|
|
1035
|
+
const sections = [];
|
|
1036
|
+
let current = null;
|
|
1037
|
+
const lines = content.split("\n");
|
|
1038
|
+
for (const rawLine of lines) {
|
|
1039
|
+
const line = rawLine.trim();
|
|
1040
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
1041
|
+
const sectionMatch = line.match(
|
|
1042
|
+
/^\[([a-zA-Z][a-zA-Z0-9-]*)(?:\s+"([^"]*)")?\]$/
|
|
1043
|
+
);
|
|
1044
|
+
if (sectionMatch) {
|
|
1045
|
+
current = {
|
|
1046
|
+
name: sectionMatch[1],
|
|
1047
|
+
subsection: sectionMatch[2] ?? void 0,
|
|
1048
|
+
entries: []
|
|
1049
|
+
};
|
|
1050
|
+
sections.push(current);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (current) {
|
|
1054
|
+
const kvMatch = line.match(/^([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*(.*)$/);
|
|
1055
|
+
if (kvMatch) {
|
|
1056
|
+
current.entries.push({
|
|
1057
|
+
key: kvMatch[1],
|
|
1058
|
+
value: kvMatch[2].trim()
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return sections;
|
|
1064
|
+
}
|
|
1065
|
+
serialize() {
|
|
1066
|
+
const lines = [];
|
|
1067
|
+
for (const section of this.sections) {
|
|
1068
|
+
if (section.subsection !== void 0) {
|
|
1069
|
+
lines.push(`[${section.name} "${section.subsection}"]`);
|
|
1070
|
+
} else {
|
|
1071
|
+
lines.push(`[${section.name}]`);
|
|
1072
|
+
}
|
|
1073
|
+
for (const entry of section.entries) {
|
|
1074
|
+
lines.push(` ${entry.key} = ${entry.value}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return lines.join("\n") + "\n";
|
|
1078
|
+
}
|
|
1079
|
+
// ---- Key parsing -------------------------------------------------------
|
|
1080
|
+
parseKey(key) {
|
|
1081
|
+
const parts = key.split(".");
|
|
1082
|
+
if (parts.length === 2) {
|
|
1083
|
+
return { section: parts[0], name: parts[1] };
|
|
1084
|
+
}
|
|
1085
|
+
if (parts.length === 3) {
|
|
1086
|
+
return { section: parts[0], subsection: parts[1], name: parts[2] };
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error(`Invalid config key: ${key}`);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// src/ignore.ts
|
|
1093
|
+
init_utils();
|
|
1094
|
+
var GitIgnore = class {
|
|
1095
|
+
constructor(layer, workDir) {
|
|
1096
|
+
this.layer = layer;
|
|
1097
|
+
this.workDir = workDir;
|
|
1098
|
+
}
|
|
1099
|
+
rules = [];
|
|
1100
|
+
async load() {
|
|
1101
|
+
this.rules = [];
|
|
1102
|
+
await this.loadFile(`${this.workDir}/.gitignore`, "");
|
|
1103
|
+
}
|
|
1104
|
+
async loadNested(dirPath) {
|
|
1105
|
+
const ignoreFile = dirPath ? `${this.workDir}/${dirPath}/.gitignore` : `${this.workDir}/.gitignore`;
|
|
1106
|
+
if (await this.layer.exists(ignoreFile)) {
|
|
1107
|
+
await this.loadFile(ignoreFile, dirPath);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async loadFile(path, basePath) {
|
|
1111
|
+
if (!await this.layer.exists(path)) return;
|
|
1112
|
+
const content = decode(await this.layer.readFile(path));
|
|
1113
|
+
this.parseRules(content, basePath);
|
|
1114
|
+
}
|
|
1115
|
+
parseRules(content, basePath) {
|
|
1116
|
+
const lines = content.split("\n");
|
|
1117
|
+
for (let line of lines) {
|
|
1118
|
+
line = line.trim();
|
|
1119
|
+
if (!line || line.startsWith("#")) continue;
|
|
1120
|
+
let negation = false;
|
|
1121
|
+
let directoryOnly = false;
|
|
1122
|
+
if (line.startsWith("!")) {
|
|
1123
|
+
negation = true;
|
|
1124
|
+
line = line.substring(1);
|
|
1125
|
+
}
|
|
1126
|
+
if (line.endsWith("/")) {
|
|
1127
|
+
directoryOnly = true;
|
|
1128
|
+
line = line.substring(0, line.length - 1);
|
|
1129
|
+
}
|
|
1130
|
+
line = line.replace(/(?<!\\)\s+$/, "");
|
|
1131
|
+
if (!line) continue;
|
|
1132
|
+
const regex = globToRegex(line, basePath);
|
|
1133
|
+
this.rules.push({
|
|
1134
|
+
pattern: line,
|
|
1135
|
+
negation,
|
|
1136
|
+
directoryOnly,
|
|
1137
|
+
regex,
|
|
1138
|
+
basePath
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
isIgnored(path, isDirectory = false) {
|
|
1143
|
+
if (path === ".git" || path.startsWith(".git/")) return true;
|
|
1144
|
+
let ignored = false;
|
|
1145
|
+
for (const rule of this.rules) {
|
|
1146
|
+
if (rule.directoryOnly && !isDirectory) continue;
|
|
1147
|
+
const testPath = path;
|
|
1148
|
+
if (rule.regex.test(testPath)) {
|
|
1149
|
+
ignored = !rule.negation;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return ignored;
|
|
1153
|
+
}
|
|
1154
|
+
addRules(content, basePath = "") {
|
|
1155
|
+
this.parseRules(content, basePath);
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
function globToRegex(pattern, basePath) {
|
|
1159
|
+
const hasSlash = pattern.includes("/");
|
|
1160
|
+
let regexStr = "";
|
|
1161
|
+
if (hasSlash && !pattern.startsWith("/")) {
|
|
1162
|
+
if (basePath) {
|
|
1163
|
+
regexStr = "^" + escapeRegExp(basePath) + "/";
|
|
1164
|
+
} else {
|
|
1165
|
+
regexStr = "^";
|
|
1166
|
+
}
|
|
1167
|
+
} else if (pattern.startsWith("/")) {
|
|
1168
|
+
pattern = pattern.substring(1);
|
|
1169
|
+
if (basePath) {
|
|
1170
|
+
regexStr = "^" + escapeRegExp(basePath) + "/";
|
|
1171
|
+
} else {
|
|
1172
|
+
regexStr = "^";
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
let i = 0;
|
|
1176
|
+
while (i < pattern.length) {
|
|
1177
|
+
const c = pattern[i];
|
|
1178
|
+
if (c === "*") {
|
|
1179
|
+
if (pattern[i + 1] === "*") {
|
|
1180
|
+
if (pattern[i + 2] === "/") {
|
|
1181
|
+
regexStr += "(?:.+/)?";
|
|
1182
|
+
i += 3;
|
|
1183
|
+
continue;
|
|
1184
|
+
} else {
|
|
1185
|
+
regexStr += ".*";
|
|
1186
|
+
i += 2;
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
regexStr += "[^/]*";
|
|
1191
|
+
i++;
|
|
1192
|
+
} else if (c === "?") {
|
|
1193
|
+
regexStr += "[^/]";
|
|
1194
|
+
i++;
|
|
1195
|
+
} else if (c === "[") {
|
|
1196
|
+
let j = i + 1;
|
|
1197
|
+
let classStr = "[";
|
|
1198
|
+
if (j < pattern.length && pattern[j] === "!") {
|
|
1199
|
+
classStr += "^";
|
|
1200
|
+
j++;
|
|
1201
|
+
}
|
|
1202
|
+
while (j < pattern.length && pattern[j] !== "]") {
|
|
1203
|
+
classStr += pattern[j];
|
|
1204
|
+
j++;
|
|
1205
|
+
}
|
|
1206
|
+
classStr += "]";
|
|
1207
|
+
regexStr += classStr;
|
|
1208
|
+
i = j + 1;
|
|
1209
|
+
} else {
|
|
1210
|
+
regexStr += escapeRegExp(c);
|
|
1211
|
+
i++;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (!hasSlash) {
|
|
1215
|
+
regexStr = "(?:^|/)" + regexStr + "$";
|
|
1216
|
+
} else {
|
|
1217
|
+
regexStr += "$";
|
|
1218
|
+
}
|
|
1219
|
+
return new RegExp(regexStr);
|
|
1220
|
+
}
|
|
1221
|
+
function escapeRegExp(s) {
|
|
1222
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/diff-engine.ts
|
|
1226
|
+
init_utils();
|
|
1227
|
+
var DiffEngine = class {
|
|
1228
|
+
constructor(layer, workDir, objectDB, indexFile, refStore, gitDir) {
|
|
1229
|
+
this.layer = layer;
|
|
1230
|
+
this.workDir = workDir;
|
|
1231
|
+
this.objectDB = objectDB;
|
|
1232
|
+
this.indexFile = indexFile;
|
|
1233
|
+
this.refStore = refStore;
|
|
1234
|
+
this.gitDir = gitDir;
|
|
1235
|
+
}
|
|
1236
|
+
// ---- Public API --------------------------------------------------------
|
|
1237
|
+
async diff(options = {}) {
|
|
1238
|
+
if (options.from && options.to) {
|
|
1239
|
+
return this.diffRefs(options.from, options.to, options.path);
|
|
1240
|
+
}
|
|
1241
|
+
if (options.staged) {
|
|
1242
|
+
return this.diffCached(options.path, options.contextLines);
|
|
1243
|
+
}
|
|
1244
|
+
return this.diffWorkingTree(options.path, options.contextLines);
|
|
1245
|
+
}
|
|
1246
|
+
// ---- Working tree vs index --------------------------------------------
|
|
1247
|
+
async diffWorkingTree(path, contextLines) {
|
|
1248
|
+
const files = [];
|
|
1249
|
+
for (const entry of this.indexFile.entries) {
|
|
1250
|
+
if (entry.stage !== 0) continue;
|
|
1251
|
+
if (path && !entry.path.startsWith(path)) continue;
|
|
1252
|
+
const filePath = `${this.workDir}/${entry.path}`;
|
|
1253
|
+
if (!await this.layer.exists(filePath)) {
|
|
1254
|
+
const oldContent = await this.readBlob(entry.oid);
|
|
1255
|
+
files.push(
|
|
1256
|
+
this.createDiffFile(entry.path, void 0, "deleted", oldContent, "", contextLines)
|
|
1257
|
+
);
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
const workingContent = decode(await this.layer.readFile(filePath));
|
|
1261
|
+
const indexContent = await this.readBlob(entry.oid);
|
|
1262
|
+
if (workingContent !== indexContent) {
|
|
1263
|
+
files.push(
|
|
1264
|
+
this.createDiffFile(
|
|
1265
|
+
entry.path,
|
|
1266
|
+
void 0,
|
|
1267
|
+
"modified",
|
|
1268
|
+
indexContent,
|
|
1269
|
+
workingContent,
|
|
1270
|
+
contextLines
|
|
1271
|
+
)
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return { files };
|
|
1276
|
+
}
|
|
1277
|
+
// ---- Index vs HEAD (cached/staged) ------------------------------------
|
|
1278
|
+
async diffCached(path, contextLines) {
|
|
1279
|
+
const files = [];
|
|
1280
|
+
const headTree = await this.getHeadTree();
|
|
1281
|
+
for (const entry of this.indexFile.entries) {
|
|
1282
|
+
if (entry.stage !== 0) continue;
|
|
1283
|
+
if (path && !entry.path.startsWith(path)) continue;
|
|
1284
|
+
const headOid = headTree.get(entry.path);
|
|
1285
|
+
if (!headOid) {
|
|
1286
|
+
const content = await this.readBlob(entry.oid);
|
|
1287
|
+
files.push(
|
|
1288
|
+
this.createDiffFile(entry.path, void 0, "added", "", content, contextLines)
|
|
1289
|
+
);
|
|
1290
|
+
} else if (headOid !== entry.oid) {
|
|
1291
|
+
const oldContent = await this.readBlob(headOid);
|
|
1292
|
+
const newContent = await this.readBlob(entry.oid);
|
|
1293
|
+
files.push(
|
|
1294
|
+
this.createDiffFile(
|
|
1295
|
+
entry.path,
|
|
1296
|
+
void 0,
|
|
1297
|
+
"modified",
|
|
1298
|
+
oldContent,
|
|
1299
|
+
newContent,
|
|
1300
|
+
contextLines
|
|
1301
|
+
)
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const [filePath, oid] of headTree) {
|
|
1306
|
+
if (path && !filePath.startsWith(path)) continue;
|
|
1307
|
+
if (!this.indexFile.getEntry(filePath)) {
|
|
1308
|
+
const content = await this.readBlob(oid);
|
|
1309
|
+
files.push(
|
|
1310
|
+
this.createDiffFile(filePath, void 0, "deleted", content, "", contextLines)
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return { files };
|
|
1315
|
+
}
|
|
1316
|
+
// ---- Ref vs Ref -------------------------------------------------------
|
|
1317
|
+
async diffRefs(fromRef, toRef, path) {
|
|
1318
|
+
const fromTree = await this.getTreeForRef(fromRef);
|
|
1319
|
+
const toTree = await this.getTreeForRef(toRef);
|
|
1320
|
+
const files = [];
|
|
1321
|
+
for (const [filePath, oid] of toTree) {
|
|
1322
|
+
if (path && !filePath.startsWith(path)) continue;
|
|
1323
|
+
const fromOid = fromTree.get(filePath);
|
|
1324
|
+
if (!fromOid) {
|
|
1325
|
+
const content = await this.readBlob(oid);
|
|
1326
|
+
files.push(this.createDiffFile(filePath, void 0, "added", "", content));
|
|
1327
|
+
} else if (fromOid !== oid) {
|
|
1328
|
+
const oldContent = await this.readBlob(fromOid);
|
|
1329
|
+
const newContent = await this.readBlob(oid);
|
|
1330
|
+
files.push(
|
|
1331
|
+
this.createDiffFile(filePath, void 0, "modified", oldContent, newContent)
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
for (const [filePath, oid] of fromTree) {
|
|
1336
|
+
if (path && !filePath.startsWith(path)) continue;
|
|
1337
|
+
if (!toTree.has(filePath)) {
|
|
1338
|
+
const content = await this.readBlob(oid);
|
|
1339
|
+
files.push(this.createDiffFile(filePath, void 0, "deleted", content, ""));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return { files };
|
|
1343
|
+
}
|
|
1344
|
+
// ---- Tree flattening ---------------------------------------------------
|
|
1345
|
+
async getHeadTree() {
|
|
1346
|
+
try {
|
|
1347
|
+
const headOid = await this.refStore.resolveRef("HEAD");
|
|
1348
|
+
return this.getTreeForCommit(headOid);
|
|
1349
|
+
} catch {
|
|
1350
|
+
return /* @__PURE__ */ new Map();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
async getTreeForRef(ref) {
|
|
1354
|
+
const oid = await this.refStore.resolveRef(ref);
|
|
1355
|
+
return this.getTreeForCommit(oid);
|
|
1356
|
+
}
|
|
1357
|
+
async getTreeForCommit(commitOid) {
|
|
1358
|
+
const obj = await this.objectDB.readObject(commitOid);
|
|
1359
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
1360
|
+
return this.flattenTree(commit.tree, "");
|
|
1361
|
+
}
|
|
1362
|
+
async flattenTree(treeOid, prefix) {
|
|
1363
|
+
const result = /* @__PURE__ */ new Map();
|
|
1364
|
+
const obj = await this.objectDB.readObject(treeOid);
|
|
1365
|
+
const entries = this.objectDB.parseTree(obj.content);
|
|
1366
|
+
for (const entry of entries) {
|
|
1367
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1368
|
+
if (entry.mode === "40000" || entry.mode === "40") {
|
|
1369
|
+
const subTree = await this.flattenTree(entry.oid, path);
|
|
1370
|
+
for (const [p, o] of subTree) {
|
|
1371
|
+
result.set(p, o);
|
|
1372
|
+
}
|
|
1373
|
+
} else {
|
|
1374
|
+
result.set(path, entry.oid);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return result;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Look up the OID of a specific path in a tree without flattening
|
|
1381
|
+
* the entire tree. Walks tree objects one path segment at a time.
|
|
1382
|
+
* Returns null if the path does not exist in the tree.
|
|
1383
|
+
*/
|
|
1384
|
+
async lookupPathInTree(treeOid, filePath) {
|
|
1385
|
+
const segments = filePath.split("/").filter(Boolean);
|
|
1386
|
+
let currentTreeOid = treeOid;
|
|
1387
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1388
|
+
const segment = segments[i];
|
|
1389
|
+
const obj = await this.objectDB.readObject(currentTreeOid);
|
|
1390
|
+
const entries = this.objectDB.parseTree(obj.content);
|
|
1391
|
+
const entry = entries.find((e) => e.name === segment);
|
|
1392
|
+
if (!entry) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
if (i === segments.length - 1) {
|
|
1396
|
+
return entry.oid;
|
|
1397
|
+
}
|
|
1398
|
+
if (entry.mode !== "40000" && entry.mode !== "40") {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
currentTreeOid = entry.oid;
|
|
1402
|
+
}
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Get the OID of a specific path in a commit's tree.
|
|
1407
|
+
* Returns null if the path does not exist.
|
|
1408
|
+
*/
|
|
1409
|
+
async getPathOidInCommit(commitOid, filePath) {
|
|
1410
|
+
const obj = await this.objectDB.readObject(commitOid);
|
|
1411
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
1412
|
+
return this.lookupPathInTree(commit.tree, filePath);
|
|
1413
|
+
}
|
|
1414
|
+
// ---- Diff computation --------------------------------------------------
|
|
1415
|
+
createDiffFile(path, oldPath, status, oldContent, newContent, contextLines) {
|
|
1416
|
+
if (isBinary(oldContent) || isBinary(newContent)) {
|
|
1417
|
+
return { path, oldPath, status, binary: true, hunks: [] };
|
|
1418
|
+
}
|
|
1419
|
+
const hunks = computeDiffHunks(oldContent, newContent, contextLines);
|
|
1420
|
+
return { path, oldPath, status, binary: false, hunks };
|
|
1421
|
+
}
|
|
1422
|
+
async readBlob(oid) {
|
|
1423
|
+
const obj = await this.objectDB.readObject(oid);
|
|
1424
|
+
return decode(obj.content);
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
function isBinary(content) {
|
|
1428
|
+
for (let i = 0; i < Math.min(content.length, 8e3); i++) {
|
|
1429
|
+
if (content.charCodeAt(i) === 0) return true;
|
|
1430
|
+
}
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
function myersDiff(oldLines, newLines) {
|
|
1434
|
+
const n = oldLines.length;
|
|
1435
|
+
const m = newLines.length;
|
|
1436
|
+
if (n === 0 && m === 0) return [];
|
|
1437
|
+
if (n === 0) {
|
|
1438
|
+
return newLines.map((line) => ({ type: "insert", newLine: line }));
|
|
1439
|
+
}
|
|
1440
|
+
if (m === 0) {
|
|
1441
|
+
return oldLines.map((line) => ({ type: "delete", oldLine: line }));
|
|
1442
|
+
}
|
|
1443
|
+
const max = n + m;
|
|
1444
|
+
const vSize = 2 * max + 1;
|
|
1445
|
+
const v = new Array(vSize).fill(0);
|
|
1446
|
+
const trace = [];
|
|
1447
|
+
for (let d = 0; d <= max; d++) {
|
|
1448
|
+
trace.push([...v]);
|
|
1449
|
+
for (let k = -d; k <= d; k += 2) {
|
|
1450
|
+
let x;
|
|
1451
|
+
if (k === -d || k !== d && v[k - 1 + max] < v[k + 1 + max]) {
|
|
1452
|
+
x = v[k + 1 + max];
|
|
1453
|
+
} else {
|
|
1454
|
+
x = v[k - 1 + max] + 1;
|
|
1455
|
+
}
|
|
1456
|
+
let y = x - k;
|
|
1457
|
+
while (x < n && y < m && oldLines[x] === newLines[y]) {
|
|
1458
|
+
x++;
|
|
1459
|
+
y++;
|
|
1460
|
+
}
|
|
1461
|
+
v[k + max] = x;
|
|
1462
|
+
if (x >= n && y >= m) {
|
|
1463
|
+
return backtrack(trace, oldLines, newLines, d, max);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return [
|
|
1468
|
+
...oldLines.map((line) => ({ type: "delete", oldLine: line })),
|
|
1469
|
+
...newLines.map((line) => ({ type: "insert", newLine: line }))
|
|
1470
|
+
];
|
|
1471
|
+
}
|
|
1472
|
+
function backtrack(trace, oldLines, newLines, d, max) {
|
|
1473
|
+
const ops = [];
|
|
1474
|
+
let x = oldLines.length;
|
|
1475
|
+
let y = newLines.length;
|
|
1476
|
+
for (let step = d; step > 0; step--) {
|
|
1477
|
+
const v = trace[step - 1];
|
|
1478
|
+
const k = x - y;
|
|
1479
|
+
let prevK;
|
|
1480
|
+
if (k === -step || k !== step && v[k - 1 + max] < v[k + 1 + max]) {
|
|
1481
|
+
prevK = k + 1;
|
|
1482
|
+
} else {
|
|
1483
|
+
prevK = k - 1;
|
|
1484
|
+
}
|
|
1485
|
+
const prevX = v[prevK + max];
|
|
1486
|
+
const prevY = prevX - prevK;
|
|
1487
|
+
while (x > prevX + (prevK < k ? 1 : 0) && y > prevY + (prevK < k ? 0 : 1)) {
|
|
1488
|
+
x--;
|
|
1489
|
+
y--;
|
|
1490
|
+
ops.push({ type: "equal", oldLine: oldLines[x], newLine: newLines[y] });
|
|
1491
|
+
}
|
|
1492
|
+
if (step > 0) {
|
|
1493
|
+
if (prevK < k) {
|
|
1494
|
+
x--;
|
|
1495
|
+
ops.push({ type: "delete", oldLine: oldLines[x] });
|
|
1496
|
+
} else {
|
|
1497
|
+
y--;
|
|
1498
|
+
ops.push({ type: "insert", newLine: newLines[y] });
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
while (x > 0 && y > 0) {
|
|
1503
|
+
x--;
|
|
1504
|
+
y--;
|
|
1505
|
+
ops.push({ type: "equal", oldLine: oldLines[x], newLine: newLines[y] });
|
|
1506
|
+
}
|
|
1507
|
+
ops.reverse();
|
|
1508
|
+
return ops;
|
|
1509
|
+
}
|
|
1510
|
+
function computeDiffHunks(oldContent, newContent, contextLines = 3) {
|
|
1511
|
+
const oldLines = oldContent ? oldContent.split("\n") : [];
|
|
1512
|
+
const newLines = newContent ? newContent.split("\n") : [];
|
|
1513
|
+
const ops = myersDiff(oldLines, newLines);
|
|
1514
|
+
if (ops.length === 0) return [];
|
|
1515
|
+
const diffLines = [];
|
|
1516
|
+
let oldLineNum = 1;
|
|
1517
|
+
let newLineNum = 1;
|
|
1518
|
+
for (const op of ops) {
|
|
1519
|
+
switch (op.type) {
|
|
1520
|
+
case "equal":
|
|
1521
|
+
diffLines.push({
|
|
1522
|
+
origin: " ",
|
|
1523
|
+
content: op.oldLine,
|
|
1524
|
+
oldLineNumber: oldLineNum,
|
|
1525
|
+
newLineNumber: newLineNum
|
|
1526
|
+
});
|
|
1527
|
+
oldLineNum++;
|
|
1528
|
+
newLineNum++;
|
|
1529
|
+
break;
|
|
1530
|
+
case "delete":
|
|
1531
|
+
diffLines.push({
|
|
1532
|
+
origin: "-",
|
|
1533
|
+
content: op.oldLine,
|
|
1534
|
+
oldLineNumber: oldLineNum
|
|
1535
|
+
});
|
|
1536
|
+
oldLineNum++;
|
|
1537
|
+
break;
|
|
1538
|
+
case "insert":
|
|
1539
|
+
diffLines.push({
|
|
1540
|
+
origin: "+",
|
|
1541
|
+
content: op.newLine,
|
|
1542
|
+
newLineNumber: newLineNum
|
|
1543
|
+
});
|
|
1544
|
+
newLineNum++;
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return groupIntoHunks(diffLines, contextLines);
|
|
1549
|
+
}
|
|
1550
|
+
function groupIntoHunks(allLines, contextLines) {
|
|
1551
|
+
const changed = [];
|
|
1552
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
1553
|
+
if (allLines[i].origin !== " ") {
|
|
1554
|
+
changed.push(i);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (changed.length === 0) return [];
|
|
1558
|
+
const hunks = [];
|
|
1559
|
+
let hunkStart = Math.max(0, changed[0] - contextLines);
|
|
1560
|
+
let hunkEnd = Math.min(allLines.length - 1, changed[0] + contextLines);
|
|
1561
|
+
for (let i = 1; i < changed.length; i++) {
|
|
1562
|
+
const start = Math.max(0, changed[i] - contextLines);
|
|
1563
|
+
const end = Math.min(allLines.length - 1, changed[i] + contextLines);
|
|
1564
|
+
if (start <= hunkEnd + 1) {
|
|
1565
|
+
hunkEnd = end;
|
|
1566
|
+
} else {
|
|
1567
|
+
hunks.push(buildHunk(allLines, hunkStart, hunkEnd));
|
|
1568
|
+
hunkStart = start;
|
|
1569
|
+
hunkEnd = end;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
hunks.push(buildHunk(allLines, hunkStart, hunkEnd));
|
|
1573
|
+
return hunks;
|
|
1574
|
+
}
|
|
1575
|
+
function buildHunk(allLines, start, end) {
|
|
1576
|
+
const lines = allLines.slice(start, end + 1);
|
|
1577
|
+
let oldStart = 0;
|
|
1578
|
+
let oldCount = 0;
|
|
1579
|
+
let newStart = 0;
|
|
1580
|
+
let newCount = 0;
|
|
1581
|
+
for (const line of lines) {
|
|
1582
|
+
if (line.origin === " ") {
|
|
1583
|
+
if (oldStart === 0 && line.oldLineNumber) oldStart = line.oldLineNumber;
|
|
1584
|
+
if (newStart === 0 && line.newLineNumber) newStart = line.newLineNumber;
|
|
1585
|
+
oldCount++;
|
|
1586
|
+
newCount++;
|
|
1587
|
+
} else if (line.origin === "-") {
|
|
1588
|
+
if (oldStart === 0 && line.oldLineNumber) oldStart = line.oldLineNumber;
|
|
1589
|
+
oldCount++;
|
|
1590
|
+
} else if (line.origin === "+") {
|
|
1591
|
+
if (newStart === 0 && line.newLineNumber) newStart = line.newLineNumber;
|
|
1592
|
+
newCount++;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (oldStart === 0) oldStart = 1;
|
|
1596
|
+
if (newStart === 0) newStart = 1;
|
|
1597
|
+
const header = `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`;
|
|
1598
|
+
return {
|
|
1599
|
+
oldStart,
|
|
1600
|
+
oldLines: oldCount,
|
|
1601
|
+
newStart,
|
|
1602
|
+
newLines: newCount,
|
|
1603
|
+
header,
|
|
1604
|
+
lines
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/merge-engine.ts
|
|
1609
|
+
init_utils();
|
|
1610
|
+
var MergeEngine = class {
|
|
1611
|
+
constructor(layer, workDir, gitDir, objectDB, indexFile, refStore, diffEngine) {
|
|
1612
|
+
this.layer = layer;
|
|
1613
|
+
this.workDir = workDir;
|
|
1614
|
+
this.gitDir = gitDir;
|
|
1615
|
+
this.objectDB = objectDB;
|
|
1616
|
+
this.indexFile = indexFile;
|
|
1617
|
+
this.refStore = refStore;
|
|
1618
|
+
this.diffEngine = diffEngine;
|
|
1619
|
+
}
|
|
1620
|
+
// ---- Find merge base ---------------------------------------------------
|
|
1621
|
+
async findMergeBase(oid1, oid2) {
|
|
1622
|
+
const ancestors1 = /* @__PURE__ */ new Set();
|
|
1623
|
+
const queue1 = [oid1];
|
|
1624
|
+
while (queue1.length > 0) {
|
|
1625
|
+
const current = queue1.shift();
|
|
1626
|
+
if (ancestors1.has(current)) continue;
|
|
1627
|
+
ancestors1.add(current);
|
|
1628
|
+
try {
|
|
1629
|
+
const obj = await this.objectDB.readObject(current);
|
|
1630
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
1631
|
+
for (const parent of commit.parents) {
|
|
1632
|
+
queue1.push(parent);
|
|
1633
|
+
}
|
|
1634
|
+
} catch {
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const visited2 = /* @__PURE__ */ new Set();
|
|
1638
|
+
const queue2 = [oid2];
|
|
1639
|
+
while (queue2.length > 0) {
|
|
1640
|
+
const current = queue2.shift();
|
|
1641
|
+
if (visited2.has(current)) continue;
|
|
1642
|
+
visited2.add(current);
|
|
1643
|
+
if (ancestors1.has(current)) {
|
|
1644
|
+
return current;
|
|
1645
|
+
}
|
|
1646
|
+
try {
|
|
1647
|
+
const obj = await this.objectDB.readObject(current);
|
|
1648
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
1649
|
+
for (const parent of commit.parents) {
|
|
1650
|
+
queue2.push(parent);
|
|
1651
|
+
}
|
|
1652
|
+
} catch {
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
// ---- Three-way merge --------------------------------------------------
|
|
1658
|
+
async merge(oursOid, theirsOid, message, author, committer) {
|
|
1659
|
+
const base = await this.findMergeBase(oursOid, theirsOid);
|
|
1660
|
+
if (base === theirsOid) {
|
|
1661
|
+
return { type: "already-up-to-date", oid: oursOid, conflicts: [] };
|
|
1662
|
+
}
|
|
1663
|
+
if (base === oursOid) {
|
|
1664
|
+
return {
|
|
1665
|
+
type: "fast-forward",
|
|
1666
|
+
oid: theirsOid,
|
|
1667
|
+
conflicts: []
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
const baseTree = base ? await this.diffEngine.getTreeForCommit(base) : /* @__PURE__ */ new Map();
|
|
1671
|
+
const oursTree = await this.diffEngine.getTreeForCommit(oursOid);
|
|
1672
|
+
const theirsTree = await this.diffEngine.getTreeForCommit(theirsOid);
|
|
1673
|
+
const conflicts = [];
|
|
1674
|
+
const mergedFiles = /* @__PURE__ */ new Map();
|
|
1675
|
+
const allPaths = /* @__PURE__ */ new Set();
|
|
1676
|
+
for (const path of baseTree.keys()) allPaths.add(path);
|
|
1677
|
+
for (const path of oursTree.keys()) allPaths.add(path);
|
|
1678
|
+
for (const path of theirsTree.keys()) allPaths.add(path);
|
|
1679
|
+
for (const path of allPaths) {
|
|
1680
|
+
const baseOid = baseTree.get(path);
|
|
1681
|
+
const oursOidFile = oursTree.get(path);
|
|
1682
|
+
const theirsOidFile = theirsTree.get(path);
|
|
1683
|
+
if (oursOidFile === theirsOidFile) {
|
|
1684
|
+
if (oursOidFile) mergedFiles.set(path, oursOidFile);
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (oursOidFile === baseOid) {
|
|
1688
|
+
if (theirsOidFile) {
|
|
1689
|
+
mergedFiles.set(path, theirsOidFile);
|
|
1690
|
+
}
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (theirsOidFile === baseOid) {
|
|
1694
|
+
if (oursOidFile) {
|
|
1695
|
+
mergedFiles.set(path, oursOidFile);
|
|
1696
|
+
}
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (oursOidFile && theirsOidFile) {
|
|
1700
|
+
const baseContent = baseOid ? await this.readBlobText(baseOid) : "";
|
|
1701
|
+
const oursContent = await this.readBlobText(oursOidFile);
|
|
1702
|
+
const theirsContent = await this.readBlobText(theirsOidFile);
|
|
1703
|
+
if (isBinary2(baseContent) || isBinary2(oursContent) || isBinary2(theirsContent)) {
|
|
1704
|
+
conflicts.push(path);
|
|
1705
|
+
mergedFiles.set(path, oursOidFile);
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const result = threeWayMergeText(
|
|
1709
|
+
baseContent,
|
|
1710
|
+
oursContent,
|
|
1711
|
+
theirsContent
|
|
1712
|
+
);
|
|
1713
|
+
if (result.conflict) {
|
|
1714
|
+
conflicts.push(path);
|
|
1715
|
+
const conflictContent = encode(result.content);
|
|
1716
|
+
await this.layer.writeFile(
|
|
1717
|
+
`${this.workDir}/${path}`,
|
|
1718
|
+
conflictContent
|
|
1719
|
+
);
|
|
1720
|
+
if (baseOid) {
|
|
1721
|
+
this.addConflictEntry(path, baseOid, 1);
|
|
1722
|
+
}
|
|
1723
|
+
this.addConflictEntry(path, oursOidFile, 2);
|
|
1724
|
+
this.addConflictEntry(path, theirsOidFile, 3);
|
|
1725
|
+
this.indexFile.entries = this.indexFile.entries.filter(
|
|
1726
|
+
(e) => !(e.path === path && e.stage === 0)
|
|
1727
|
+
);
|
|
1728
|
+
} else {
|
|
1729
|
+
const mergedOid = await this.objectDB.writeBlob(
|
|
1730
|
+
encode(result.content)
|
|
1731
|
+
);
|
|
1732
|
+
mergedFiles.set(path, mergedOid);
|
|
1733
|
+
}
|
|
1734
|
+
} else if (!oursOidFile && !theirsOidFile) {
|
|
1735
|
+
} else {
|
|
1736
|
+
conflicts.push(path);
|
|
1737
|
+
if (oursOidFile) {
|
|
1738
|
+
mergedFiles.set(path, oursOidFile);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (conflicts.length > 0) {
|
|
1743
|
+
for (const [path, oid] of mergedFiles) {
|
|
1744
|
+
if (!conflicts.includes(path)) {
|
|
1745
|
+
this.indexFile.addEntry(
|
|
1746
|
+
this.createIndexEntry(path, oid, 33188)
|
|
1747
|
+
);
|
|
1748
|
+
const obj = await this.objectDB.readObject(oid);
|
|
1749
|
+
await this.layer.writeFile(
|
|
1750
|
+
`${this.workDir}/${path}`,
|
|
1751
|
+
obj.content
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
await this.layer.writeFile(
|
|
1756
|
+
`${this.gitDir}/MERGE_HEAD`,
|
|
1757
|
+
encode(`${theirsOid}
|
|
1758
|
+
`)
|
|
1759
|
+
);
|
|
1760
|
+
await this.layer.writeFile(
|
|
1761
|
+
`${this.gitDir}/MERGE_MSG`,
|
|
1762
|
+
encode(message + "\n")
|
|
1763
|
+
);
|
|
1764
|
+
await this.indexFile.write();
|
|
1765
|
+
return { type: "merge-commit", conflicts, oid: void 0 };
|
|
1766
|
+
}
|
|
1767
|
+
const treeOid = await this.buildTreeFromPaths(mergedFiles);
|
|
1768
|
+
this.indexFile.entries = [];
|
|
1769
|
+
for (const [path, oid] of mergedFiles) {
|
|
1770
|
+
this.indexFile.addEntry(this.createIndexEntry(path, oid, 33188));
|
|
1771
|
+
}
|
|
1772
|
+
const mergeCommitOid = await this.objectDB.writeCommit(
|
|
1773
|
+
treeOid,
|
|
1774
|
+
[oursOid, theirsOid],
|
|
1775
|
+
author,
|
|
1776
|
+
committer,
|
|
1777
|
+
message
|
|
1778
|
+
);
|
|
1779
|
+
for (const [path, oid] of mergedFiles) {
|
|
1780
|
+
const obj = await this.objectDB.readObject(oid);
|
|
1781
|
+
const dir = path.includes("/") ? `${this.workDir}/${path.substring(0, path.lastIndexOf("/"))}` : this.workDir;
|
|
1782
|
+
await this.layer.mkdir(dir, { recursive: true });
|
|
1783
|
+
await this.layer.writeFile(`${this.workDir}/${path}`, obj.content);
|
|
1784
|
+
}
|
|
1785
|
+
await this.indexFile.write();
|
|
1786
|
+
return {
|
|
1787
|
+
type: "merge-commit",
|
|
1788
|
+
oid: mergeCommitOid,
|
|
1789
|
+
conflicts: []
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
// ---- Build tree from flat path->oid map --------------------------------
|
|
1793
|
+
async buildTreeFromPaths(paths) {
|
|
1794
|
+
const root = { entries: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map() };
|
|
1795
|
+
for (const [path, oid] of paths) {
|
|
1796
|
+
const parts = path.split("/");
|
|
1797
|
+
let node = root;
|
|
1798
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1799
|
+
if (!node.children.has(parts[i])) {
|
|
1800
|
+
node.children.set(parts[i], { entries: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map() });
|
|
1801
|
+
}
|
|
1802
|
+
node = node.children.get(parts[i]);
|
|
1803
|
+
}
|
|
1804
|
+
const fileName = parts[parts.length - 1];
|
|
1805
|
+
node.entries.set(fileName, { oid, mode: "100644" });
|
|
1806
|
+
}
|
|
1807
|
+
return this.writeTreeNode(root);
|
|
1808
|
+
}
|
|
1809
|
+
async writeTreeNode(node) {
|
|
1810
|
+
const treeEntries = [];
|
|
1811
|
+
for (const [name, { oid, mode }] of node.entries) {
|
|
1812
|
+
treeEntries.push({ mode, name, oid });
|
|
1813
|
+
}
|
|
1814
|
+
for (const [name, child] of node.children) {
|
|
1815
|
+
const childOid = await this.writeTreeNode(child);
|
|
1816
|
+
treeEntries.push({ mode: "40000", name, oid: childOid });
|
|
1817
|
+
}
|
|
1818
|
+
return this.objectDB.writeTree(treeEntries);
|
|
1819
|
+
}
|
|
1820
|
+
// ---- Helpers -----------------------------------------------------------
|
|
1821
|
+
async readBlobText(oid) {
|
|
1822
|
+
const obj = await this.objectDB.readObject(oid);
|
|
1823
|
+
return decode(obj.content);
|
|
1824
|
+
}
|
|
1825
|
+
addConflictEntry(path, oid, stage) {
|
|
1826
|
+
this.indexFile.addEntry(
|
|
1827
|
+
this.createIndexEntry(path, oid, 33188, stage)
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
createIndexEntry(path, oid, mode, stage = 0) {
|
|
1831
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1832
|
+
return {
|
|
1833
|
+
ctimeSeconds: now,
|
|
1834
|
+
ctimeNanoseconds: 0,
|
|
1835
|
+
mtimeSeconds: now,
|
|
1836
|
+
mtimeNanoseconds: 0,
|
|
1837
|
+
dev: 0,
|
|
1838
|
+
ino: 0,
|
|
1839
|
+
mode,
|
|
1840
|
+
uid: 0,
|
|
1841
|
+
gid: 0,
|
|
1842
|
+
size: 0,
|
|
1843
|
+
oid,
|
|
1844
|
+
flags: 0,
|
|
1845
|
+
path,
|
|
1846
|
+
stage
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
function threeWayMergeText(base, ours, theirs) {
|
|
1851
|
+
const baseLines = base.split("\n");
|
|
1852
|
+
const oursLines = ours.split("\n");
|
|
1853
|
+
const theirsLines = theirs.split("\n");
|
|
1854
|
+
const oursChanges = computeLineChanges(baseLines, oursLines);
|
|
1855
|
+
const theirsChanges = computeLineChanges(baseLines, theirsLines);
|
|
1856
|
+
const resultLines = [];
|
|
1857
|
+
let hasConflict = false;
|
|
1858
|
+
let baseIdx = 0;
|
|
1859
|
+
let oursIdx = 0;
|
|
1860
|
+
let theirsIdx = 0;
|
|
1861
|
+
while (baseIdx < baseLines.length || oursIdx < oursLines.length || theirsIdx < theirsLines.length) {
|
|
1862
|
+
const oursChange = oursChanges.get(baseIdx);
|
|
1863
|
+
const theirsChange = theirsChanges.get(baseIdx);
|
|
1864
|
+
if (!oursChange && !theirsChange) {
|
|
1865
|
+
if (baseIdx < baseLines.length) {
|
|
1866
|
+
resultLines.push(baseLines[baseIdx]);
|
|
1867
|
+
baseIdx++;
|
|
1868
|
+
oursIdx++;
|
|
1869
|
+
theirsIdx++;
|
|
1870
|
+
} else {
|
|
1871
|
+
break;
|
|
1872
|
+
}
|
|
1873
|
+
} else if (oursChange && !theirsChange) {
|
|
1874
|
+
for (const line of oursChange.newLines) {
|
|
1875
|
+
resultLines.push(line);
|
|
1876
|
+
}
|
|
1877
|
+
baseIdx += oursChange.baseCount;
|
|
1878
|
+
oursIdx += oursChange.newLines.length;
|
|
1879
|
+
theirsIdx += oursChange.baseCount;
|
|
1880
|
+
} else if (!oursChange && theirsChange) {
|
|
1881
|
+
for (const line of theirsChange.newLines) {
|
|
1882
|
+
resultLines.push(line);
|
|
1883
|
+
}
|
|
1884
|
+
baseIdx += theirsChange.baseCount;
|
|
1885
|
+
oursIdx += theirsChange.baseCount;
|
|
1886
|
+
theirsIdx += theirsChange.newLines.length;
|
|
1887
|
+
} else {
|
|
1888
|
+
if (oursChange.newLines.join("\n") === theirsChange.newLines.join("\n")) {
|
|
1889
|
+
for (const line of oursChange.newLines) {
|
|
1890
|
+
resultLines.push(line);
|
|
1891
|
+
}
|
|
1892
|
+
baseIdx += oursChange.baseCount;
|
|
1893
|
+
oursIdx += oursChange.newLines.length;
|
|
1894
|
+
theirsIdx += theirsChange.newLines.length;
|
|
1895
|
+
} else {
|
|
1896
|
+
hasConflict = true;
|
|
1897
|
+
resultLines.push("<<<<<<< ours");
|
|
1898
|
+
for (const line of oursChange.newLines) {
|
|
1899
|
+
resultLines.push(line);
|
|
1900
|
+
}
|
|
1901
|
+
resultLines.push("=======");
|
|
1902
|
+
for (const line of theirsChange.newLines) {
|
|
1903
|
+
resultLines.push(line);
|
|
1904
|
+
}
|
|
1905
|
+
resultLines.push(">>>>>>> theirs");
|
|
1906
|
+
baseIdx += Math.max(oursChange.baseCount, theirsChange.baseCount);
|
|
1907
|
+
oursIdx += oursChange.newLines.length;
|
|
1908
|
+
theirsIdx += theirsChange.newLines.length;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return { content: resultLines.join("\n"), conflict: hasConflict };
|
|
1913
|
+
}
|
|
1914
|
+
function computeLineChanges(baseLines, modifiedLines) {
|
|
1915
|
+
const changes = /* @__PURE__ */ new Map();
|
|
1916
|
+
let baseIdx = 0;
|
|
1917
|
+
let modIdx = 0;
|
|
1918
|
+
while (baseIdx < baseLines.length || modIdx < modifiedLines.length) {
|
|
1919
|
+
if (baseIdx < baseLines.length && modIdx < modifiedLines.length && baseLines[baseIdx] === modifiedLines[modIdx]) {
|
|
1920
|
+
baseIdx++;
|
|
1921
|
+
modIdx++;
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
let foundBase = -1;
|
|
1925
|
+
let foundMod = -1;
|
|
1926
|
+
for (let ahead = 1; ahead < 20; ahead++) {
|
|
1927
|
+
if (foundBase < 0 && baseIdx + ahead < baseLines.length) {
|
|
1928
|
+
if (modIdx < modifiedLines.length && baseLines[baseIdx + ahead] === modifiedLines[modIdx]) {
|
|
1929
|
+
foundBase = baseIdx + ahead;
|
|
1930
|
+
foundMod = modIdx;
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
if (foundMod < 0 && modIdx + ahead < modifiedLines.length) {
|
|
1935
|
+
if (baseIdx < baseLines.length && modifiedLines[modIdx + ahead] === baseLines[baseIdx]) {
|
|
1936
|
+
foundBase = baseIdx;
|
|
1937
|
+
foundMod = modIdx + ahead;
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (foundBase >= 0 && foundMod >= 0) {
|
|
1943
|
+
const baseCount = foundBase - baseIdx;
|
|
1944
|
+
const newLines = modifiedLines.slice(modIdx, foundMod);
|
|
1945
|
+
if (baseCount > 0 || newLines.length > 0) {
|
|
1946
|
+
changes.set(baseIdx, { baseStart: baseIdx, baseCount, newLines });
|
|
1947
|
+
}
|
|
1948
|
+
baseIdx = foundBase;
|
|
1949
|
+
modIdx = foundMod;
|
|
1950
|
+
} else {
|
|
1951
|
+
const newLines = modifiedLines.slice(modIdx);
|
|
1952
|
+
const baseCount = baseLines.length - baseIdx;
|
|
1953
|
+
changes.set(baseIdx, { baseStart: baseIdx, baseCount, newLines });
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return changes;
|
|
1958
|
+
}
|
|
1959
|
+
function isBinary2(content) {
|
|
1960
|
+
for (let i = 0; i < Math.min(content.length, 8e3); i++) {
|
|
1961
|
+
if (content.charCodeAt(i) === 0) return true;
|
|
1962
|
+
}
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// src/stash.ts
|
|
1967
|
+
init_utils();
|
|
1968
|
+
var StashManager = class {
|
|
1969
|
+
constructor(layer, workDir, gitDir, objectDB, indexFile, refStore, diffEngine, mergeEngine) {
|
|
1970
|
+
this.layer = layer;
|
|
1971
|
+
this.workDir = workDir;
|
|
1972
|
+
this.gitDir = gitDir;
|
|
1973
|
+
this.objectDB = objectDB;
|
|
1974
|
+
this.indexFile = indexFile;
|
|
1975
|
+
this.refStore = refStore;
|
|
1976
|
+
this.diffEngine = diffEngine;
|
|
1977
|
+
this.mergeEngine = mergeEngine;
|
|
1978
|
+
}
|
|
1979
|
+
// ---- Push stash --------------------------------------------------------
|
|
1980
|
+
async push(message, author, includeUntracked = false) {
|
|
1981
|
+
const headOid = await this.refStore.resolveRef("HEAD");
|
|
1982
|
+
const indexTree = await this.buildIndexTree();
|
|
1983
|
+
const workingTree = await this.buildWorkingTree(includeUntracked);
|
|
1984
|
+
const indexCommitOid = await this.objectDB.writeCommit(
|
|
1985
|
+
indexTree,
|
|
1986
|
+
[headOid],
|
|
1987
|
+
author,
|
|
1988
|
+
author,
|
|
1989
|
+
`index on ${message}`
|
|
1990
|
+
);
|
|
1991
|
+
const parents = [headOid, indexCommitOid];
|
|
1992
|
+
const stashCommitOid = await this.objectDB.writeCommit(
|
|
1993
|
+
workingTree,
|
|
1994
|
+
parents,
|
|
1995
|
+
author,
|
|
1996
|
+
author,
|
|
1997
|
+
message || `WIP on ${await this.getCurrentBranchName()}`
|
|
1998
|
+
);
|
|
1999
|
+
await this.pushStashRef(stashCommitOid, message);
|
|
2000
|
+
await this.resetToHead(headOid);
|
|
2001
|
+
return stashCommitOid;
|
|
2002
|
+
}
|
|
2003
|
+
// ---- Apply stash -------------------------------------------------------
|
|
2004
|
+
async apply(index = 0) {
|
|
2005
|
+
const stashes = await this.list();
|
|
2006
|
+
if (index >= stashes.length) {
|
|
2007
|
+
throw new GitError("NOT_FOUND", `Stash @{${index}} not found`);
|
|
2008
|
+
}
|
|
2009
|
+
const stashOid = stashes[index].oid;
|
|
2010
|
+
const stashObj = await this.objectDB.readObject(stashOid);
|
|
2011
|
+
const stashCommit = this.objectDB.parseCommit(stashObj.content);
|
|
2012
|
+
const stashedTree = await this.diffEngine.flattenTree(
|
|
2013
|
+
stashCommit.tree,
|
|
2014
|
+
""
|
|
2015
|
+
);
|
|
2016
|
+
for (const [path, oid] of stashedTree) {
|
|
2017
|
+
const obj = await this.objectDB.readObject(oid);
|
|
2018
|
+
const dir = path.includes("/") ? `${this.workDir}/${path.substring(0, path.lastIndexOf("/"))}` : this.workDir;
|
|
2019
|
+
await this.layer.mkdir(dir, { recursive: true });
|
|
2020
|
+
await this.layer.writeFile(`${this.workDir}/${path}`, obj.content);
|
|
2021
|
+
}
|
|
2022
|
+
await this.indexFile.read();
|
|
2023
|
+
for (const [path, oid] of stashedTree) {
|
|
2024
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2025
|
+
this.indexFile.addEntry({
|
|
2026
|
+
ctimeSeconds: now,
|
|
2027
|
+
ctimeNanoseconds: 0,
|
|
2028
|
+
mtimeSeconds: now,
|
|
2029
|
+
mtimeNanoseconds: 0,
|
|
2030
|
+
dev: 0,
|
|
2031
|
+
ino: 0,
|
|
2032
|
+
mode: 33188,
|
|
2033
|
+
uid: 0,
|
|
2034
|
+
gid: 0,
|
|
2035
|
+
size: 0,
|
|
2036
|
+
oid,
|
|
2037
|
+
flags: 0,
|
|
2038
|
+
path,
|
|
2039
|
+
stage: 0
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
await this.indexFile.write();
|
|
2043
|
+
}
|
|
2044
|
+
// ---- Pop stash ---------------------------------------------------------
|
|
2045
|
+
async pop(index = 0) {
|
|
2046
|
+
await this.apply(index);
|
|
2047
|
+
await this.drop(index);
|
|
2048
|
+
}
|
|
2049
|
+
// ---- Drop stash --------------------------------------------------------
|
|
2050
|
+
async drop(index = 0) {
|
|
2051
|
+
const stashes = await this.list();
|
|
2052
|
+
if (index >= stashes.length) {
|
|
2053
|
+
throw new GitError("NOT_FOUND", `Stash @{${index}} not found`);
|
|
2054
|
+
}
|
|
2055
|
+
stashes.splice(index, 1);
|
|
2056
|
+
await this.writeStashList(stashes);
|
|
2057
|
+
}
|
|
2058
|
+
// ---- List stashes ------------------------------------------------------
|
|
2059
|
+
async list() {
|
|
2060
|
+
const stashListPath = `${this.gitDir}/refs/stash-list`;
|
|
2061
|
+
if (!await this.layer.exists(stashListPath)) {
|
|
2062
|
+
return [];
|
|
2063
|
+
}
|
|
2064
|
+
const content = decode(await this.layer.readFile(stashListPath));
|
|
2065
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
2066
|
+
return lines.map((line, index) => {
|
|
2067
|
+
const [oid, ...messageParts] = line.split(" ");
|
|
2068
|
+
return {
|
|
2069
|
+
index,
|
|
2070
|
+
oid,
|
|
2071
|
+
message: messageParts.join(" ")
|
|
2072
|
+
};
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
// ---- Internal helpers --------------------------------------------------
|
|
2076
|
+
async pushStashRef(oid, message) {
|
|
2077
|
+
const stashes = await this.list();
|
|
2078
|
+
stashes.unshift({
|
|
2079
|
+
index: 0,
|
|
2080
|
+
oid,
|
|
2081
|
+
message: message || "WIP"
|
|
2082
|
+
});
|
|
2083
|
+
for (let i = 0; i < stashes.length; i++) {
|
|
2084
|
+
stashes[i].index = i;
|
|
2085
|
+
}
|
|
2086
|
+
await this.writeStashList(stashes);
|
|
2087
|
+
await this.refStore.writeRef("refs/stash", oid);
|
|
2088
|
+
}
|
|
2089
|
+
async writeStashList(stashes) {
|
|
2090
|
+
const stashListPath = `${this.gitDir}/refs/stash-list`;
|
|
2091
|
+
if (stashes.length === 0) {
|
|
2092
|
+
if (await this.layer.exists(stashListPath)) {
|
|
2093
|
+
await this.layer.rm(stashListPath);
|
|
2094
|
+
}
|
|
2095
|
+
try {
|
|
2096
|
+
await this.refStore.deleteRef("refs/stash");
|
|
2097
|
+
} catch {
|
|
2098
|
+
}
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
const content = stashes.map((s) => `${s.oid} ${s.message}`).join("\n") + "\n";
|
|
2102
|
+
await this.layer.mkdir(`${this.gitDir}/refs`, { recursive: true });
|
|
2103
|
+
await this.layer.writeFile(stashListPath, encode(content));
|
|
2104
|
+
await this.refStore.writeRef("refs/stash", stashes[0].oid);
|
|
2105
|
+
}
|
|
2106
|
+
async buildIndexTree() {
|
|
2107
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2108
|
+
for (const entry of this.indexFile.entries) {
|
|
2109
|
+
if (entry.stage === 0) {
|
|
2110
|
+
paths.set(entry.path, entry.oid);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return this.mergeEngine.buildTreeFromPaths(paths);
|
|
2114
|
+
}
|
|
2115
|
+
async buildWorkingTree(includeUntracked) {
|
|
2116
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2117
|
+
const indexPaths = /* @__PURE__ */ new Set();
|
|
2118
|
+
for (const entry of this.indexFile.entries) {
|
|
2119
|
+
if (entry.stage === 0) {
|
|
2120
|
+
indexPaths.add(entry.path);
|
|
2121
|
+
const filePath = `${this.workDir}/${entry.path}`;
|
|
2122
|
+
if (await this.layer.exists(filePath)) {
|
|
2123
|
+
const content = await this.layer.readFile(filePath);
|
|
2124
|
+
const oid = await this.objectDB.writeBlob(content);
|
|
2125
|
+
paths.set(entry.path, oid);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
if (includeUntracked) {
|
|
2130
|
+
const untrackedPaths = /* @__PURE__ */ new Set();
|
|
2131
|
+
await this.collectWorkingTreePaths("", untrackedPaths);
|
|
2132
|
+
for (const p of untrackedPaths) {
|
|
2133
|
+
if (indexPaths.has(p)) continue;
|
|
2134
|
+
const filePath = `${this.workDir}/${p}`;
|
|
2135
|
+
const content = await this.layer.readFile(filePath);
|
|
2136
|
+
const oid = await this.objectDB.writeBlob(content);
|
|
2137
|
+
paths.set(p, oid);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return this.mergeEngine.buildTreeFromPaths(paths);
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Recursively collects all file paths in the working directory,
|
|
2144
|
+
* excluding .git directories.
|
|
2145
|
+
*/
|
|
2146
|
+
async collectWorkingTreePaths(dir, paths) {
|
|
2147
|
+
const fullDir = dir ? this.workDir === "/" ? `/${dir}` : `${this.workDir}/${dir}` : this.workDir;
|
|
2148
|
+
let entries;
|
|
2149
|
+
try {
|
|
2150
|
+
entries = await this.layer.readdir(fullDir);
|
|
2151
|
+
} catch {
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
for (const entry of entries) {
|
|
2155
|
+
const childPath = dir ? `${dir}/${entry.name}` : entry.name;
|
|
2156
|
+
if (entry.name === ".git") continue;
|
|
2157
|
+
if (entry.isDirectory()) {
|
|
2158
|
+
await this.collectWorkingTreePaths(childPath, paths);
|
|
2159
|
+
} else {
|
|
2160
|
+
paths.add(childPath);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
async resetToHead(headOid) {
|
|
2165
|
+
const headTree = await this.diffEngine.getTreeForCommit(headOid);
|
|
2166
|
+
for (const entry of this.indexFile.entries) {
|
|
2167
|
+
if (entry.stage !== 0) continue;
|
|
2168
|
+
const filePath = `${this.workDir}/${entry.path}`;
|
|
2169
|
+
if (await this.layer.exists(filePath)) {
|
|
2170
|
+
await this.layer.rm(filePath);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
this.indexFile.entries = [];
|
|
2174
|
+
for (const [path, oid] of headTree) {
|
|
2175
|
+
const obj = await this.objectDB.readObject(oid);
|
|
2176
|
+
const dir = path.includes("/") ? `${this.workDir}/${path.substring(0, path.lastIndexOf("/"))}` : this.workDir;
|
|
2177
|
+
await this.layer.mkdir(dir, { recursive: true });
|
|
2178
|
+
await this.layer.writeFile(`${this.workDir}/${path}`, obj.content);
|
|
2179
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2180
|
+
this.indexFile.addEntry({
|
|
2181
|
+
ctimeSeconds: now,
|
|
2182
|
+
ctimeNanoseconds: 0,
|
|
2183
|
+
mtimeSeconds: now,
|
|
2184
|
+
mtimeNanoseconds: 0,
|
|
2185
|
+
dev: 0,
|
|
2186
|
+
ino: 0,
|
|
2187
|
+
mode: 33188,
|
|
2188
|
+
uid: 0,
|
|
2189
|
+
gid: 0,
|
|
2190
|
+
size: 0,
|
|
2191
|
+
oid,
|
|
2192
|
+
flags: 0,
|
|
2193
|
+
path,
|
|
2194
|
+
stage: 0
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
await this.indexFile.write();
|
|
2198
|
+
}
|
|
2199
|
+
async getCurrentBranchName() {
|
|
2200
|
+
const sym = await this.refStore.readSymbolicRef("HEAD");
|
|
2201
|
+
if (sym && sym.startsWith("refs/heads/")) {
|
|
2202
|
+
return sym.substring(11);
|
|
2203
|
+
}
|
|
2204
|
+
return "HEAD";
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
// src/repository.ts
|
|
2209
|
+
init_utils();
|
|
2210
|
+
|
|
2211
|
+
// src/pack/write.ts
|
|
2212
|
+
init_utils();
|
|
2213
|
+
import pako3 from "pako";
|
|
2214
|
+
var TYPE_NUMBERS = {
|
|
2215
|
+
commit: 1,
|
|
2216
|
+
tree: 2,
|
|
2217
|
+
blob: 3,
|
|
2218
|
+
tag: 4
|
|
2219
|
+
};
|
|
2220
|
+
async function createPackfile(objects) {
|
|
2221
|
+
const parts = [];
|
|
2222
|
+
const header = new Uint8Array(12);
|
|
2223
|
+
header[0] = 80;
|
|
2224
|
+
header[1] = 65;
|
|
2225
|
+
header[2] = 67;
|
|
2226
|
+
header[3] = 75;
|
|
2227
|
+
writeUint322(header, 4, 2);
|
|
2228
|
+
writeUint322(header, 8, objects.length);
|
|
2229
|
+
parts.push(header);
|
|
2230
|
+
for (const obj of objects) {
|
|
2231
|
+
const typeNum = TYPE_NUMBERS[obj.type];
|
|
2232
|
+
if (!typeNum) throw new Error(`Unknown object type: ${obj.type}`);
|
|
2233
|
+
const objHeader = encodeTypeAndSize(typeNum, obj.content.length);
|
|
2234
|
+
parts.push(objHeader);
|
|
2235
|
+
const compressed = pako3.deflate(obj.content);
|
|
2236
|
+
parts.push(compressed);
|
|
2237
|
+
}
|
|
2238
|
+
const packWithoutChecksum = concat(...parts);
|
|
2239
|
+
const checksum = await sha1Bytes(packWithoutChecksum);
|
|
2240
|
+
return concat(packWithoutChecksum, checksum);
|
|
2241
|
+
}
|
|
2242
|
+
function encodeTypeAndSize(type, size) {
|
|
2243
|
+
const bytes = [];
|
|
2244
|
+
let byte = (type & 7) << 4 | size & 15;
|
|
2245
|
+
size >>= 4;
|
|
2246
|
+
if (size > 0) {
|
|
2247
|
+
byte |= 128;
|
|
2248
|
+
}
|
|
2249
|
+
bytes.push(byte);
|
|
2250
|
+
while (size > 0) {
|
|
2251
|
+
byte = size & 127;
|
|
2252
|
+
size >>= 7;
|
|
2253
|
+
if (size > 0) byte |= 128;
|
|
2254
|
+
bytes.push(byte);
|
|
2255
|
+
}
|
|
2256
|
+
return new Uint8Array(bytes);
|
|
2257
|
+
}
|
|
2258
|
+
function writeUint322(buf, offset, value) {
|
|
2259
|
+
buf[offset] = value >>> 24 & 255;
|
|
2260
|
+
buf[offset + 1] = value >>> 16 & 255;
|
|
2261
|
+
buf[offset + 2] = value >>> 8 & 255;
|
|
2262
|
+
buf[offset + 3] = value & 255;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/repository.ts
|
|
2266
|
+
var Repository = class {
|
|
2267
|
+
workDir;
|
|
2268
|
+
gitDir;
|
|
2269
|
+
objectDB;
|
|
2270
|
+
refStore;
|
|
2271
|
+
indexFile;
|
|
2272
|
+
config;
|
|
2273
|
+
ignore;
|
|
2274
|
+
diffEngine;
|
|
2275
|
+
mergeEngine;
|
|
2276
|
+
stashManager;
|
|
2277
|
+
transport = null;
|
|
2278
|
+
constructor(layer, workDir = "/") {
|
|
2279
|
+
this.workDir = workDir.replace(/\/$/, "") || "/";
|
|
2280
|
+
this.gitDir = this.workDir === "/" ? "/.git" : `${this.workDir}/.git`;
|
|
2281
|
+
this.objectDB = new ObjectDB(layer, this.gitDir);
|
|
2282
|
+
this.refStore = new RefStore(layer, this.gitDir);
|
|
2283
|
+
this.indexFile = new IndexFile(layer, this.gitDir);
|
|
2284
|
+
this.config = new GitConfig(layer, this.gitDir);
|
|
2285
|
+
this.ignore = new GitIgnore(layer, this.workDir);
|
|
2286
|
+
this.diffEngine = new DiffEngine(
|
|
2287
|
+
layer,
|
|
2288
|
+
this.workDir,
|
|
2289
|
+
this.objectDB,
|
|
2290
|
+
this.indexFile,
|
|
2291
|
+
this.refStore,
|
|
2292
|
+
this.gitDir
|
|
2293
|
+
);
|
|
2294
|
+
this.mergeEngine = new MergeEngine(
|
|
2295
|
+
layer,
|
|
2296
|
+
this.workDir,
|
|
2297
|
+
this.gitDir,
|
|
2298
|
+
this.objectDB,
|
|
2299
|
+
this.indexFile,
|
|
2300
|
+
this.refStore,
|
|
2301
|
+
this.diffEngine
|
|
2302
|
+
);
|
|
2303
|
+
this.stashManager = new StashManager(
|
|
2304
|
+
layer,
|
|
2305
|
+
this.workDir,
|
|
2306
|
+
this.gitDir,
|
|
2307
|
+
this.objectDB,
|
|
2308
|
+
this.indexFile,
|
|
2309
|
+
this.refStore,
|
|
2310
|
+
this.diffEngine,
|
|
2311
|
+
this.mergeEngine
|
|
2312
|
+
);
|
|
2313
|
+
this._layer = layer;
|
|
2314
|
+
}
|
|
2315
|
+
// Keep a private ref to the layer for operations that need direct FS access
|
|
2316
|
+
_layer;
|
|
2317
|
+
setTransport(transport) {
|
|
2318
|
+
this.transport = transport;
|
|
2319
|
+
}
|
|
2320
|
+
// ---- Initialization ----------------------------------------------------
|
|
2321
|
+
async load() {
|
|
2322
|
+
await this.config.load();
|
|
2323
|
+
await this.indexFile.read();
|
|
2324
|
+
await this.ignore.load();
|
|
2325
|
+
}
|
|
2326
|
+
// ======================================================================
|
|
2327
|
+
// BRANCH OPERATIONS
|
|
2328
|
+
// ======================================================================
|
|
2329
|
+
async currentBranch() {
|
|
2330
|
+
const sym = await this.refStore.readSymbolicRef("HEAD");
|
|
2331
|
+
if (!sym) return null;
|
|
2332
|
+
if (sym.startsWith("refs/heads/")) {
|
|
2333
|
+
return sym.substring(11);
|
|
2334
|
+
}
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
async createBranch(name, options = {}) {
|
|
2338
|
+
const { startPoint, force } = options;
|
|
2339
|
+
let oid;
|
|
2340
|
+
if (startPoint) {
|
|
2341
|
+
const ref = await this.refStore.expandRef(startPoint);
|
|
2342
|
+
oid = await this.refStore.resolveRef(ref);
|
|
2343
|
+
} else {
|
|
2344
|
+
oid = await this.refStore.resolveRef("HEAD");
|
|
2345
|
+
}
|
|
2346
|
+
const refPath = `refs/heads/${name}`;
|
|
2347
|
+
try {
|
|
2348
|
+
await this.refStore.resolveRef(refPath);
|
|
2349
|
+
if (!force) {
|
|
2350
|
+
throw new GitError("ALREADY_EXISTS", `Branch '${name}' already exists`);
|
|
2351
|
+
}
|
|
2352
|
+
} catch (e) {
|
|
2353
|
+
if (e instanceof GitError && e.code === "ALREADY_EXISTS") throw e;
|
|
2354
|
+
}
|
|
2355
|
+
await this.refStore.writeRef(refPath, oid);
|
|
2356
|
+
}
|
|
2357
|
+
async deleteBranch(name, options = {}) {
|
|
2358
|
+
const { force } = options;
|
|
2359
|
+
const current = await this.currentBranch();
|
|
2360
|
+
if (current === name) {
|
|
2361
|
+
throw new GitError("CURRENT_BRANCH", `Cannot delete current branch '${name}'`);
|
|
2362
|
+
}
|
|
2363
|
+
const refPath = `refs/heads/${name}`;
|
|
2364
|
+
try {
|
|
2365
|
+
await this.refStore.resolveRef(refPath);
|
|
2366
|
+
} catch {
|
|
2367
|
+
throw new GitError("NOT_FOUND", `Branch '${name}' not found`);
|
|
2368
|
+
}
|
|
2369
|
+
if (!force) {
|
|
2370
|
+
}
|
|
2371
|
+
await this.refStore.deleteRef(refPath);
|
|
2372
|
+
}
|
|
2373
|
+
async listBranches(options = {}) {
|
|
2374
|
+
const branches = [];
|
|
2375
|
+
const currentBranchName = await this.currentBranch();
|
|
2376
|
+
if (!options.remote) {
|
|
2377
|
+
const localRefs = await this.refStore.listRefs("refs/heads/");
|
|
2378
|
+
for (const { name: refName, oid } of localRefs) {
|
|
2379
|
+
const branchName = refName.substring(11);
|
|
2380
|
+
const remote = this.config.get(`branch.${branchName}.remote`);
|
|
2381
|
+
const merge = this.config.get(`branch.${branchName}.merge`);
|
|
2382
|
+
let upstream = null;
|
|
2383
|
+
if (remote && merge) {
|
|
2384
|
+
const upstreamBranch = merge.startsWith("refs/heads/") ? merge.substring(11) : merge;
|
|
2385
|
+
upstream = `${remote}/${upstreamBranch}`;
|
|
2386
|
+
}
|
|
2387
|
+
branches.push({
|
|
2388
|
+
name: branchName,
|
|
2389
|
+
current: branchName === currentBranchName,
|
|
2390
|
+
commit: oid,
|
|
2391
|
+
upstream
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (options.remote) {
|
|
2396
|
+
const remoteRefs = await this.refStore.listRefs("refs/remotes/");
|
|
2397
|
+
for (const { name: refName, oid } of remoteRefs) {
|
|
2398
|
+
const branchName = refName.substring(13);
|
|
2399
|
+
if (branchName.endsWith("/HEAD")) continue;
|
|
2400
|
+
branches.push({
|
|
2401
|
+
name: branchName,
|
|
2402
|
+
current: false,
|
|
2403
|
+
commit: oid,
|
|
2404
|
+
upstream: null
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return branches;
|
|
2409
|
+
}
|
|
2410
|
+
async renameBranch(oldName, newName) {
|
|
2411
|
+
const oldRef = `refs/heads/${oldName}`;
|
|
2412
|
+
const newRef = `refs/heads/${newName}`;
|
|
2413
|
+
let oid;
|
|
2414
|
+
try {
|
|
2415
|
+
oid = await this.refStore.resolveRef(oldRef);
|
|
2416
|
+
} catch {
|
|
2417
|
+
throw new GitError("NOT_FOUND", `Branch '${oldName}' not found`);
|
|
2418
|
+
}
|
|
2419
|
+
try {
|
|
2420
|
+
await this.refStore.resolveRef(newRef);
|
|
2421
|
+
throw new GitError("ALREADY_EXISTS", `Branch '${newName}' already exists`);
|
|
2422
|
+
} catch (e) {
|
|
2423
|
+
if (e instanceof GitError && e.code === "ALREADY_EXISTS") throw e;
|
|
2424
|
+
}
|
|
2425
|
+
await this.refStore.writeRef(newRef, oid);
|
|
2426
|
+
await this.refStore.deleteRef(oldRef);
|
|
2427
|
+
const current = await this.currentBranch();
|
|
2428
|
+
if (current === oldName) {
|
|
2429
|
+
await this.refStore.writeSymbolicRef("HEAD", newRef);
|
|
2430
|
+
}
|
|
2431
|
+
const merge = this.config.get(`branch.${oldName}.merge`);
|
|
2432
|
+
const remote = this.config.get(`branch.${oldName}.remote`);
|
|
2433
|
+
if (merge || remote) {
|
|
2434
|
+
this.config.deleteSection(`branch.${oldName}`);
|
|
2435
|
+
if (merge) this.config.set(`branch.${newName}.merge`, merge);
|
|
2436
|
+
if (remote) this.config.set(`branch.${newName}.remote`, remote);
|
|
2437
|
+
await this.config.save();
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
async checkout(ref, options = {}) {
|
|
2441
|
+
const { force, create } = options;
|
|
2442
|
+
if (create) {
|
|
2443
|
+
await this.createBranch(ref);
|
|
2444
|
+
}
|
|
2445
|
+
let targetOid;
|
|
2446
|
+
let targetRef = null;
|
|
2447
|
+
try {
|
|
2448
|
+
const expanded = await this.refStore.expandRef(ref);
|
|
2449
|
+
targetOid = await this.refStore.resolveRef(expanded);
|
|
2450
|
+
if (expanded.startsWith("refs/heads/")) {
|
|
2451
|
+
targetRef = expanded;
|
|
2452
|
+
}
|
|
2453
|
+
} catch {
|
|
2454
|
+
throw new GitError("NOT_FOUND", `Ref '${ref}' not found`);
|
|
2455
|
+
}
|
|
2456
|
+
if (!force) {
|
|
2457
|
+
}
|
|
2458
|
+
const targetTree = await this.diffEngine.getTreeForCommit(targetOid);
|
|
2459
|
+
const currentTree = await this.diffEngine.getHeadTree();
|
|
2460
|
+
for (const [path] of currentTree) {
|
|
2461
|
+
if (!targetTree.has(path)) {
|
|
2462
|
+
const filePath = this.workDir === "/" ? `/${path}` : `${this.workDir}/${path}`;
|
|
2463
|
+
if (await this._layer.exists(filePath)) {
|
|
2464
|
+
await this._layer.rm(filePath);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
for (const [path, oid] of targetTree) {
|
|
2469
|
+
const obj = await this.objectDB.readObject(oid);
|
|
2470
|
+
const filePath = this.workDir === "/" ? `/${path}` : `${this.workDir}/${path}`;
|
|
2471
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
2472
|
+
if (dir) {
|
|
2473
|
+
await this._layer.mkdir(dir, { recursive: true });
|
|
2474
|
+
}
|
|
2475
|
+
await this._layer.writeFile(filePath, obj.content);
|
|
2476
|
+
}
|
|
2477
|
+
this.indexFile.entries = [];
|
|
2478
|
+
for (const [path, oid] of targetTree) {
|
|
2479
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2480
|
+
this.indexFile.addEntry({
|
|
2481
|
+
ctimeSeconds: now,
|
|
2482
|
+
ctimeNanoseconds: 0,
|
|
2483
|
+
mtimeSeconds: now,
|
|
2484
|
+
mtimeNanoseconds: 0,
|
|
2485
|
+
dev: 0,
|
|
2486
|
+
ino: 0,
|
|
2487
|
+
mode: 33188,
|
|
2488
|
+
uid: 0,
|
|
2489
|
+
gid: 0,
|
|
2490
|
+
size: 0,
|
|
2491
|
+
oid,
|
|
2492
|
+
flags: 0,
|
|
2493
|
+
path,
|
|
2494
|
+
stage: 0
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
await this.indexFile.write();
|
|
2498
|
+
if (targetRef) {
|
|
2499
|
+
await this.refStore.writeSymbolicRef("HEAD", targetRef);
|
|
2500
|
+
} else {
|
|
2501
|
+
const headPath = `${this.gitDir}/HEAD`;
|
|
2502
|
+
await this._layer.writeFile(headPath, encode(`${targetOid}
|
|
2503
|
+
`));
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Update working tree and index to match the given commit OID,
|
|
2508
|
+
* without changing HEAD's symbolic ref. Used by merge to avoid
|
|
2509
|
+
* accidentally switching branches.
|
|
2510
|
+
*/
|
|
2511
|
+
async updateWorkingTreeToCommit(commitOid) {
|
|
2512
|
+
const targetTree = await this.diffEngine.getTreeForCommit(commitOid);
|
|
2513
|
+
const currentTree = await this.diffEngine.getHeadTree();
|
|
2514
|
+
for (const [path] of currentTree) {
|
|
2515
|
+
if (!targetTree.has(path)) {
|
|
2516
|
+
const filePath = this.workDir === "/" ? `/${path}` : `${this.workDir}/${path}`;
|
|
2517
|
+
if (await this._layer.exists(filePath)) {
|
|
2518
|
+
await this._layer.rm(filePath);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
for (const [path, oid] of targetTree) {
|
|
2523
|
+
const obj = await this.objectDB.readObject(oid);
|
|
2524
|
+
const filePath = this.workDir === "/" ? `/${path}` : `${this.workDir}/${path}`;
|
|
2525
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
2526
|
+
if (dir) {
|
|
2527
|
+
await this._layer.mkdir(dir, { recursive: true });
|
|
2528
|
+
}
|
|
2529
|
+
await this._layer.writeFile(filePath, obj.content);
|
|
2530
|
+
}
|
|
2531
|
+
this.indexFile.entries = [];
|
|
2532
|
+
for (const [path, oid] of targetTree) {
|
|
2533
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2534
|
+
this.indexFile.addEntry({
|
|
2535
|
+
ctimeSeconds: now,
|
|
2536
|
+
ctimeNanoseconds: 0,
|
|
2537
|
+
mtimeSeconds: now,
|
|
2538
|
+
mtimeNanoseconds: 0,
|
|
2539
|
+
dev: 0,
|
|
2540
|
+
ino: 0,
|
|
2541
|
+
mode: 33188,
|
|
2542
|
+
uid: 0,
|
|
2543
|
+
gid: 0,
|
|
2544
|
+
size: 0,
|
|
2545
|
+
oid,
|
|
2546
|
+
flags: 0,
|
|
2547
|
+
path,
|
|
2548
|
+
stage: 0
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
await this.indexFile.write();
|
|
2552
|
+
}
|
|
2553
|
+
// ======================================================================
|
|
2554
|
+
// STAGING (add, unstage, remove, status)
|
|
2555
|
+
// ======================================================================
|
|
2556
|
+
async add(pathOrPaths) {
|
|
2557
|
+
const paths = Array.isArray(pathOrPaths) ? pathOrPaths : [pathOrPaths];
|
|
2558
|
+
for (let p of paths) {
|
|
2559
|
+
p = normalizePath(p);
|
|
2560
|
+
const filePath = this.workDir === "/" ? `/${p}` : `${this.workDir}/${p}`;
|
|
2561
|
+
if (!await this._layer.exists(filePath)) {
|
|
2562
|
+
this.indexFile.removeEntry(p);
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
const stat = await this._layer.stat(filePath);
|
|
2566
|
+
if (stat.isDirectory()) {
|
|
2567
|
+
await this.addDirectory(p);
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
const content = await this._layer.readFile(filePath);
|
|
2571
|
+
const oid = await this.objectDB.writeBlob(content);
|
|
2572
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2573
|
+
this.indexFile.clearConflictEntries(p);
|
|
2574
|
+
this.indexFile.addEntry({
|
|
2575
|
+
ctimeSeconds: now,
|
|
2576
|
+
ctimeNanoseconds: 0,
|
|
2577
|
+
mtimeSeconds: now,
|
|
2578
|
+
mtimeNanoseconds: 0,
|
|
2579
|
+
dev: 0,
|
|
2580
|
+
ino: 0,
|
|
2581
|
+
mode: stat.isFile() ? 33188 : 40960,
|
|
2582
|
+
uid: 0,
|
|
2583
|
+
gid: 0,
|
|
2584
|
+
size: content.length,
|
|
2585
|
+
oid,
|
|
2586
|
+
flags: 0,
|
|
2587
|
+
path: p,
|
|
2588
|
+
stage: 0
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
await this.indexFile.write();
|
|
2592
|
+
}
|
|
2593
|
+
async addDirectory(dirPath) {
|
|
2594
|
+
const fullPath = this.workDir === "/" ? `/${dirPath}` : `${this.workDir}/${dirPath}`;
|
|
2595
|
+
const entries = await this._layer.readdir(fullPath);
|
|
2596
|
+
for (const entry of entries) {
|
|
2597
|
+
const childPath = dirPath ? `${dirPath}/${entry.name}` : entry.name;
|
|
2598
|
+
if (this.ignore.isIgnored(childPath, entry.isDirectory())) continue;
|
|
2599
|
+
if (entry.isDirectory()) {
|
|
2600
|
+
await this.addDirectory(childPath);
|
|
2601
|
+
} else {
|
|
2602
|
+
await this.add([childPath]);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
async unstage(pathOrPaths) {
|
|
2607
|
+
const headTree = await this.diffEngine.getHeadTree();
|
|
2608
|
+
const paths = Array.isArray(pathOrPaths) ? pathOrPaths : [pathOrPaths];
|
|
2609
|
+
for (let p of paths) {
|
|
2610
|
+
p = normalizePath(p);
|
|
2611
|
+
const headOid = headTree.get(p);
|
|
2612
|
+
if (headOid) {
|
|
2613
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2614
|
+
this.indexFile.addEntry({
|
|
2615
|
+
ctimeSeconds: now,
|
|
2616
|
+
ctimeNanoseconds: 0,
|
|
2617
|
+
mtimeSeconds: now,
|
|
2618
|
+
mtimeNanoseconds: 0,
|
|
2619
|
+
dev: 0,
|
|
2620
|
+
ino: 0,
|
|
2621
|
+
mode: 33188,
|
|
2622
|
+
uid: 0,
|
|
2623
|
+
gid: 0,
|
|
2624
|
+
size: 0,
|
|
2625
|
+
oid: headOid,
|
|
2626
|
+
flags: 0,
|
|
2627
|
+
path: p,
|
|
2628
|
+
stage: 0
|
|
2629
|
+
});
|
|
2630
|
+
} else {
|
|
2631
|
+
this.indexFile.removeEntry(p);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
await this.indexFile.write();
|
|
2635
|
+
}
|
|
2636
|
+
async remove(pathOrPaths, options = {}) {
|
|
2637
|
+
const paths = Array.isArray(pathOrPaths) ? pathOrPaths : [pathOrPaths];
|
|
2638
|
+
for (let p of paths) {
|
|
2639
|
+
p = normalizePath(p);
|
|
2640
|
+
this.indexFile.removeEntry(p);
|
|
2641
|
+
if (!options.cached) {
|
|
2642
|
+
const filePath = this.workDir === "/" ? `/${p}` : `${this.workDir}/${p}`;
|
|
2643
|
+
if (await this._layer.exists(filePath)) {
|
|
2644
|
+
await this._layer.rm(filePath, { recursive: options.recursive });
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
await this.indexFile.write();
|
|
2649
|
+
}
|
|
2650
|
+
async status(path) {
|
|
2651
|
+
const results = [];
|
|
2652
|
+
const headTree = await this.diffEngine.getHeadTree();
|
|
2653
|
+
const indexPaths = /* @__PURE__ */ new Set();
|
|
2654
|
+
const workingPaths = /* @__PURE__ */ new Set();
|
|
2655
|
+
for (const entry of this.indexFile.entries) {
|
|
2656
|
+
if (entry.stage === 0) {
|
|
2657
|
+
indexPaths.add(entry.path);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
await this.collectWorkingTreePaths("", workingPaths);
|
|
2661
|
+
for (const entry of this.indexFile.entries) {
|
|
2662
|
+
if (entry.stage !== 0) continue;
|
|
2663
|
+
const headOid = headTree.get(entry.path);
|
|
2664
|
+
let index = "unmodified";
|
|
2665
|
+
if (!headOid) {
|
|
2666
|
+
index = "added";
|
|
2667
|
+
} else if (headOid !== entry.oid) {
|
|
2668
|
+
index = "modified";
|
|
2669
|
+
}
|
|
2670
|
+
let workingTree = "unmodified";
|
|
2671
|
+
const filePath = this.workDir === "/" ? `/${entry.path}` : `${this.workDir}/${entry.path}`;
|
|
2672
|
+
if (!await this._layer.exists(filePath)) {
|
|
2673
|
+
workingTree = "deleted";
|
|
2674
|
+
} else {
|
|
2675
|
+
const content = await this._layer.readFile(filePath);
|
|
2676
|
+
const workingOid = await this.objectDB.hashObject("blob", content);
|
|
2677
|
+
if (workingOid !== entry.oid) {
|
|
2678
|
+
workingTree = "modified";
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (index !== "unmodified" || workingTree !== "unmodified") {
|
|
2682
|
+
results.push({
|
|
2683
|
+
path: entry.path,
|
|
2684
|
+
index,
|
|
2685
|
+
workingTree
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
for (const [entryPath] of headTree) {
|
|
2690
|
+
if (!indexPaths.has(entryPath)) {
|
|
2691
|
+
results.push({
|
|
2692
|
+
path: entryPath,
|
|
2693
|
+
index: "deleted",
|
|
2694
|
+
workingTree: "unmodified"
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
for (const entryPath of workingPaths) {
|
|
2699
|
+
if (!indexPaths.has(entryPath) && !this.ignore.isIgnored(entryPath, false)) {
|
|
2700
|
+
results.push({
|
|
2701
|
+
path: entryPath,
|
|
2702
|
+
index: "unmodified",
|
|
2703
|
+
workingTree: "untracked"
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
const sorted = results.sort((a, b) => a.path.localeCompare(b.path));
|
|
2708
|
+
if (path !== void 0) {
|
|
2709
|
+
const normalized = normalizePath(path);
|
|
2710
|
+
const entry = sorted.find((e) => e.path === normalized);
|
|
2711
|
+
return entry ?? null;
|
|
2712
|
+
}
|
|
2713
|
+
return sorted;
|
|
2714
|
+
}
|
|
2715
|
+
async collectWorkingTreePaths(dir, paths) {
|
|
2716
|
+
const fullDir = dir ? this.workDir === "/" ? `/${dir}` : `${this.workDir}/${dir}` : this.workDir;
|
|
2717
|
+
let entries;
|
|
2718
|
+
try {
|
|
2719
|
+
entries = await this._layer.readdir(fullDir);
|
|
2720
|
+
} catch {
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
for (const entry of entries) {
|
|
2724
|
+
const childPath = dir ? `${dir}/${entry.name}` : entry.name;
|
|
2725
|
+
if (entry.name === ".git") continue;
|
|
2726
|
+
if (this.ignore.isIgnored(childPath, entry.isDirectory())) continue;
|
|
2727
|
+
if (entry.isDirectory()) {
|
|
2728
|
+
await this.collectWorkingTreePaths(childPath, paths);
|
|
2729
|
+
} else {
|
|
2730
|
+
paths.add(childPath);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
async listFiles() {
|
|
2735
|
+
return this.indexFile.entries.filter((e) => e.stage === 0).map((e) => e.path).sort();
|
|
2736
|
+
}
|
|
2737
|
+
async isIgnored(path) {
|
|
2738
|
+
return this.ignore.isIgnored(path, false);
|
|
2739
|
+
}
|
|
2740
|
+
// ======================================================================
|
|
2741
|
+
// COMMIT
|
|
2742
|
+
// ======================================================================
|
|
2743
|
+
async commit(options) {
|
|
2744
|
+
const { message, author, committer, amend, allowEmpty } = options;
|
|
2745
|
+
if (this.indexFile.hasConflicts()) {
|
|
2746
|
+
throw new GitError(
|
|
2747
|
+
"UNRESOLVED_CONFLICT",
|
|
2748
|
+
"Cannot commit with unresolved conflicts"
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
const identity = await this.buildIdentity(author);
|
|
2752
|
+
const committerIdentity = await this.buildIdentity(committer || author);
|
|
2753
|
+
let parents;
|
|
2754
|
+
let headTree;
|
|
2755
|
+
if (amend) {
|
|
2756
|
+
const headOid = await this.refStore.resolveRef("HEAD");
|
|
2757
|
+
const headObj = await this.objectDB.readObject(headOid);
|
|
2758
|
+
const headCommit = this.objectDB.parseCommit(headObj.content);
|
|
2759
|
+
parents = headCommit.parents;
|
|
2760
|
+
if (parents.length > 0) {
|
|
2761
|
+
headTree = await this.diffEngine.getTreeForCommit(parents[0]);
|
|
2762
|
+
} else {
|
|
2763
|
+
headTree = /* @__PURE__ */ new Map();
|
|
2764
|
+
}
|
|
2765
|
+
} else {
|
|
2766
|
+
try {
|
|
2767
|
+
const headOid = await this.refStore.resolveRef("HEAD");
|
|
2768
|
+
parents = [headOid];
|
|
2769
|
+
headTree = await this.diffEngine.getTreeForCommit(headOid);
|
|
2770
|
+
} catch {
|
|
2771
|
+
parents = [];
|
|
2772
|
+
headTree = /* @__PURE__ */ new Map();
|
|
2773
|
+
}
|
|
2774
|
+
const mergeHeadPath2 = `${this.gitDir}/MERGE_HEAD`;
|
|
2775
|
+
if (await this._layer.exists(mergeHeadPath2)) {
|
|
2776
|
+
const mergeHead = decode(
|
|
2777
|
+
await this._layer.readFile(mergeHeadPath2)
|
|
2778
|
+
).trim();
|
|
2779
|
+
parents.push(mergeHead);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const treeOid = await this.buildTreeFromIndex();
|
|
2783
|
+
if (!allowEmpty && parents.length > 0) {
|
|
2784
|
+
let parentTreeOid;
|
|
2785
|
+
if (amend && parents.length > 0) {
|
|
2786
|
+
const parentObj = await this.objectDB.readObject(parents[0]);
|
|
2787
|
+
const parentCommit = this.objectDB.parseCommit(parentObj.content);
|
|
2788
|
+
parentTreeOid = parentCommit.tree;
|
|
2789
|
+
} else {
|
|
2790
|
+
const headObj = await this.objectDB.readObject(parents[0]);
|
|
2791
|
+
const headCommit = this.objectDB.parseCommit(headObj.content);
|
|
2792
|
+
parentTreeOid = headCommit.tree;
|
|
2793
|
+
}
|
|
2794
|
+
if (treeOid === parentTreeOid) {
|
|
2795
|
+
throw new GitError("NOTHING_TO_COMMIT", "Nothing to commit");
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
const commitOid = await this.objectDB.writeCommit(
|
|
2799
|
+
treeOid,
|
|
2800
|
+
parents,
|
|
2801
|
+
identity,
|
|
2802
|
+
committerIdentity,
|
|
2803
|
+
message
|
|
2804
|
+
);
|
|
2805
|
+
const sym = await this.refStore.readSymbolicRef("HEAD");
|
|
2806
|
+
if (sym) {
|
|
2807
|
+
await this.refStore.writeRef(sym, commitOid);
|
|
2808
|
+
} else {
|
|
2809
|
+
await this._layer.writeFile(
|
|
2810
|
+
`${this.gitDir}/HEAD`,
|
|
2811
|
+
encode(`${commitOid}
|
|
2812
|
+
`)
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
const mergeHeadPath = `${this.gitDir}/MERGE_HEAD`;
|
|
2816
|
+
if (await this._layer.exists(mergeHeadPath)) {
|
|
2817
|
+
await this._layer.rm(mergeHeadPath);
|
|
2818
|
+
}
|
|
2819
|
+
const mergeMsgPath = `${this.gitDir}/MERGE_MSG`;
|
|
2820
|
+
if (await this._layer.exists(mergeMsgPath)) {
|
|
2821
|
+
await this._layer.rm(mergeMsgPath);
|
|
2822
|
+
}
|
|
2823
|
+
return commitOid;
|
|
2824
|
+
}
|
|
2825
|
+
async log(options = {}) {
|
|
2826
|
+
const { ref, maxCount = 50, skip = 0, path: filterPath } = options;
|
|
2827
|
+
const results = [];
|
|
2828
|
+
let startOid;
|
|
2829
|
+
try {
|
|
2830
|
+
if (ref) {
|
|
2831
|
+
const expanded = await this.refStore.expandRef(ref);
|
|
2832
|
+
startOid = await this.refStore.resolveRef(expanded);
|
|
2833
|
+
} else {
|
|
2834
|
+
startOid = await this.refStore.resolveRef("HEAD");
|
|
2835
|
+
}
|
|
2836
|
+
} catch {
|
|
2837
|
+
return [];
|
|
2838
|
+
}
|
|
2839
|
+
const queue = [startOid];
|
|
2840
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2841
|
+
let skipped = 0;
|
|
2842
|
+
while (queue.length > 0 && results.length < maxCount) {
|
|
2843
|
+
const oid = queue.shift();
|
|
2844
|
+
if (visited.has(oid)) continue;
|
|
2845
|
+
visited.add(oid);
|
|
2846
|
+
const obj = await this.objectDB.readObject(oid);
|
|
2847
|
+
if (obj.type !== "commit") continue;
|
|
2848
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
2849
|
+
for (const parent of commit.parents) {
|
|
2850
|
+
queue.push(parent);
|
|
2851
|
+
}
|
|
2852
|
+
if (options.since && commit.author.timestamp < options.since.getTime() / 1e3) {
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
if (options.until && commit.author.timestamp > options.until.getTime() / 1e3) {
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
if (filterPath) {
|
|
2859
|
+
const commitPathOid = await this.diffEngine.lookupPathInTree(
|
|
2860
|
+
commit.tree,
|
|
2861
|
+
filterPath
|
|
2862
|
+
);
|
|
2863
|
+
let pathChanged = false;
|
|
2864
|
+
if (commit.parents.length === 0) {
|
|
2865
|
+
pathChanged = commitPathOid !== null;
|
|
2866
|
+
} else {
|
|
2867
|
+
for (const parentOid of commit.parents) {
|
|
2868
|
+
const parentPathOid = await this.diffEngine.getPathOidInCommit(
|
|
2869
|
+
parentOid,
|
|
2870
|
+
filterPath
|
|
2871
|
+
);
|
|
2872
|
+
if (commitPathOid !== parentPathOid) {
|
|
2873
|
+
pathChanged = true;
|
|
2874
|
+
break;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
if (!pathChanged) {
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
if (skipped < skip) {
|
|
2883
|
+
skipped++;
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
results.push({
|
|
2887
|
+
oid,
|
|
2888
|
+
tree: commit.tree,
|
|
2889
|
+
parents: commit.parents,
|
|
2890
|
+
author: commit.author,
|
|
2891
|
+
committer: commit.committer,
|
|
2892
|
+
message: commit.message
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
return results;
|
|
2896
|
+
}
|
|
2897
|
+
async readCommit(oid) {
|
|
2898
|
+
const obj = await this.objectDB.readObject(oid);
|
|
2899
|
+
if (obj.type !== "commit") {
|
|
2900
|
+
throw new GitError("INVALID_OBJECT", `Not a commit: ${oid}`);
|
|
2901
|
+
}
|
|
2902
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
2903
|
+
return {
|
|
2904
|
+
oid,
|
|
2905
|
+
tree: commit.tree,
|
|
2906
|
+
parents: commit.parents,
|
|
2907
|
+
author: commit.author,
|
|
2908
|
+
committer: commit.committer,
|
|
2909
|
+
message: commit.message
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
// ======================================================================
|
|
2913
|
+
// REMOTES
|
|
2914
|
+
// ======================================================================
|
|
2915
|
+
async addRemote(name, url) {
|
|
2916
|
+
if (this.config.get(`remote.${name}.url`)) {
|
|
2917
|
+
throw new GitError("ALREADY_EXISTS", `Remote '${name}' already exists`);
|
|
2918
|
+
}
|
|
2919
|
+
this.config.set(`remote.${name}.url`, url);
|
|
2920
|
+
this.config.set(
|
|
2921
|
+
`remote.${name}.fetch`,
|
|
2922
|
+
`+refs/heads/*:refs/remotes/${name}/*`
|
|
2923
|
+
);
|
|
2924
|
+
await this.config.save();
|
|
2925
|
+
}
|
|
2926
|
+
async listRemotes() {
|
|
2927
|
+
const remotes = [];
|
|
2928
|
+
await this.config.load();
|
|
2929
|
+
const configContent = this.config.serialize();
|
|
2930
|
+
const parsed = this.config.parseConfig(configContent);
|
|
2931
|
+
for (const section of parsed) {
|
|
2932
|
+
if (section.name === "remote" && section.subsection) {
|
|
2933
|
+
const url = section.entries.find((e) => e.key === "url")?.value ?? "";
|
|
2934
|
+
remotes.push({
|
|
2935
|
+
name: section.subsection,
|
|
2936
|
+
url
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return remotes;
|
|
2941
|
+
}
|
|
2942
|
+
async deleteRemote(name) {
|
|
2943
|
+
if (!this.config.get(`remote.${name}.url`)) {
|
|
2944
|
+
throw new GitError("NOT_FOUND", `Remote '${name}' not found`);
|
|
2945
|
+
}
|
|
2946
|
+
this.config.deleteSection(`remote.${name}`);
|
|
2947
|
+
await this.config.save();
|
|
2948
|
+
try {
|
|
2949
|
+
const remoteBranchDir = `${this.gitDir}/refs/remotes/${name}`;
|
|
2950
|
+
if (await this._layer.exists(remoteBranchDir)) {
|
|
2951
|
+
await this._layer.rm(remoteBranchDir, { recursive: true });
|
|
2952
|
+
}
|
|
2953
|
+
} catch {
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
// ======================================================================
|
|
2957
|
+
// FETCH / PULL / PUSH
|
|
2958
|
+
// ======================================================================
|
|
2959
|
+
async fetch(remote, options = {}) {
|
|
2960
|
+
const transport = options.transport || this.transport;
|
|
2961
|
+
if (!transport) {
|
|
2962
|
+
throw new GitError("TRANSPORT_ERROR", "No transport configured");
|
|
2963
|
+
}
|
|
2964
|
+
const remoteName = remote;
|
|
2965
|
+
const remoteUrl = this.config.get(`remote.${remoteName}.url`);
|
|
2966
|
+
if (!remoteUrl) {
|
|
2967
|
+
throw new GitError("NOT_FOUND", `Remote '${remoteName}' not found`);
|
|
2968
|
+
}
|
|
2969
|
+
const discovery = await transport.discover(
|
|
2970
|
+
remoteUrl,
|
|
2971
|
+
"git-upload-pack"
|
|
2972
|
+
);
|
|
2973
|
+
const wants = [];
|
|
2974
|
+
const haves = [];
|
|
2975
|
+
for (const remoteRef of discovery.refs) {
|
|
2976
|
+
if (remoteRef.name.startsWith("refs/heads/")) {
|
|
2977
|
+
if (options.branch) {
|
|
2978
|
+
const branchName = remoteRef.name.substring(11);
|
|
2979
|
+
if (branchName !== options.branch) continue;
|
|
2980
|
+
}
|
|
2981
|
+
const exists = await this.objectDB.existsObject(remoteRef.oid);
|
|
2982
|
+
if (!exists) {
|
|
2983
|
+
wants.push(remoteRef.oid);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
const localRefs = await this.refStore.listRefs("refs/");
|
|
2988
|
+
for (const localRef of localRefs) {
|
|
2989
|
+
haves.push(localRef.oid);
|
|
2990
|
+
}
|
|
2991
|
+
const updated = [];
|
|
2992
|
+
if (wants.length > 0) {
|
|
2993
|
+
const response = await transport.fetch(remoteUrl, {
|
|
2994
|
+
wants,
|
|
2995
|
+
haves,
|
|
2996
|
+
depth: options.depth
|
|
2997
|
+
});
|
|
2998
|
+
if (response.packfile.length > 0) {
|
|
2999
|
+
await this.storePackData(response.packfile);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
for (const remoteRef of discovery.refs) {
|
|
3003
|
+
if (remoteRef.name.startsWith("refs/heads/")) {
|
|
3004
|
+
if (options.branch) {
|
|
3005
|
+
const branchName = remoteRef.name.substring(11);
|
|
3006
|
+
if (branchName !== options.branch) continue;
|
|
3007
|
+
}
|
|
3008
|
+
const trackingRef = `refs/remotes/${remoteName}/${remoteRef.name.substring(11)}`;
|
|
3009
|
+
let oldOid = null;
|
|
3010
|
+
try {
|
|
3011
|
+
oldOid = await this.refStore.resolveRef(trackingRef);
|
|
3012
|
+
} catch {
|
|
3013
|
+
}
|
|
3014
|
+
if (oldOid !== remoteRef.oid) {
|
|
3015
|
+
await this.refStore.writeRef(trackingRef, remoteRef.oid);
|
|
3016
|
+
updated.push({ ref: trackingRef, oldOid, newOid: remoteRef.oid });
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
return { updated };
|
|
3021
|
+
}
|
|
3022
|
+
async pull(remote, options = {}) {
|
|
3023
|
+
await this.fetch(remote, {
|
|
3024
|
+
auth: options.auth,
|
|
3025
|
+
depth: options.depth,
|
|
3026
|
+
tags: options.tags,
|
|
3027
|
+
transport: options.transport,
|
|
3028
|
+
branch: options.branch
|
|
3029
|
+
});
|
|
3030
|
+
const branch = await this.currentBranch();
|
|
3031
|
+
if (!branch) {
|
|
3032
|
+
throw new GitError("DETACHED_HEAD", "Cannot pull with detached HEAD");
|
|
3033
|
+
}
|
|
3034
|
+
const remoteBranch = options.branch || branch;
|
|
3035
|
+
const trackingRef = `refs/remotes/${remote}/${remoteBranch}`;
|
|
3036
|
+
let remoteOid;
|
|
3037
|
+
try {
|
|
3038
|
+
remoteOid = await this.refStore.resolveRef(trackingRef);
|
|
3039
|
+
} catch {
|
|
3040
|
+
throw new GitError("NOT_FOUND", `Remote tracking branch not found: ${trackingRef}`);
|
|
3041
|
+
}
|
|
3042
|
+
const headOid = await this.refStore.resolveRef("HEAD");
|
|
3043
|
+
return this.merge(
|
|
3044
|
+
trackingRef,
|
|
3045
|
+
{
|
|
3046
|
+
message: `Merge branch '${remoteBranch}' of ${remote}`,
|
|
3047
|
+
author: options.author,
|
|
3048
|
+
fastForwardOnly: options.fastForwardOnly
|
|
3049
|
+
}
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
async push(remote, options = {}) {
|
|
3053
|
+
const transport = options.transport || this.transport;
|
|
3054
|
+
if (!transport) {
|
|
3055
|
+
throw new GitError("TRANSPORT_ERROR", "No transport configured");
|
|
3056
|
+
}
|
|
3057
|
+
const remoteName = remote;
|
|
3058
|
+
const remoteUrl = this.config.get(`remote.${remoteName}.url`);
|
|
3059
|
+
if (!remoteUrl) {
|
|
3060
|
+
throw new GitError("NOT_FOUND", `Remote '${remoteName}' not found`);
|
|
3061
|
+
}
|
|
3062
|
+
const branch = options.branch || await this.currentBranch();
|
|
3063
|
+
if (!branch) {
|
|
3064
|
+
throw new GitError("DETACHED_HEAD", "Cannot push with detached HEAD");
|
|
3065
|
+
}
|
|
3066
|
+
const localOid = await this.refStore.resolveRef(`refs/heads/${branch}`);
|
|
3067
|
+
const discovery = await transport.discover(
|
|
3068
|
+
remoteUrl,
|
|
3069
|
+
"git-receive-pack"
|
|
3070
|
+
);
|
|
3071
|
+
const remoteRef = discovery.refs.find(
|
|
3072
|
+
(r) => r.name === `refs/heads/${branch}`
|
|
3073
|
+
);
|
|
3074
|
+
const oldOid = remoteRef?.oid || "0".repeat(40);
|
|
3075
|
+
const objects = await this.collectObjectsForPush(
|
|
3076
|
+
localOid,
|
|
3077
|
+
oldOid === "0".repeat(40) ? null : oldOid
|
|
3078
|
+
);
|
|
3079
|
+
const packData = await createPackfile(objects);
|
|
3080
|
+
const result = await transport.push(remoteUrl, {
|
|
3081
|
+
updates: [
|
|
3082
|
+
{
|
|
3083
|
+
ref: `refs/heads/${branch}`,
|
|
3084
|
+
oldOid,
|
|
3085
|
+
newOid: localOid
|
|
3086
|
+
}
|
|
3087
|
+
],
|
|
3088
|
+
packfile: packData
|
|
3089
|
+
});
|
|
3090
|
+
if (options.setUpstream) {
|
|
3091
|
+
await this.setUpstream(branch, `${remoteName}/${branch}`);
|
|
3092
|
+
}
|
|
3093
|
+
return result;
|
|
3094
|
+
}
|
|
3095
|
+
async collectObjectsForPush(localOid, remoteOid) {
|
|
3096
|
+
const objects = [];
|
|
3097
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3098
|
+
const queue = [localOid];
|
|
3099
|
+
while (queue.length > 0) {
|
|
3100
|
+
const oid = queue.shift();
|
|
3101
|
+
if (visited.has(oid)) continue;
|
|
3102
|
+
if (oid === remoteOid) continue;
|
|
3103
|
+
visited.add(oid);
|
|
3104
|
+
const obj = await this.objectDB.readObject(oid);
|
|
3105
|
+
objects.push({ type: obj.type, content: obj.content });
|
|
3106
|
+
if (obj.type === "commit") {
|
|
3107
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
3108
|
+
queue.push(commit.tree);
|
|
3109
|
+
for (const parent of commit.parents) {
|
|
3110
|
+
if (parent !== remoteOid) {
|
|
3111
|
+
queue.push(parent);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
} else if (obj.type === "tree") {
|
|
3115
|
+
const entries = this.objectDB.parseTree(obj.content);
|
|
3116
|
+
for (const entry of entries) {
|
|
3117
|
+
queue.push(entry.oid);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
return objects;
|
|
3122
|
+
}
|
|
3123
|
+
async storePackData(packData) {
|
|
3124
|
+
const { PackReader: PackReader2 } = await Promise.resolve().then(() => (init_read(), read_exports));
|
|
3125
|
+
if (packData.length < 12 || packData[0] !== 80 || packData[1] !== 65 || packData[2] !== 67 || packData[3] !== 75) {
|
|
3126
|
+
throw new GitError("TRANSPORT_ERROR", "Invalid pack data");
|
|
3127
|
+
}
|
|
3128
|
+
const packDir = `${this.gitDir}/objects/pack`;
|
|
3129
|
+
await this._layer.mkdir(packDir, { recursive: true });
|
|
3130
|
+
const packHash = await this.objectDB.hashObject("blob", packData);
|
|
3131
|
+
const packPath = `${packDir}/pack-${packHash}.pack`;
|
|
3132
|
+
await this._layer.writeFile(packPath, packData);
|
|
3133
|
+
const reader = new PackReader2(this._layer, packPath);
|
|
3134
|
+
const objects = await reader.extractAll();
|
|
3135
|
+
for (const obj of objects) {
|
|
3136
|
+
await this.objectDB.writeObject(obj.type, obj.content);
|
|
3137
|
+
}
|
|
3138
|
+
try {
|
|
3139
|
+
await this._layer.rm(packPath);
|
|
3140
|
+
} catch {
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
// ======================================================================
|
|
3144
|
+
// MERGE
|
|
3145
|
+
// ======================================================================
|
|
3146
|
+
async merge(ref, options = {}) {
|
|
3147
|
+
const { message, noFastForward, fastForwardOnly, author } = options;
|
|
3148
|
+
let theirsOid;
|
|
3149
|
+
try {
|
|
3150
|
+
const expanded = await this.refStore.expandRef(ref);
|
|
3151
|
+
theirsOid = await this.refStore.resolveRef(expanded);
|
|
3152
|
+
} catch {
|
|
3153
|
+
throw new GitError("NOT_FOUND", `Ref '${ref}' not found`);
|
|
3154
|
+
}
|
|
3155
|
+
const oursOid = await this.refStore.resolveRef("HEAD");
|
|
3156
|
+
const identity = await this.buildIdentity(author);
|
|
3157
|
+
const mergeMsg = message || `Merge branch '${ref}'`;
|
|
3158
|
+
const result = await this.mergeEngine.merge(
|
|
3159
|
+
oursOid,
|
|
3160
|
+
theirsOid,
|
|
3161
|
+
mergeMsg,
|
|
3162
|
+
identity,
|
|
3163
|
+
identity
|
|
3164
|
+
);
|
|
3165
|
+
if (result.type === "already-up-to-date") {
|
|
3166
|
+
return result;
|
|
3167
|
+
}
|
|
3168
|
+
if (result.type === "fast-forward" && noFastForward) {
|
|
3169
|
+
const theirsObj = await this.objectDB.readObject(theirsOid);
|
|
3170
|
+
const commit = this.objectDB.parseCommit(theirsObj.content);
|
|
3171
|
+
const commitOid = await this.objectDB.writeCommit(
|
|
3172
|
+
commit.tree,
|
|
3173
|
+
[oursOid, theirsOid],
|
|
3174
|
+
identity,
|
|
3175
|
+
identity,
|
|
3176
|
+
mergeMsg
|
|
3177
|
+
);
|
|
3178
|
+
const sym = await this.refStore.readSymbolicRef("HEAD");
|
|
3179
|
+
if (sym) {
|
|
3180
|
+
await this.refStore.writeRef(sym, commitOid);
|
|
3181
|
+
}
|
|
3182
|
+
await this.updateWorkingTreeToCommit(commitOid);
|
|
3183
|
+
return { type: "merge-commit", oid: commitOid, conflicts: [] };
|
|
3184
|
+
}
|
|
3185
|
+
if (result.type === "fast-forward" && !noFastForward) {
|
|
3186
|
+
const sym = await this.refStore.readSymbolicRef("HEAD");
|
|
3187
|
+
if (sym) {
|
|
3188
|
+
await this.refStore.writeRef(sym, theirsOid);
|
|
3189
|
+
}
|
|
3190
|
+
await this.updateWorkingTreeToCommit(theirsOid);
|
|
3191
|
+
return result;
|
|
3192
|
+
}
|
|
3193
|
+
if (fastForwardOnly && result.type === "merge-commit") {
|
|
3194
|
+
throw new GitError("NON_FAST_FORWARD", "Fast-forward only merge requested but a merge commit is required");
|
|
3195
|
+
}
|
|
3196
|
+
if (result.conflicts.length === 0 && result.oid) {
|
|
3197
|
+
const sym = await this.refStore.readSymbolicRef("HEAD");
|
|
3198
|
+
if (sym) {
|
|
3199
|
+
await this.refStore.writeRef(sym, result.oid);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
return result;
|
|
3203
|
+
}
|
|
3204
|
+
async abortMerge() {
|
|
3205
|
+
const mergeHeadPath = `${this.gitDir}/MERGE_HEAD`;
|
|
3206
|
+
if (!await this._layer.exists(mergeHeadPath)) {
|
|
3207
|
+
throw new GitError("NOT_MERGING", "Not currently merging");
|
|
3208
|
+
}
|
|
3209
|
+
const headOid = await this.refStore.resolveRef("HEAD");
|
|
3210
|
+
await this.resetHard(headOid);
|
|
3211
|
+
await this._layer.rm(mergeHeadPath);
|
|
3212
|
+
const mergeMsgPath = `${this.gitDir}/MERGE_MSG`;
|
|
3213
|
+
if (await this._layer.exists(mergeMsgPath)) {
|
|
3214
|
+
await this._layer.rm(mergeMsgPath);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
// ======================================================================
|
|
3218
|
+
// TAGS
|
|
3219
|
+
// ======================================================================
|
|
3220
|
+
async createTag(name, options = {}) {
|
|
3221
|
+
const { target, message, tagger } = options;
|
|
3222
|
+
let targetOid;
|
|
3223
|
+
if (target) {
|
|
3224
|
+
const expanded = await this.refStore.expandRef(target);
|
|
3225
|
+
targetOid = await this.refStore.resolveRef(expanded);
|
|
3226
|
+
} else {
|
|
3227
|
+
targetOid = await this.refStore.resolveRef("HEAD");
|
|
3228
|
+
}
|
|
3229
|
+
const tagRef = `refs/tags/${name}`;
|
|
3230
|
+
try {
|
|
3231
|
+
await this.refStore.resolveRef(tagRef);
|
|
3232
|
+
throw new GitError("ALREADY_EXISTS", `Tag '${name}' already exists`);
|
|
3233
|
+
} catch (e) {
|
|
3234
|
+
if (e instanceof GitError && e.code === "ALREADY_EXISTS") throw e;
|
|
3235
|
+
}
|
|
3236
|
+
if (message) {
|
|
3237
|
+
const identity = await this.buildIdentity(tagger);
|
|
3238
|
+
const tagOid = await this.objectDB.writeTag(
|
|
3239
|
+
targetOid,
|
|
3240
|
+
"commit",
|
|
3241
|
+
name,
|
|
3242
|
+
identity,
|
|
3243
|
+
message || ""
|
|
3244
|
+
);
|
|
3245
|
+
await this.refStore.writeRef(tagRef, tagOid);
|
|
3246
|
+
} else {
|
|
3247
|
+
await this.refStore.writeRef(tagRef, targetOid);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
async deleteTag(name) {
|
|
3251
|
+
const tagRef = `refs/tags/${name}`;
|
|
3252
|
+
try {
|
|
3253
|
+
await this.refStore.resolveRef(tagRef);
|
|
3254
|
+
} catch {
|
|
3255
|
+
throw new GitError("NOT_FOUND", `Tag '${name}' not found`);
|
|
3256
|
+
}
|
|
3257
|
+
await this.refStore.deleteRef(tagRef);
|
|
3258
|
+
}
|
|
3259
|
+
async listTags() {
|
|
3260
|
+
const tagRefs = await this.refStore.listRefs("refs/tags/");
|
|
3261
|
+
const tags = [];
|
|
3262
|
+
for (const { name: refName, oid } of tagRefs) {
|
|
3263
|
+
const tagName = refName.substring(10);
|
|
3264
|
+
try {
|
|
3265
|
+
const obj = await this.objectDB.readObject(oid);
|
|
3266
|
+
if (obj.type === "tag") {
|
|
3267
|
+
const tag = this.objectDB.parseTag(obj.content);
|
|
3268
|
+
tags.push({
|
|
3269
|
+
name: tagName,
|
|
3270
|
+
oid,
|
|
3271
|
+
type: "annotated",
|
|
3272
|
+
target: tag.object,
|
|
3273
|
+
tagger: tag.tagger,
|
|
3274
|
+
message: tag.message
|
|
3275
|
+
});
|
|
3276
|
+
} else {
|
|
3277
|
+
tags.push({
|
|
3278
|
+
name: tagName,
|
|
3279
|
+
oid,
|
|
3280
|
+
type: "lightweight"
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
} catch {
|
|
3284
|
+
tags.push({ name: tagName, oid, type: "lightweight" });
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return tags;
|
|
3288
|
+
}
|
|
3289
|
+
// ======================================================================
|
|
3290
|
+
// DIFF
|
|
3291
|
+
// ======================================================================
|
|
3292
|
+
async diff(options = {}) {
|
|
3293
|
+
return this.diffEngine.diff(options);
|
|
3294
|
+
}
|
|
3295
|
+
// ======================================================================
|
|
3296
|
+
// STASH
|
|
3297
|
+
// ======================================================================
|
|
3298
|
+
async stash(options = {}) {
|
|
3299
|
+
const statusEntries = await this.status();
|
|
3300
|
+
const entries = Array.isArray(statusEntries) ? statusEntries : statusEntries ? [statusEntries] : [];
|
|
3301
|
+
const hasChanges = entries.some(
|
|
3302
|
+
(e) => e.index !== "unmodified" || e.workingTree !== "unmodified" && e.workingTree !== "untracked"
|
|
3303
|
+
);
|
|
3304
|
+
if (!hasChanges) {
|
|
3305
|
+
throw new GitError("NOTHING_TO_STASH", "No changes to stash");
|
|
3306
|
+
}
|
|
3307
|
+
const identity = await this.buildIdentity();
|
|
3308
|
+
const message = options.message || `WIP on ${await this.currentBranch() || "HEAD"}`;
|
|
3309
|
+
await this.stashManager.push(
|
|
3310
|
+
message,
|
|
3311
|
+
identity,
|
|
3312
|
+
options.includeUntracked ?? false
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
async listStashes() {
|
|
3316
|
+
return this.stashManager.list();
|
|
3317
|
+
}
|
|
3318
|
+
async applyStash(options = {}) {
|
|
3319
|
+
return this.stashManager.apply(options.index ?? 0);
|
|
3320
|
+
}
|
|
3321
|
+
async popStash(options = {}) {
|
|
3322
|
+
return this.stashManager.pop(options.index ?? 0);
|
|
3323
|
+
}
|
|
3324
|
+
async dropStash(index = 0) {
|
|
3325
|
+
return this.stashManager.drop(index);
|
|
3326
|
+
}
|
|
3327
|
+
// ======================================================================
|
|
3328
|
+
// CONFIG
|
|
3329
|
+
// ======================================================================
|
|
3330
|
+
async getConfig(key) {
|
|
3331
|
+
return this.config.get(key) ?? null;
|
|
3332
|
+
}
|
|
3333
|
+
async setConfig(key, value) {
|
|
3334
|
+
this.config.set(key, value);
|
|
3335
|
+
await this.config.save();
|
|
3336
|
+
}
|
|
3337
|
+
async deleteConfig(key) {
|
|
3338
|
+
const result = this.config.delete(key);
|
|
3339
|
+
if (result) await this.config.save();
|
|
3340
|
+
}
|
|
3341
|
+
// ======================================================================
|
|
3342
|
+
// REFS
|
|
3343
|
+
// ======================================================================
|
|
3344
|
+
async resolveRef(ref) {
|
|
3345
|
+
const expanded = await this.refStore.expandRef(ref);
|
|
3346
|
+
return this.refStore.resolveRef(expanded);
|
|
3347
|
+
}
|
|
3348
|
+
async listRefs(prefix) {
|
|
3349
|
+
return this.refStore.listRefs(prefix);
|
|
3350
|
+
}
|
|
3351
|
+
async setUpstream(branch, upstream) {
|
|
3352
|
+
const slashIdx = upstream.indexOf("/");
|
|
3353
|
+
if (slashIdx < 0) {
|
|
3354
|
+
throw new GitError("INVALID_OBJECT", `Invalid upstream format: ${upstream}. Expected "remote/branch".`);
|
|
3355
|
+
}
|
|
3356
|
+
const remote = upstream.substring(0, slashIdx);
|
|
3357
|
+
const remoteBranch = upstream.substring(slashIdx + 1);
|
|
3358
|
+
this.config.set(`branch.${branch}.remote`, remote);
|
|
3359
|
+
this.config.set(`branch.${branch}.merge`, `refs/heads/${remoteBranch}`);
|
|
3360
|
+
await this.config.save();
|
|
3361
|
+
}
|
|
3362
|
+
// ======================================================================
|
|
3363
|
+
// RESET
|
|
3364
|
+
// ======================================================================
|
|
3365
|
+
async reset(ref, options = {}) {
|
|
3366
|
+
const { mode = "mixed", paths } = options;
|
|
3367
|
+
const resolvedRef = await this.resolveRefExpression(ref);
|
|
3368
|
+
if (paths && paths.length > 0) {
|
|
3369
|
+
const expanded = await this.refStore.expandRef(resolvedRef);
|
|
3370
|
+
const targetOid2 = await this.refStore.resolveRef(expanded);
|
|
3371
|
+
const targetTree = await this.diffEngine.getTreeForCommit(targetOid2);
|
|
3372
|
+
for (let p of paths) {
|
|
3373
|
+
p = normalizePath(p);
|
|
3374
|
+
const treeOid = targetTree.get(p);
|
|
3375
|
+
if (treeOid) {
|
|
3376
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3377
|
+
this.indexFile.addEntry({
|
|
3378
|
+
ctimeSeconds: now,
|
|
3379
|
+
ctimeNanoseconds: 0,
|
|
3380
|
+
mtimeSeconds: now,
|
|
3381
|
+
mtimeNanoseconds: 0,
|
|
3382
|
+
dev: 0,
|
|
3383
|
+
ino: 0,
|
|
3384
|
+
mode: 33188,
|
|
3385
|
+
uid: 0,
|
|
3386
|
+
gid: 0,
|
|
3387
|
+
size: 0,
|
|
3388
|
+
oid: treeOid,
|
|
3389
|
+
flags: 0,
|
|
3390
|
+
path: p,
|
|
3391
|
+
stage: 0
|
|
3392
|
+
});
|
|
3393
|
+
} else {
|
|
3394
|
+
this.indexFile.removeEntry(p);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
await this.indexFile.write();
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
let targetOid;
|
|
3401
|
+
try {
|
|
3402
|
+
const expanded = await this.refStore.expandRef(resolvedRef);
|
|
3403
|
+
targetOid = await this.refStore.resolveRef(expanded);
|
|
3404
|
+
} catch {
|
|
3405
|
+
throw new GitError("NOT_FOUND", `Ref '${ref}' not found`);
|
|
3406
|
+
}
|
|
3407
|
+
const sym = await this.refStore.readSymbolicRef("HEAD");
|
|
3408
|
+
if (sym) {
|
|
3409
|
+
await this.refStore.writeRef(sym, targetOid);
|
|
3410
|
+
} else {
|
|
3411
|
+
await this._layer.writeFile(
|
|
3412
|
+
`${this.gitDir}/HEAD`,
|
|
3413
|
+
encode(`${targetOid}
|
|
3414
|
+
`)
|
|
3415
|
+
);
|
|
3416
|
+
}
|
|
3417
|
+
if (mode === "soft") {
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
if (mode === "mixed") {
|
|
3421
|
+
await this.resetIndex(targetOid);
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
if (mode === "hard") {
|
|
3425
|
+
await this.resetHard(targetOid);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* Resolve ref expressions like HEAD~1, HEAD~3, HEAD^, main~2, etc.
|
|
3430
|
+
*/
|
|
3431
|
+
async resolveRefExpression(ref) {
|
|
3432
|
+
const tildeMatch = ref.match(/^(.+)~(\d+)$/);
|
|
3433
|
+
if (tildeMatch) {
|
|
3434
|
+
const baseRef = tildeMatch[1];
|
|
3435
|
+
const count = parseInt(tildeMatch[2], 10);
|
|
3436
|
+
let expanded;
|
|
3437
|
+
try {
|
|
3438
|
+
expanded = await this.refStore.expandRef(baseRef);
|
|
3439
|
+
} catch {
|
|
3440
|
+
expanded = baseRef;
|
|
3441
|
+
}
|
|
3442
|
+
let oid = await this.refStore.resolveRef(expanded);
|
|
3443
|
+
for (let i = 0; i < count; i++) {
|
|
3444
|
+
const obj = await this.objectDB.readObject(oid);
|
|
3445
|
+
if (obj.type !== "commit") {
|
|
3446
|
+
throw new GitError("INVALID_OBJECT", `Not a commit: ${oid}`);
|
|
3447
|
+
}
|
|
3448
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
3449
|
+
if (commit.parents.length === 0) {
|
|
3450
|
+
throw new GitError("NOT_FOUND", `Cannot go back ${count} commits from '${ref}' \u2014 ran out of parents`);
|
|
3451
|
+
}
|
|
3452
|
+
oid = commit.parents[0];
|
|
3453
|
+
}
|
|
3454
|
+
return oid;
|
|
3455
|
+
}
|
|
3456
|
+
const caretMatch = ref.match(/^(.+)\^$/);
|
|
3457
|
+
if (caretMatch) {
|
|
3458
|
+
const baseRef = caretMatch[1];
|
|
3459
|
+
let expanded;
|
|
3460
|
+
try {
|
|
3461
|
+
expanded = await this.refStore.expandRef(baseRef);
|
|
3462
|
+
} catch {
|
|
3463
|
+
expanded = baseRef;
|
|
3464
|
+
}
|
|
3465
|
+
const oid = await this.refStore.resolveRef(expanded);
|
|
3466
|
+
const obj = await this.objectDB.readObject(oid);
|
|
3467
|
+
if (obj.type !== "commit") {
|
|
3468
|
+
throw new GitError("INVALID_OBJECT", `Not a commit: ${oid}`);
|
|
3469
|
+
}
|
|
3470
|
+
const commit = this.objectDB.parseCommit(obj.content);
|
|
3471
|
+
if (commit.parents.length === 0) {
|
|
3472
|
+
throw new GitError("NOT_FOUND", `'${ref}' has no parent commit`);
|
|
3473
|
+
}
|
|
3474
|
+
return commit.parents[0];
|
|
3475
|
+
}
|
|
3476
|
+
return ref;
|
|
3477
|
+
}
|
|
3478
|
+
async resetIndex(commitOid) {
|
|
3479
|
+
const tree = await this.diffEngine.getTreeForCommit(commitOid);
|
|
3480
|
+
this.indexFile.entries = [];
|
|
3481
|
+
for (const [path, oid] of tree) {
|
|
3482
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3483
|
+
this.indexFile.addEntry({
|
|
3484
|
+
ctimeSeconds: now,
|
|
3485
|
+
ctimeNanoseconds: 0,
|
|
3486
|
+
mtimeSeconds: now,
|
|
3487
|
+
mtimeNanoseconds: 0,
|
|
3488
|
+
dev: 0,
|
|
3489
|
+
ino: 0,
|
|
3490
|
+
mode: 33188,
|
|
3491
|
+
uid: 0,
|
|
3492
|
+
gid: 0,
|
|
3493
|
+
size: 0,
|
|
3494
|
+
oid,
|
|
3495
|
+
flags: 0,
|
|
3496
|
+
path,
|
|
3497
|
+
stage: 0
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
await this.indexFile.write();
|
|
3501
|
+
}
|
|
3502
|
+
async resetHard(commitOid) {
|
|
3503
|
+
const tree = await this.diffEngine.getTreeForCommit(commitOid);
|
|
3504
|
+
for (const entry of this.indexFile.entries) {
|
|
3505
|
+
if (entry.stage !== 0) continue;
|
|
3506
|
+
const filePath = this.workDir === "/" ? `/${entry.path}` : `${this.workDir}/${entry.path}`;
|
|
3507
|
+
if (await this._layer.exists(filePath)) {
|
|
3508
|
+
await this._layer.rm(filePath);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
this.indexFile.entries = [];
|
|
3512
|
+
for (const [path, oid] of tree) {
|
|
3513
|
+
const obj = await this.objectDB.readObject(oid);
|
|
3514
|
+
const filePath = this.workDir === "/" ? `/${path}` : `${this.workDir}/${path}`;
|
|
3515
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
3516
|
+
if (dir) {
|
|
3517
|
+
await this._layer.mkdir(dir, { recursive: true });
|
|
3518
|
+
}
|
|
3519
|
+
await this._layer.writeFile(filePath, obj.content);
|
|
3520
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3521
|
+
this.indexFile.addEntry({
|
|
3522
|
+
ctimeSeconds: now,
|
|
3523
|
+
ctimeNanoseconds: 0,
|
|
3524
|
+
mtimeSeconds: now,
|
|
3525
|
+
mtimeNanoseconds: 0,
|
|
3526
|
+
dev: 0,
|
|
3527
|
+
ino: 0,
|
|
3528
|
+
mode: 33188,
|
|
3529
|
+
uid: 0,
|
|
3530
|
+
gid: 0,
|
|
3531
|
+
size: 0,
|
|
3532
|
+
oid,
|
|
3533
|
+
flags: 0,
|
|
3534
|
+
path,
|
|
3535
|
+
stage: 0
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
await this.indexFile.write();
|
|
3539
|
+
}
|
|
3540
|
+
// ======================================================================
|
|
3541
|
+
// HELPERS
|
|
3542
|
+
// ======================================================================
|
|
3543
|
+
async buildIdentity(identity) {
|
|
3544
|
+
const name = identity?.name || this.config.get("user.name") || "Unknown";
|
|
3545
|
+
const email = identity?.email || this.config.get("user.email") || "unknown@example.com";
|
|
3546
|
+
return {
|
|
3547
|
+
name,
|
|
3548
|
+
email,
|
|
3549
|
+
timestamp: identity?.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
3550
|
+
timezoneOffset: identity?.timezoneOffset ?? 0
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
async buildTreeFromIndex() {
|
|
3554
|
+
const paths = /* @__PURE__ */ new Map();
|
|
3555
|
+
for (const entry of this.indexFile.entries) {
|
|
3556
|
+
if (entry.stage === 0) {
|
|
3557
|
+
paths.set(entry.path, entry.oid);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
return this.mergeEngine.buildTreeFromPaths(paths);
|
|
3561
|
+
}
|
|
3562
|
+
};
|
|
3563
|
+
|
|
3564
|
+
// src/index.ts
|
|
3565
|
+
init_utils();
|
|
3566
|
+
|
|
3567
|
+
// src/transport/http.ts
|
|
3568
|
+
init_utils();
|
|
3569
|
+
var HttpTransport = class {
|
|
3570
|
+
headers;
|
|
3571
|
+
constructor(options = {}) {
|
|
3572
|
+
const headers = { ...options.headers };
|
|
3573
|
+
if (options.auth) {
|
|
3574
|
+
headers["Authorization"] = `Basic ${btoa(`${options.auth.username}:${options.auth.password}`)}`;
|
|
3575
|
+
}
|
|
3576
|
+
this.headers = headers;
|
|
3577
|
+
}
|
|
3578
|
+
// ---- Discovery ---------------------------------------------------------
|
|
3579
|
+
async discover(url, service) {
|
|
3580
|
+
const infoUrl = `${url.replace(/\/$/, "")}/info/refs?service=${service}`;
|
|
3581
|
+
const response = await fetch(infoUrl, {
|
|
3582
|
+
headers: {
|
|
3583
|
+
...this.headers,
|
|
3584
|
+
"User-Agent": "catmint-fs-git/0.1"
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
if (!response.ok) {
|
|
3588
|
+
throw new Error(
|
|
3589
|
+
`HTTP ${response.status}: ${response.statusText}`
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
3593
|
+
return this.parseDiscovery(body, service);
|
|
3594
|
+
}
|
|
3595
|
+
parseDiscovery(data, service) {
|
|
3596
|
+
const refs = [];
|
|
3597
|
+
const capabilities = [];
|
|
3598
|
+
let offset = 0;
|
|
3599
|
+
let firstLine = true;
|
|
3600
|
+
while (offset < data.length) {
|
|
3601
|
+
const { line, nextOffset } = readPktLine(data, offset);
|
|
3602
|
+
offset = nextOffset;
|
|
3603
|
+
if (line === null) continue;
|
|
3604
|
+
if (line.length === 0) continue;
|
|
3605
|
+
const text = decode(line).trim();
|
|
3606
|
+
if (text === `# service=${service}`) continue;
|
|
3607
|
+
if (!text) continue;
|
|
3608
|
+
if (firstLine) {
|
|
3609
|
+
const nullIdx = text.indexOf("\0");
|
|
3610
|
+
if (nullIdx >= 0) {
|
|
3611
|
+
const refPart = text.substring(0, nullIdx);
|
|
3612
|
+
const capsPart = text.substring(nullIdx + 1);
|
|
3613
|
+
capabilities.push(...capsPart.split(" ").filter(Boolean));
|
|
3614
|
+
const [oid, name] = refPart.split(" ");
|
|
3615
|
+
if (oid && name) {
|
|
3616
|
+
refs.push({ oid, name });
|
|
3617
|
+
}
|
|
3618
|
+
} else {
|
|
3619
|
+
const [oid, name] = text.split(" ");
|
|
3620
|
+
if (oid && name) {
|
|
3621
|
+
refs.push({ oid, name });
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
firstLine = false;
|
|
3625
|
+
} else {
|
|
3626
|
+
const [oid, name] = text.split(" ");
|
|
3627
|
+
if (oid && name && /^[0-9a-f]{40}$/.test(oid)) {
|
|
3628
|
+
refs.push({ oid, name });
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
return { refs, capabilities };
|
|
3633
|
+
}
|
|
3634
|
+
// ---- Fetch -------------------------------------------------------------
|
|
3635
|
+
async fetch(url, request) {
|
|
3636
|
+
const body = this.buildFetchRequest(request);
|
|
3637
|
+
const response = await fetch(`${url.replace(/\/$/, "")}/git-upload-pack`, {
|
|
3638
|
+
method: "POST",
|
|
3639
|
+
headers: {
|
|
3640
|
+
...this.headers,
|
|
3641
|
+
"Content-Type": "application/x-git-upload-pack-request",
|
|
3642
|
+
"User-Agent": "catmint-fs-git/0.1"
|
|
3643
|
+
},
|
|
3644
|
+
body
|
|
3645
|
+
});
|
|
3646
|
+
if (!response.ok) {
|
|
3647
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3648
|
+
}
|
|
3649
|
+
const responseData = new Uint8Array(await response.arrayBuffer());
|
|
3650
|
+
return this.parseFetchResponse(responseData);
|
|
3651
|
+
}
|
|
3652
|
+
buildFetchRequest(request) {
|
|
3653
|
+
const lines = [];
|
|
3654
|
+
for (const want of request.wants) {
|
|
3655
|
+
lines.push(writePktLine(`want ${want}
|
|
3656
|
+
`));
|
|
3657
|
+
}
|
|
3658
|
+
if (request.depth) {
|
|
3659
|
+
lines.push(writePktLine(`deepen ${request.depth}
|
|
3660
|
+
`));
|
|
3661
|
+
}
|
|
3662
|
+
lines.push(flushPkt());
|
|
3663
|
+
for (const have of request.haves) {
|
|
3664
|
+
lines.push(writePktLine(`have ${have}
|
|
3665
|
+
`));
|
|
3666
|
+
}
|
|
3667
|
+
lines.push(writePktLine("done\n"));
|
|
3668
|
+
return concat(...lines);
|
|
3669
|
+
}
|
|
3670
|
+
parseFetchResponse(data) {
|
|
3671
|
+
const acks = [];
|
|
3672
|
+
let offset = 0;
|
|
3673
|
+
while (offset < data.length) {
|
|
3674
|
+
if (data[offset] === 80 && data[offset + 1] === 65 && data[offset + 2] === 67 && data[offset + 3] === 75) {
|
|
3675
|
+
break;
|
|
3676
|
+
}
|
|
3677
|
+
const { line, nextOffset } = readPktLine(data, offset);
|
|
3678
|
+
offset = nextOffset;
|
|
3679
|
+
if (line === null) continue;
|
|
3680
|
+
const text = decode(line).trim();
|
|
3681
|
+
if (text.startsWith("ACK ")) {
|
|
3682
|
+
acks.push(text.substring(4).split(" ")[0]);
|
|
3683
|
+
}
|
|
3684
|
+
if (text === "NAK") {
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
const packfile = data.subarray(offset);
|
|
3688
|
+
return { packfile, acks };
|
|
3689
|
+
}
|
|
3690
|
+
// ---- Push --------------------------------------------------------------
|
|
3691
|
+
async push(url, request) {
|
|
3692
|
+
const body = this.buildPushRequest(request);
|
|
3693
|
+
const response = await fetch(`${url.replace(/\/$/, "")}/git-receive-pack`, {
|
|
3694
|
+
method: "POST",
|
|
3695
|
+
headers: {
|
|
3696
|
+
...this.headers,
|
|
3697
|
+
"Content-Type": "application/x-git-receive-pack-request",
|
|
3698
|
+
"User-Agent": "catmint-fs-git/0.1"
|
|
3699
|
+
},
|
|
3700
|
+
body
|
|
3701
|
+
});
|
|
3702
|
+
if (!response.ok) {
|
|
3703
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3704
|
+
}
|
|
3705
|
+
const responseData = new Uint8Array(await response.arrayBuffer());
|
|
3706
|
+
return this.parsePushResponse(responseData);
|
|
3707
|
+
}
|
|
3708
|
+
buildPushRequest(request) {
|
|
3709
|
+
const lines = [];
|
|
3710
|
+
for (let i = 0; i < request.updates.length; i++) {
|
|
3711
|
+
const update = request.updates[i];
|
|
3712
|
+
let line = `${update.oldOid} ${update.newOid} ${update.ref}`;
|
|
3713
|
+
if (i === 0) {
|
|
3714
|
+
line += "\0 report-status";
|
|
3715
|
+
}
|
|
3716
|
+
lines.push(writePktLine(line + "\n"));
|
|
3717
|
+
}
|
|
3718
|
+
lines.push(flushPkt());
|
|
3719
|
+
lines.push(request.packfile);
|
|
3720
|
+
return concat(...lines);
|
|
3721
|
+
}
|
|
3722
|
+
parsePushResponse(data) {
|
|
3723
|
+
const refs = [];
|
|
3724
|
+
let ok = true;
|
|
3725
|
+
let offset = 0;
|
|
3726
|
+
while (offset < data.length) {
|
|
3727
|
+
const { line, nextOffset } = readPktLine(data, offset);
|
|
3728
|
+
offset = nextOffset;
|
|
3729
|
+
if (line === null) continue;
|
|
3730
|
+
const text = decode(line).trim();
|
|
3731
|
+
if (text.startsWith("ok ")) {
|
|
3732
|
+
refs.push({ ref: text.substring(3), status: "ok" });
|
|
3733
|
+
} else if (text.startsWith("ng ")) {
|
|
3734
|
+
ok = false;
|
|
3735
|
+
const parts = text.substring(3).split(" ");
|
|
3736
|
+
const ref = parts[0];
|
|
3737
|
+
const reason = parts.slice(1).join(" ");
|
|
3738
|
+
refs.push({ ref, status: "rejected", reason });
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return { ok, refs };
|
|
3742
|
+
}
|
|
3743
|
+
};
|
|
3744
|
+
function readPktLine(data, offset) {
|
|
3745
|
+
if (offset + 4 > data.length) {
|
|
3746
|
+
return { line: null, nextOffset: data.length };
|
|
3747
|
+
}
|
|
3748
|
+
const lenHex = decode(data.subarray(offset, offset + 4));
|
|
3749
|
+
const len = parseInt(lenHex, 16);
|
|
3750
|
+
if (len === 0) {
|
|
3751
|
+
return { line: null, nextOffset: offset + 4 };
|
|
3752
|
+
}
|
|
3753
|
+
if (len < 4) {
|
|
3754
|
+
return { line: null, nextOffset: offset + 4 };
|
|
3755
|
+
}
|
|
3756
|
+
const line = data.subarray(offset + 4, offset + len);
|
|
3757
|
+
return { line, nextOffset: offset + len };
|
|
3758
|
+
}
|
|
3759
|
+
function writePktLine(text) {
|
|
3760
|
+
const data = encode(text);
|
|
3761
|
+
const len = data.length + 4;
|
|
3762
|
+
const lenHex = len.toString(16).padStart(4, "0");
|
|
3763
|
+
return concat(encode(lenHex), data);
|
|
3764
|
+
}
|
|
3765
|
+
function flushPkt() {
|
|
3766
|
+
return encode("0000");
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
// src/index.ts
|
|
3770
|
+
async function initRepository(layer, options = {}) {
|
|
3771
|
+
const bare = options.bare ?? false;
|
|
3772
|
+
const defaultBranch = options.defaultBranch || "main";
|
|
3773
|
+
const workDir = bare ? "/" : "/";
|
|
3774
|
+
const gitDir = bare ? "/" : "/.git";
|
|
3775
|
+
if (bare) {
|
|
3776
|
+
if (await layer.exists("/refs") || await layer.exists("/objects")) {
|
|
3777
|
+
throw new GitError("REPO_NOT_EMPTY", "Repository already exists");
|
|
3778
|
+
}
|
|
3779
|
+
} else {
|
|
3780
|
+
if (await layer.exists(gitDir)) {
|
|
3781
|
+
throw new GitError("REPO_NOT_EMPTY", "Repository already exists");
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
await layer.mkdir(`${gitDir}/objects`, { recursive: true });
|
|
3785
|
+
await layer.mkdir(`${gitDir}/refs/heads`, { recursive: true });
|
|
3786
|
+
await layer.mkdir(`${gitDir}/refs/tags`, { recursive: true });
|
|
3787
|
+
await layer.writeFile(
|
|
3788
|
+
`${gitDir}/HEAD`,
|
|
3789
|
+
encode(`ref: refs/heads/${defaultBranch}
|
|
3790
|
+
`)
|
|
3791
|
+
);
|
|
3792
|
+
const configContent = [
|
|
3793
|
+
"[core]",
|
|
3794
|
+
" repositoryformatversion = 0",
|
|
3795
|
+
" filemode = true",
|
|
3796
|
+
` bare = ${bare}`,
|
|
3797
|
+
""
|
|
3798
|
+
].join("\n");
|
|
3799
|
+
await layer.writeFile(`${gitDir}/config`, encode(configContent));
|
|
3800
|
+
const repo = new Repository(layer, workDir);
|
|
3801
|
+
await repo.load();
|
|
3802
|
+
return repo;
|
|
3803
|
+
}
|
|
3804
|
+
async function openRepository(layer) {
|
|
3805
|
+
const gitDir = "/.git";
|
|
3806
|
+
if (!await layer.exists(gitDir)) {
|
|
3807
|
+
throw new GitError("NOT_A_GIT_REPO", "Not a git repository");
|
|
3808
|
+
}
|
|
3809
|
+
const repo = new Repository(layer, "/");
|
|
3810
|
+
await repo.load();
|
|
3811
|
+
return repo;
|
|
3812
|
+
}
|
|
3813
|
+
async function cloneRepository(layer, options) {
|
|
3814
|
+
try {
|
|
3815
|
+
const entries = await layer.readdir("/");
|
|
3816
|
+
if (entries.length > 0) {
|
|
3817
|
+
throw new GitError("REPO_NOT_EMPTY", "Destination directory is not empty");
|
|
3818
|
+
}
|
|
3819
|
+
} catch (e) {
|
|
3820
|
+
if (e instanceof GitError) throw e;
|
|
3821
|
+
}
|
|
3822
|
+
const repo = await initRepository(layer, {
|
|
3823
|
+
defaultBranch: options.branch || "main",
|
|
3824
|
+
bare: options.bare
|
|
3825
|
+
});
|
|
3826
|
+
const transport = options.transport || new HttpTransport({
|
|
3827
|
+
headers: options.auth ? {
|
|
3828
|
+
Authorization: `Basic ${btoa(`${options.auth.username}:${options.auth.password}`)}`
|
|
3829
|
+
} : void 0
|
|
3830
|
+
});
|
|
3831
|
+
repo.setTransport(transport);
|
|
3832
|
+
await repo.addRemote("origin", options.url);
|
|
3833
|
+
await repo.fetch("origin", {
|
|
3834
|
+
depth: options.depth,
|
|
3835
|
+
transport
|
|
3836
|
+
});
|
|
3837
|
+
if (!options.bare) {
|
|
3838
|
+
const branch = options.branch || "main";
|
|
3839
|
+
const trackingRef = `refs/remotes/origin/${branch}`;
|
|
3840
|
+
try {
|
|
3841
|
+
const oid = await repo.refStore.resolveRef(trackingRef);
|
|
3842
|
+
await repo.refStore.writeRef(`refs/heads/${branch}`, oid);
|
|
3843
|
+
await repo.checkout(branch);
|
|
3844
|
+
} catch {
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return repo;
|
|
3848
|
+
}
|
|
3849
|
+
function httpTransport(options) {
|
|
3850
|
+
return new HttpTransport(options);
|
|
3851
|
+
}
|
|
3852
|
+
export {
|
|
3853
|
+
DiffEngine,
|
|
3854
|
+
GitConfig,
|
|
3855
|
+
GitError,
|
|
3856
|
+
GitIgnore,
|
|
3857
|
+
HttpTransport,
|
|
3858
|
+
IndexFile,
|
|
3859
|
+
MergeEngine,
|
|
3860
|
+
ObjectDB,
|
|
3861
|
+
RefStore,
|
|
3862
|
+
Repository,
|
|
3863
|
+
StashManager,
|
|
3864
|
+
cloneRepository,
|
|
3865
|
+
computeDiffHunks,
|
|
3866
|
+
formatIdentity,
|
|
3867
|
+
httpTransport,
|
|
3868
|
+
initRepository,
|
|
3869
|
+
openRepository,
|
|
3870
|
+
parseIdentity
|
|
3871
|
+
};
|