gent-cli 15.0.0 → 21.0.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/README.md +1 -1
- package/package.json +7 -4
- package/src/commands/canonical.js +246 -0
- package/src/commands/clone.js +2 -13
- package/src/commands/pet.js +23 -45
- package/src/commands/push.js +1 -1
- package/src/commands/share.js +40 -15
- package/src/commands/web.js +30 -15
- package/src/index.js +70 -40
- package/src/utils/api-client.js +6 -9
- package/src/utils/attributes.js +280 -0
- package/src/utils/canonical-journal.js +122 -0
- package/src/utils/feature-support.js +92 -0
- package/src/utils/feature-support.json +210 -0
- package/src/utils/gent-ops.js +836 -0
- package/src/utils/git-config.js +606 -0
- package/src/utils/git-index.js +581 -0
- package/src/utils/git-objects.js +537 -0
- package/src/utils/ignore.js +365 -0
- package/src/utils/lockfile.js +219 -0
- package/src/utils/merge-engine.js +10 -7
- package/src/utils/merge-ops.js +283 -0
- package/src/utils/migrate.js +257 -0
- package/src/utils/object-store.js +332 -36
- package/src/utils/packfile.js +812 -0
- package/src/utils/refs.js +595 -0
- package/src/utils/repository.js +533 -0
- package/src/utils/smart-http.js +195 -0
- package/src/utils/stash-ops.js +355 -0
- package/src/utils/user-config.js +10 -0
- package/src/utils/web-urls.js +125 -0
- package/src/utils/worktree.js +676 -0
|
@@ -1,54 +1,350 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Object Store - the single interface to repository objects
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Read and write canonical Git objects. Every object id in Gent comes from
|
|
8
|
+
* here or from git-objects.js; no other module may hash or lay out objects.
|
|
9
|
+
*
|
|
10
|
+
* LAYOUT:
|
|
11
|
+
* <gitdir>/objects/<oid[0:2]>/<oid[2:]> zlib(framing)
|
|
12
|
+
* <gitdir>/objects/pack/*.pack|.idx read through the pack backend
|
|
13
|
+
*
|
|
14
|
+
* DURABILITY:
|
|
15
|
+
* Loose writes go to a temp file in the same directory, are fsync'd, then
|
|
16
|
+
* renamed. An object that already exists is left alone — content addressing
|
|
17
|
+
* makes a rewrite pointless and a partial rewrite dangerous.
|
|
18
|
+
*
|
|
19
|
+
* TRUST:
|
|
20
|
+
* read() always re-validates the framing. Untrusted sources (network,
|
|
21
|
+
* import, migration) additionally recompute the id: a mismatch is a
|
|
22
|
+
* corruption error, never a cache miss.
|
|
23
|
+
*
|
|
24
|
+
* See docs/git-compat/format-contract.md section 2.
|
|
25
|
+
* ============================================================================
|
|
4
26
|
*/
|
|
5
27
|
|
|
6
28
|
const fs = require('fs').promises;
|
|
29
|
+
const fsSync = require('fs');
|
|
7
30
|
const path = require('path');
|
|
8
|
-
const
|
|
31
|
+
const zlib = require('zlib');
|
|
32
|
+
const crypto = require('crypto');
|
|
33
|
+
const { promisify } = require('util');
|
|
9
34
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* @param {String} gentPath
|
|
13
|
-
* @param {String} hash
|
|
14
|
-
* @returns {String}
|
|
15
|
-
*/
|
|
16
|
-
function getObjectPath(gentPath, hash) {
|
|
17
|
-
return path.join(gentPath, 'objects', `${hash}.blob`);
|
|
18
|
-
}
|
|
35
|
+
const deflate = promisify(zlib.deflate);
|
|
36
|
+
const inflate = promisify(zlib.inflate);
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const {
|
|
39
|
+
OID_HEX_LENGTH,
|
|
40
|
+
assertObjectId,
|
|
41
|
+
isObjectId,
|
|
42
|
+
frameObject,
|
|
43
|
+
hashObject,
|
|
44
|
+
unframeObject,
|
|
45
|
+
parseTree,
|
|
46
|
+
parseCommit,
|
|
47
|
+
parseTag,
|
|
48
|
+
MalformedObjectError,
|
|
49
|
+
MAX_TAG_PEEL_DEPTH
|
|
50
|
+
} = require('./git-objects');
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
/** Raised when a requested object is genuinely absent (as opposed to broken). */
|
|
53
|
+
class ObjectNotFoundError extends Error {
|
|
54
|
+
constructor(oid) {
|
|
55
|
+
super(`object ${oid} not found`);
|
|
56
|
+
this.name = 'ObjectNotFoundError';
|
|
57
|
+
this.code = 'GENT_OBJECT_NOT_FOUND';
|
|
58
|
+
this.oid = oid;
|
|
31
59
|
}
|
|
32
60
|
}
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
class ObjectStore {
|
|
63
|
+
/**
|
|
64
|
+
* @param {String} objectsDir - <gitdir>/objects
|
|
65
|
+
* @param {Object} [options]
|
|
66
|
+
* @param {Object} [options.packBackend] - set by pack-store.js in Phase 4
|
|
67
|
+
*/
|
|
68
|
+
constructor(objectsDir, options = {}) {
|
|
69
|
+
this.objectsDir = objectsDir;
|
|
70
|
+
this.packBackend = options.packBackend || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {String} oid
|
|
75
|
+
* @returns {String}
|
|
76
|
+
*/
|
|
77
|
+
loosePath(oid) {
|
|
78
|
+
assertObjectId(oid);
|
|
79
|
+
return path.join(this.objectsDir, oid.slice(0, 2), oid.slice(2));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {String} oid
|
|
84
|
+
* @returns {Promise<Boolean>}
|
|
85
|
+
*/
|
|
86
|
+
async has(oid) {
|
|
87
|
+
if (!isObjectId(oid)) return false;
|
|
88
|
+
try {
|
|
89
|
+
await fs.access(this.loosePath(oid));
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
/* fall through to packs */
|
|
93
|
+
}
|
|
94
|
+
return this.packBackend ? this.packBackend.has(oid) : false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {String} oid
|
|
99
|
+
* @param {Object} [options]
|
|
100
|
+
* @param {Boolean} [options.trusted=true] - false re-verifies the id
|
|
101
|
+
* @returns {Promise<{oid, type, size, payload}>}
|
|
102
|
+
*/
|
|
103
|
+
async read(oid, options = {}) {
|
|
104
|
+
assertObjectId(oid);
|
|
105
|
+
const trusted = options.trusted !== false;
|
|
106
|
+
|
|
107
|
+
let framed = null;
|
|
108
|
+
try {
|
|
109
|
+
framed = await inflate(await fs.readFile(this.loosePath(oid)));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error.code !== 'ENOENT') {
|
|
112
|
+
throw new MalformedObjectError(`object ${oid} could not be decompressed: ${error.message}`, { oid });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (framed) {
|
|
117
|
+
const { type, size, payload } = unframeObject(framed);
|
|
118
|
+
if (!trusted && hashObject(type, payload) !== oid) {
|
|
119
|
+
throw new MalformedObjectError(`object ${oid} does not hash to its own name`, { oid });
|
|
120
|
+
}
|
|
121
|
+
return { oid, type, size, payload };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.packBackend) {
|
|
125
|
+
const packed = await this.packBackend.read(oid);
|
|
126
|
+
if (packed) {
|
|
127
|
+
if (!trusted && hashObject(packed.type, packed.payload) !== oid) {
|
|
128
|
+
throw new MalformedObjectError(`packed object ${oid} does not hash to its own name`, { oid });
|
|
129
|
+
}
|
|
130
|
+
return { oid, ...packed };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw new ObjectNotFoundError(oid);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {String} oid
|
|
139
|
+
* @param {String} expectedType
|
|
140
|
+
* @returns {Promise<{oid, type, size, payload}>}
|
|
141
|
+
*/
|
|
142
|
+
async readTyped(oid, expectedType) {
|
|
143
|
+
const object = await this.read(oid);
|
|
144
|
+
if (object.type !== expectedType) {
|
|
145
|
+
throw new MalformedObjectError(`expected ${oid} to be a ${expectedType}, found a ${object.type}`, { oid });
|
|
146
|
+
}
|
|
147
|
+
return object;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Type of an object without materialising it where the backend allows.
|
|
152
|
+
* @param {String} oid
|
|
153
|
+
* @returns {Promise<String>}
|
|
154
|
+
*/
|
|
155
|
+
async typeOf(oid) {
|
|
156
|
+
return (await this.read(oid)).type;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Store an object, computing its id.
|
|
161
|
+
* @param {String} type
|
|
162
|
+
* @param {Buffer} payload
|
|
163
|
+
* @returns {Promise<String>} oid
|
|
164
|
+
*/
|
|
165
|
+
async write(type, payload) {
|
|
166
|
+
const framed = frameObject(type, payload);
|
|
167
|
+
const oid = crypto.createHash('sha256').update(framed).digest('hex');
|
|
168
|
+
await this._writeLoose(oid, framed);
|
|
169
|
+
return oid;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Store an object whose id was supplied by an untrusted peer.
|
|
174
|
+
* @param {String} oid
|
|
175
|
+
* @param {String} type
|
|
176
|
+
* @param {Buffer} payload
|
|
177
|
+
* @returns {Promise<String>} oid
|
|
178
|
+
*/
|
|
179
|
+
async writeVerified(oid, type, payload) {
|
|
180
|
+
const framed = frameObject(type, payload);
|
|
181
|
+
const actual = crypto.createHash('sha256').update(framed).digest('hex');
|
|
182
|
+
if (actual !== assertObjectId(oid)) {
|
|
183
|
+
throw new MalformedObjectError(`refusing to store object under ${oid}: its contents hash to ${actual}`, { oid, actual });
|
|
184
|
+
}
|
|
185
|
+
await this._writeLoose(oid, framed);
|
|
186
|
+
return oid;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {String} oid
|
|
191
|
+
* @param {Buffer} framed
|
|
192
|
+
*/
|
|
193
|
+
async _writeLoose(oid, framed) {
|
|
194
|
+
const target = this.loosePath(oid);
|
|
195
|
+
try {
|
|
196
|
+
await fs.access(target);
|
|
197
|
+
return; // already present
|
|
198
|
+
} catch {
|
|
199
|
+
/* not present — write it */
|
|
200
|
+
}
|
|
42
201
|
|
|
43
|
-
|
|
44
|
-
|
|
202
|
+
const dir = path.dirname(target);
|
|
203
|
+
await fs.mkdir(dir, { recursive: true });
|
|
204
|
+
|
|
205
|
+
const compressed = await deflate(framed);
|
|
206
|
+
const tmp = path.join(dir, `tmp_obj_${process.pid}_${crypto.randomBytes(6).toString('hex')}`);
|
|
207
|
+
|
|
208
|
+
let handle;
|
|
209
|
+
try {
|
|
210
|
+
handle = await fs.open(tmp, 'wx', 0o444);
|
|
211
|
+
await handle.writeFile(compressed);
|
|
212
|
+
await handle.sync();
|
|
213
|
+
} finally {
|
|
214
|
+
if (handle) await handle.close();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await fs.rename(tmp, target);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
await fs.rm(tmp, { force: true });
|
|
221
|
+
if (error.code !== 'EEXIST') throw error; // lost a benign race
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Typed convenience readers ───────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {String} oid
|
|
229
|
+
* @returns {Promise<Buffer>}
|
|
230
|
+
*/
|
|
231
|
+
async readBlob(oid) {
|
|
232
|
+
return (await this.readTyped(oid, 'blob')).payload;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {String} oid
|
|
237
|
+
* @returns {Promise<Array>}
|
|
238
|
+
*/
|
|
239
|
+
async readTree(oid) {
|
|
240
|
+
return parseTree((await this.readTyped(oid, 'tree')).payload);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {String} oid
|
|
245
|
+
* @returns {Promise<Object>}
|
|
246
|
+
*/
|
|
247
|
+
async readCommit(oid) {
|
|
248
|
+
const object = await this.readTyped(oid, 'commit');
|
|
249
|
+
return { oid, ...parseCommit(object.payload) };
|
|
45
250
|
}
|
|
46
251
|
|
|
47
|
-
|
|
252
|
+
/**
|
|
253
|
+
* @param {String} oid
|
|
254
|
+
* @returns {Promise<Object>}
|
|
255
|
+
*/
|
|
256
|
+
async readTag(oid) {
|
|
257
|
+
const object = await this.readTyped(oid, 'tag');
|
|
258
|
+
return { oid, ...parseTag(object.payload) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Follow tag objects to the first non-tag object.
|
|
263
|
+
* @param {String} oid
|
|
264
|
+
* @returns {Promise<{oid: String, type: String}>}
|
|
265
|
+
*/
|
|
266
|
+
async peel(oid) {
|
|
267
|
+
const seen = new Set();
|
|
268
|
+
let current = assertObjectId(oid);
|
|
269
|
+
|
|
270
|
+
for (let depth = 0; depth <= MAX_TAG_PEEL_DEPTH; depth++) {
|
|
271
|
+
if (seen.has(current)) {
|
|
272
|
+
throw new MalformedObjectError(`tag chain from ${oid} is cyclic at ${current}`, { oid });
|
|
273
|
+
}
|
|
274
|
+
seen.add(current);
|
|
275
|
+
|
|
276
|
+
const object = await this.read(current);
|
|
277
|
+
if (object.type !== 'tag') return { oid: current, type: object.type };
|
|
278
|
+
current = parseTag(object.payload).object;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw new MalformedObjectError(`tag chain from ${oid} exceeds ${MAX_TAG_PEEL_DEPTH} levels`, { oid });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Every loose object id in the store.
|
|
286
|
+
* @returns {AsyncGenerator<String>}
|
|
287
|
+
*/
|
|
288
|
+
async *listLoose() {
|
|
289
|
+
let prefixes;
|
|
290
|
+
try {
|
|
291
|
+
prefixes = await fs.readdir(this.objectsDir, { withFileTypes: true });
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error.code === 'ENOENT') return;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const prefix of prefixes) {
|
|
298
|
+
if (!prefix.isDirectory() || !/^[0-9a-f]{2}$/.test(prefix.name)) continue;
|
|
299
|
+
const names = await fs.readdir(path.join(this.objectsDir, prefix.name));
|
|
300
|
+
for (const name of names) {
|
|
301
|
+
const oid = prefix.name + name;
|
|
302
|
+
if (oid.length === OID_HEX_LENGTH && isObjectId(oid)) yield oid;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Loose oids plus every oid the pack backend knows about.
|
|
309
|
+
* @returns {Promise<Set<String>>}
|
|
310
|
+
*/
|
|
311
|
+
async listAll() {
|
|
312
|
+
const all = new Set();
|
|
313
|
+
for await (const oid of this.listLoose()) all.add(oid);
|
|
314
|
+
if (this.packBackend) {
|
|
315
|
+
for (const oid of await this.packBackend.listAll()) all.add(oid);
|
|
316
|
+
}
|
|
317
|
+
return all;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Byte size of the loose object files, for `gent summary`.
|
|
322
|
+
* @returns {Promise<Number>}
|
|
323
|
+
*/
|
|
324
|
+
async looseByteSize() {
|
|
325
|
+
let total = 0;
|
|
326
|
+
for await (const oid of this.listLoose()) {
|
|
327
|
+
try {
|
|
328
|
+
total += (await fs.stat(this.loosePath(oid))).size;
|
|
329
|
+
} catch { /* raced with maintenance */ }
|
|
330
|
+
}
|
|
331
|
+
return total;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Synchronous existence probe used by preflight paths that cannot await.
|
|
337
|
+
* @param {String} objectsDir
|
|
338
|
+
* @param {String} oid
|
|
339
|
+
* @returns {Boolean}
|
|
340
|
+
*/
|
|
341
|
+
function looseObjectExistsSync(objectsDir, oid) {
|
|
342
|
+
if (!isObjectId(oid)) return false;
|
|
343
|
+
return fsSync.existsSync(path.join(objectsDir, oid.slice(0, 2), oid.slice(2)));
|
|
48
344
|
}
|
|
49
345
|
|
|
50
346
|
module.exports = {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
347
|
+
ObjectStore,
|
|
348
|
+
ObjectNotFoundError,
|
|
349
|
+
looseObjectExistsSync
|
|
54
350
|
};
|