gent-cli 15.0.0 → 20.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.
@@ -0,0 +1,581 @@
1
+ /**
2
+ * ============================================================================
3
+ * Git Index - binary staging area (DIRC)
4
+ * ============================================================================
5
+ *
6
+ * PURPOSE:
7
+ * Read and write the index Git reads and writes, so `gent add` and
8
+ * `git status` agree without either re-scanning the other's state.
9
+ *
10
+ * VERSIONS:
11
+ * Read 2, 3 and 4. Write 2 whenever every entry is representable in it; an
12
+ * entry needing a v3-only flag (skip-worktree, intent-to-add) refuses with
13
+ * an actionable error rather than silently dropping the flag.
14
+ *
15
+ * ENTRY LAYOUT (SHA-256):
16
+ * ctime(8) mtime(8) dev(4) ino(4) mode(4) uid(4) gid(4) size(4)
17
+ * oid(32) flags(2) [extended flags(2)] path NUL-padded to a multiple of 8.
18
+ * v4 drops the padding and prefix-compresses the path against its
19
+ * predecessor.
20
+ *
21
+ * EXTENSIONS:
22
+ * An uppercase first signature byte means optional (TREE, REUC, UNTR, ...);
23
+ * lowercase means required (link, sdir). An unknown *required* extension
24
+ * refuses the operation before anything is written.
25
+ *
26
+ * RACY TIMESTAMPS:
27
+ * An entry whose mtime is not strictly older than the index file's own
28
+ * mtime cannot be trusted as clean on stat alone; isRacy() marks those so
29
+ * status falls back to comparing content.
30
+ *
31
+ * See docs/git-compat/format-contract.md section 8.
32
+ * ============================================================================
33
+ */
34
+
35
+ const fs = require('fs').promises;
36
+ const crypto = require('crypto');
37
+ const path = require('path');
38
+
39
+ const { OID_RAW_LENGTH, assertObjectId, MODE } = require('./git-objects');
40
+ const { withLock, readFileOrNull, writeAtomic } = require('./lockfile');
41
+ const { UnsupportedFeatureError, feature } = require('./feature-support');
42
+
43
+ const SIGNATURE = 0x44495243; // 'DIRC'
44
+ const SUPPORTED_READ_VERSIONS = new Set([2, 3, 4]);
45
+ const WRITE_VERSION = 2;
46
+ /** stat block (40) + oid (32) + flags (2) */
47
+ const FIXED_ENTRY_PREFIX = 40 + OID_RAW_LENGTH + 2;
48
+
49
+ const FLAG_ASSUME_VALID = 0x8000;
50
+ const FLAG_EXTENDED = 0x4000;
51
+ const FLAG_STAGE_MASK = 0x3000;
52
+ const FLAG_NAME_MASK = 0x0fff;
53
+ const EXT_FLAG_SKIP_WORKTREE = 0x4000;
54
+ const EXT_FLAG_INTENT_TO_ADD = 0x2000;
55
+
56
+ /** Extensions Gent understands well enough to keep meaningful. */
57
+ const UNDERSTOOD_EXTENSIONS = new Set(['TREE', 'REUC']);
58
+ /** Lowercase-signature extensions with a known meaning, for better errors. */
59
+ const KNOWN_REQUIRED_EXTENSIONS = {
60
+ link: 'index.split',
61
+ sdir: 'index.sparse'
62
+ };
63
+
64
+ class IndexError extends Error {
65
+ constructor(message) {
66
+ super(message);
67
+ this.name = 'IndexError';
68
+ this.code = 'GENT_BAD_INDEX';
69
+ }
70
+ }
71
+
72
+ /**
73
+ * One index entry. `stage` 0 is the normal staged state; 1/2/3 are the base,
74
+ * ours and theirs sides of an unresolved conflict.
75
+ */
76
+ class IndexEntry {
77
+ constructor(fields) {
78
+ this.ctimeSeconds = fields.ctimeSeconds || 0;
79
+ this.ctimeNanoseconds = fields.ctimeNanoseconds || 0;
80
+ this.mtimeSeconds = fields.mtimeSeconds || 0;
81
+ this.mtimeNanoseconds = fields.mtimeNanoseconds || 0;
82
+ this.dev = fields.dev || 0;
83
+ this.ino = fields.ino || 0;
84
+ this.mode = fields.mode;
85
+ this.uid = fields.uid || 0;
86
+ this.gid = fields.gid || 0;
87
+ this.size = fields.size || 0;
88
+ this.oid = assertObjectId(fields.oid, `index entry ${fields.path}`);
89
+ this.path = fields.path;
90
+ this.stage = fields.stage || 0;
91
+ this.assumeValid = Boolean(fields.assumeValid);
92
+ this.skipWorktree = Boolean(fields.skipWorktree);
93
+ this.intentToAdd = Boolean(fields.intentToAdd);
94
+ }
95
+
96
+ /**
97
+ * Build an entry from a worktree lstat. Pass a `{ bigint: true }` stat for
98
+ * true nanosecond fidelity; a plain stat degrades to millisecond
99
+ * precision, which only costs an occasional extra content comparison.
100
+ * @param {fs.Stats|fs.BigIntStats} stat
101
+ * @param {String} relativePath
102
+ * @param {String} oid
103
+ * @param {Number} mode - one of MODE.*
104
+ * @returns {IndexEntry}
105
+ */
106
+ static fromStat(stat, relativePath, oid, mode) {
107
+ return new IndexEntry({
108
+ ctimeSeconds: Math.floor(Number(stat.ctimeMs) / 1000),
109
+ ctimeNanoseconds: nanosecondsOf(stat.ctimeNs, stat.ctimeMs),
110
+ mtimeSeconds: Math.floor(Number(stat.mtimeMs) / 1000),
111
+ mtimeNanoseconds: nanosecondsOf(stat.mtimeNs, stat.mtimeMs),
112
+ dev: truncate32(stat.dev),
113
+ ino: truncate32(stat.ino),
114
+ mode,
115
+ uid: truncate32(stat.uid),
116
+ gid: truncate32(stat.gid),
117
+ size: truncate32(stat.size),
118
+ oid,
119
+ path: relativePath
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Cheap "unchanged" test. Deliberately conservative: any difference, or a
125
+ * racy timestamp, sends the caller to a content comparison.
126
+ * @param {fs.Stats} stat
127
+ * @returns {Boolean}
128
+ */
129
+ matchesStat(stat) {
130
+ if (this.assumeValid) return true;
131
+ return this.mtimeSeconds === Math.floor(Number(stat.mtimeMs) / 1000) &&
132
+ this.size === truncate32(stat.size) &&
133
+ this.ino === truncate32(stat.ino) &&
134
+ this.dev === truncate32(stat.dev) &&
135
+ this.mode === modeFromStat(stat, this.mode);
136
+ }
137
+
138
+ /**
139
+ * @param {Number} indexMtimeSeconds
140
+ * @returns {Boolean} true when stat data cannot prove cleanliness
141
+ */
142
+ isRacy(indexMtimeSeconds) {
143
+ return this.mtimeSeconds >= indexMtimeSeconds;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * @param {Number|BigInt} value
149
+ * @returns {Number}
150
+ */
151
+ function truncate32(value) {
152
+ return Number(BigInt.asUintN(32, BigInt(Math.trunc(Number(value || 0)))));
153
+ }
154
+
155
+ /**
156
+ * Sub-second part of a timestamp, preferring the bigint nanosecond field.
157
+ * @param {BigInt|undefined} nanoseconds
158
+ * @param {Number|BigInt} milliseconds
159
+ * @returns {Number}
160
+ */
161
+ function nanosecondsOf(nanoseconds, milliseconds) {
162
+ if (typeof nanoseconds === 'bigint') return Number(nanoseconds % BigInt(1e9));
163
+ return Math.round((Number(milliseconds || 0) % 1000) * 1e6);
164
+ }
165
+
166
+ /**
167
+ * Git's view of a worktree file's mode.
168
+ * @param {fs.Stats} stat
169
+ * @param {Number} [fallback] - used when the platform has no executable bit
170
+ * @returns {Number}
171
+ */
172
+ function modeFromStat(stat, fallback) {
173
+ if (stat.isSymbolicLink()) return MODE.SYMLINK;
174
+ if (stat.isDirectory()) return MODE.GITLINK;
175
+ if (fallback === MODE.EXECUTABLE || fallback === MODE.REGULAR) {
176
+ return (stat.mode & 0o111) ? MODE.EXECUTABLE : MODE.REGULAR;
177
+ }
178
+ return (stat.mode & 0o111) ? MODE.EXECUTABLE : MODE.REGULAR;
179
+ }
180
+
181
+ /**
182
+ * Index order: path bytes ascending, then stage ascending.
183
+ * @param {IndexEntry} a
184
+ * @param {IndexEntry} b
185
+ * @returns {Number}
186
+ */
187
+ function compareEntries(a, b) {
188
+ const cmp = Buffer.compare(Buffer.from(a.path, 'utf8'), Buffer.from(b.path, 'utf8'));
189
+ if (cmp !== 0) return cmp;
190
+ return a.stage - b.stage;
191
+ }
192
+
193
+ class GitIndex {
194
+ constructor() {
195
+ this.version = WRITE_VERSION;
196
+ /** @type {Array<IndexEntry>} kept sorted */
197
+ this.entries = [];
198
+ /** @type {Map<String, Buffer>} understood extensions, verbatim */
199
+ this.extensions = new Map();
200
+ /** @type {Number} mtime of the file we read, for racy detection */
201
+ this.readMtimeSeconds = 0;
202
+ this.dirty = false;
203
+ this.sourceBytes = null;
204
+ }
205
+
206
+ /**
207
+ * @param {String} indexPath
208
+ * @returns {Promise<GitIndex>} an empty index when the file does not exist
209
+ */
210
+ static async read(indexPath) {
211
+ const raw = await readFileOrNull(indexPath);
212
+ const index = raw ? GitIndex.parse(raw) : new GitIndex();
213
+ index.sourceBytes = raw;
214
+ if (raw) {
215
+ const stat = await fs.stat(indexPath).catch(() => null);
216
+ index.readMtimeSeconds = stat ? Math.floor(stat.mtimeMs / 1000) : 0;
217
+ }
218
+ return index;
219
+ }
220
+
221
+ /**
222
+ * @param {Buffer} buffer
223
+ * @returns {GitIndex}
224
+ */
225
+ static parse(buffer) {
226
+ if (buffer.length < 12 + OID_RAW_LENGTH) throw new IndexError('index is too short to be valid');
227
+ if (buffer.readUInt32BE(0) !== SIGNATURE) throw new IndexError('index does not start with DIRC');
228
+
229
+ const version = buffer.readUInt32BE(4);
230
+ if (!SUPPORTED_READ_VERSIONS.has(version)) {
231
+ throw new IndexError(`index version ${version} is not supported (Gent reads 2, 3 and 4)`);
232
+ }
233
+
234
+ const body = buffer.subarray(0, buffer.length - OID_RAW_LENGTH);
235
+ const stored = buffer.subarray(buffer.length - OID_RAW_LENGTH).toString('hex');
236
+ const actual = crypto.createHash('sha256').update(body).digest('hex');
237
+ if (stored !== actual) {
238
+ throw new IndexError(`index checksum mismatch: file says ${stored.slice(0, 12)}, contents hash to ${actual.slice(0, 12)}`);
239
+ }
240
+
241
+ const index = new GitIndex();
242
+ index.version = version;
243
+
244
+ const count = buffer.readUInt32BE(8);
245
+ let offset = 12;
246
+ let previousPath = '';
247
+
248
+ for (let i = 0; i < count; i++) {
249
+ const parsed = parseEntry(buffer, offset, version, previousPath);
250
+ index.entries.push(parsed.entry);
251
+ previousPath = parsed.entry.path;
252
+ offset = parsed.offset;
253
+ }
254
+
255
+ while (offset + 8 <= body.length) {
256
+ const signature = body.toString('latin1', offset, offset + 4);
257
+ const size = body.readUInt32BE(offset + 4);
258
+ const start = offset + 8;
259
+ const end = start + size;
260
+ if (end > body.length) throw new IndexError(`index extension '${signature}' claims ${size} bytes but the file ends first`);
261
+
262
+ const optional = /^[A-Z]/.test(signature);
263
+ if (!optional) {
264
+ const featureId = KNOWN_REQUIRED_EXTENSIONS[signature];
265
+ throw new UnsupportedFeatureError(
266
+ [featureId
267
+ ? feature(featureId)
268
+ : {
269
+ id: `index.${signature}`,
270
+ status: 'unsupported',
271
+ title: `Required index extension '${signature}'`,
272
+ detail: 'The index cannot be interpreted without it.',
273
+ remedy: 'Rewrite the index with Git, or remove the feature that produced this extension.'
274
+ }],
275
+ 'reading the index'
276
+ );
277
+ }
278
+ if (UNDERSTOOD_EXTENSIONS.has(signature)) {
279
+ index.extensions.set(signature, body.subarray(start, end));
280
+ }
281
+ offset = end;
282
+ }
283
+
284
+ index.entries.sort(compareEntries);
285
+ return index;
286
+ }
287
+
288
+ /**
289
+ * @param {String} filePath
290
+ * @param {Number} [stage]
291
+ * @returns {IndexEntry|undefined}
292
+ */
293
+ get(filePath, stage = 0) {
294
+ return this.entries.find(e => e.path === filePath && e.stage === stage);
295
+ }
296
+
297
+ /**
298
+ * Every entry for a path, any stage.
299
+ * @param {String} filePath
300
+ * @returns {Array<IndexEntry>}
301
+ */
302
+ getAll(filePath) {
303
+ return this.entries.filter(e => e.path === filePath);
304
+ }
305
+
306
+ /**
307
+ * Stage-0 entries only — the normal "what will be committed" view.
308
+ * @returns {Array<IndexEntry>}
309
+ */
310
+ staged() {
311
+ return this.entries.filter(e => e.stage === 0);
312
+ }
313
+
314
+ /**
315
+ * @returns {Map<String, {base?: IndexEntry, ours?: IndexEntry, theirs?: IndexEntry}>}
316
+ */
317
+ conflicts() {
318
+ const byPath = new Map();
319
+ for (const entry of this.entries) {
320
+ if (entry.stage === 0) continue;
321
+ if (!byPath.has(entry.path)) byPath.set(entry.path, {});
322
+ byPath.get(entry.path)[['', 'base', 'ours', 'theirs'][entry.stage]] = entry;
323
+ }
324
+ return byPath;
325
+ }
326
+
327
+ /**
328
+ * @returns {Boolean}
329
+ */
330
+ hasConflicts() {
331
+ return this.entries.some(e => e.stage !== 0);
332
+ }
333
+
334
+ /**
335
+ * Insert or replace a stage-0 entry, clearing any conflict on that path.
336
+ * @param {IndexEntry} entry
337
+ */
338
+ add(entry) {
339
+ this.remove(entry.path);
340
+ this.entries.push(entry);
341
+ this.entries.sort(compareEntries);
342
+ this._invalidateDerived(entry.path);
343
+ }
344
+
345
+ /**
346
+ * Insert an entry at a specific stage without touching the others.
347
+ * @param {IndexEntry} entry
348
+ */
349
+ addStage(entry) {
350
+ this.entries = this.entries.filter(e => !(e.path === entry.path && e.stage === entry.stage));
351
+ this.entries.push(entry);
352
+ this.entries.sort(compareEntries);
353
+ this._invalidateDerived(entry.path);
354
+ }
355
+
356
+ /**
357
+ * Remove every stage of a path.
358
+ * @param {String} filePath
359
+ * @returns {Boolean} whether anything was removed
360
+ */
361
+ remove(filePath) {
362
+ const before = this.entries.length;
363
+ this.entries = this.entries.filter(e => e.path !== filePath);
364
+ if (this.entries.length !== before) this._invalidateDerived(filePath);
365
+ return this.entries.length !== before;
366
+ }
367
+
368
+ /**
369
+ * Replace the conflict stages of a path with a resolved stage-0 entry.
370
+ * @param {IndexEntry} entry
371
+ */
372
+ resolve(entry) {
373
+ this.add(entry);
374
+ }
375
+
376
+ /**
377
+ * Cached trees and resolve-undo data stop being true the moment an entry
378
+ * moves. Dropping them entirely is correct and cheap; a stale cache-tree
379
+ * would make Git write a wrong commit.
380
+ */
381
+ _invalidateDerived() {
382
+ this.extensions.delete('TREE');
383
+ this.extensions.delete('REUC');
384
+ this.dirty = true;
385
+ }
386
+
387
+ /**
388
+ * @returns {Buffer}
389
+ */
390
+ serialize() {
391
+ const unrepresentable = this.entries.filter(e => e.skipWorktree || e.intentToAdd);
392
+ if (unrepresentable.length) {
393
+ throw new IndexError(
394
+ `cannot write this index as version ${WRITE_VERSION}: ` +
395
+ `${unrepresentable.map(e => e.path).join(', ')} carry skip-worktree or intent-to-add flags that only version 3 can express.\n` +
396
+ `Clear those flags with Git, or commit through Git for these paths.`
397
+ );
398
+ }
399
+
400
+ const header = Buffer.alloc(12);
401
+ header.writeUInt32BE(SIGNATURE, 0);
402
+ header.writeUInt32BE(WRITE_VERSION, 4);
403
+ header.writeUInt32BE(this.entries.length, 8);
404
+
405
+ const parts = [header];
406
+ for (const entry of [...this.entries].sort(compareEntries)) {
407
+ parts.push(serializeEntry(entry));
408
+ }
409
+
410
+ // Optional extensions Gent does not maintain are dropped rather than
411
+ // written back stale; TREE/REUC are only kept when still valid.
412
+ for (const [signature, data] of this.extensions) {
413
+ const head = Buffer.alloc(8);
414
+ head.write(signature, 0, 'latin1');
415
+ head.writeUInt32BE(data.length, 4);
416
+ parts.push(head, data);
417
+ }
418
+
419
+ const body = Buffer.concat(parts);
420
+ return Buffer.concat([body, crypto.createHash('sha256').update(body).digest()]);
421
+ }
422
+
423
+ /**
424
+ * Write under index.lock. Returns the mtime so callers can reason about
425
+ * racy entries immediately afterwards.
426
+ * @param {String} indexPath
427
+ * @returns {Promise<Number>} mtime in seconds
428
+ */
429
+ async write(indexPath) {
430
+ const bytes = this.serialize();
431
+ await withLock(indexPath, async (lock) => {
432
+ const current = await lock.readTarget();
433
+ if (!(current === null && this.sourceBytes === null) &&
434
+ !(current && this.sourceBytes && current.equals(this.sourceBytes))) {
435
+ throw new IndexError("index changed since it was read; retry the operation");
436
+ }
437
+ const recoveryPath = path.join(path.dirname(indexPath), 'gent', 'checkout-plan.json');
438
+ const recovery = await readFileOrNull(recoveryPath);
439
+ if (recovery) {
440
+ const record = JSON.parse(recovery.toString());
441
+ record.nextIndex = bytes.toString('base64');
442
+ await writeAtomic(recoveryPath, JSON.stringify(record));
443
+ }
444
+ await lock.write(bytes);
445
+ });
446
+ this.sourceBytes = bytes;
447
+ this.dirty = false;
448
+ const stat = await fs.stat(indexPath);
449
+ this.readMtimeSeconds = Math.floor(stat.mtimeMs / 1000);
450
+ return this.readMtimeSeconds;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * @param {Buffer} buffer
456
+ * @param {Number} offset
457
+ * @param {Number} version
458
+ * @param {String} previousPath
459
+ * @returns {{entry: IndexEntry, offset: Number}}
460
+ */
461
+ function parseEntry(buffer, offset, version, previousPath) {
462
+ const start = offset;
463
+ if (offset + FIXED_ENTRY_PREFIX > buffer.length) throw new IndexError('index entry is truncated');
464
+
465
+ const fields = {
466
+ ctimeSeconds: buffer.readUInt32BE(offset),
467
+ ctimeNanoseconds: buffer.readUInt32BE(offset + 4),
468
+ mtimeSeconds: buffer.readUInt32BE(offset + 8),
469
+ mtimeNanoseconds: buffer.readUInt32BE(offset + 12),
470
+ dev: buffer.readUInt32BE(offset + 16),
471
+ ino: buffer.readUInt32BE(offset + 20),
472
+ mode: buffer.readUInt32BE(offset + 24),
473
+ uid: buffer.readUInt32BE(offset + 28),
474
+ gid: buffer.readUInt32BE(offset + 32),
475
+ size: buffer.readUInt32BE(offset + 36)
476
+ };
477
+ offset += 40;
478
+
479
+ fields.oid = buffer.toString('hex', offset, offset + OID_RAW_LENGTH);
480
+ offset += OID_RAW_LENGTH;
481
+
482
+ const flags = buffer.readUInt16BE(offset);
483
+ offset += 2;
484
+
485
+ fields.assumeValid = Boolean(flags & FLAG_ASSUME_VALID);
486
+ fields.stage = (flags & FLAG_STAGE_MASK) >> 12;
487
+ const extended = Boolean(flags & FLAG_EXTENDED);
488
+
489
+ if (extended) {
490
+ if (version < 3) throw new IndexError('an extended-flag entry appeared in a version 2 index');
491
+ const extraFlags = buffer.readUInt16BE(offset);
492
+ offset += 2;
493
+ fields.skipWorktree = Boolean(extraFlags & EXT_FLAG_SKIP_WORKTREE);
494
+ fields.intentToAdd = Boolean(extraFlags & EXT_FLAG_INTENT_TO_ADD);
495
+ }
496
+
497
+ let nameLength = flags & FLAG_NAME_MASK;
498
+
499
+ if (version >= 4) {
500
+ const stripped = readVarint(buffer, offset);
501
+ offset = stripped.offset;
502
+ if (stripped.value > previousPath.length) {
503
+ throw new IndexError('version 4 path prefix removes more bytes than the previous path has');
504
+ }
505
+ const prefix = previousPath.slice(0, previousPath.length - stripped.value);
506
+
507
+ const nul = buffer.indexOf(0, offset);
508
+ if (nul < 0) throw new IndexError('index entry path is not NUL-terminated');
509
+ fields.path = prefix + buffer.toString('utf8', offset, nul);
510
+ offset = nul + 1;
511
+ } else {
512
+ if (nameLength === FLAG_NAME_MASK) {
513
+ const nul = buffer.indexOf(0, offset);
514
+ if (nul < 0) throw new IndexError('index entry path is not NUL-terminated');
515
+ nameLength = nul - offset;
516
+ }
517
+ fields.path = buffer.toString('utf8', offset, offset + nameLength);
518
+ const entrySize = (FIXED_ENTRY_PREFIX + (extended ? 2 : 0) + nameLength + 8) & ~7;
519
+ offset = start + entrySize;
520
+ }
521
+
522
+ if (fields.path.includes('\0')) throw new IndexError('index entry path contains NUL');
523
+ return { entry: new IndexEntry(fields), offset };
524
+ }
525
+
526
+ /**
527
+ * @param {IndexEntry} entry
528
+ * @returns {Buffer}
529
+ */
530
+ function serializeEntry(entry) {
531
+ const name = Buffer.from(entry.path, 'utf8');
532
+ const size = (FIXED_ENTRY_PREFIX + name.length + 8) & ~7;
533
+ const buffer = Buffer.alloc(size);
534
+
535
+ buffer.writeUInt32BE(entry.ctimeSeconds >>> 0, 0);
536
+ buffer.writeUInt32BE(entry.ctimeNanoseconds >>> 0, 4);
537
+ buffer.writeUInt32BE(entry.mtimeSeconds >>> 0, 8);
538
+ buffer.writeUInt32BE(entry.mtimeNanoseconds >>> 0, 12);
539
+ buffer.writeUInt32BE(entry.dev >>> 0, 16);
540
+ buffer.writeUInt32BE(entry.ino >>> 0, 20);
541
+ buffer.writeUInt32BE(entry.mode >>> 0, 24);
542
+ buffer.writeUInt32BE(entry.uid >>> 0, 28);
543
+ buffer.writeUInt32BE(entry.gid >>> 0, 32);
544
+ buffer.writeUInt32BE(entry.size >>> 0, 36);
545
+ Buffer.from(entry.oid, 'hex').copy(buffer, 40);
546
+
547
+ let flags = Math.min(name.length, FLAG_NAME_MASK);
548
+ flags |= (entry.stage & 0x3) << 12;
549
+ if (entry.assumeValid) flags |= FLAG_ASSUME_VALID;
550
+ buffer.writeUInt16BE(flags, 40 + OID_RAW_LENGTH);
551
+
552
+ name.copy(buffer, FIXED_ENTRY_PREFIX);
553
+ return buffer;
554
+ }
555
+
556
+ /**
557
+ * Git's offset varint (most-significant group first, with an implicit +1).
558
+ * @param {Buffer} buffer
559
+ * @param {Number} offset
560
+ * @returns {{value: Number, offset: Number}}
561
+ */
562
+ function readVarint(buffer, offset) {
563
+ let byte = buffer[offset++];
564
+ let value = byte & 0x7f;
565
+ while (byte & 0x80) {
566
+ byte = buffer[offset++];
567
+ if (byte === undefined) throw new IndexError('truncated varint in index');
568
+ value = ((value + 1) << 7) | (byte & 0x7f);
569
+ }
570
+ return { value, offset };
571
+ }
572
+
573
+ module.exports = {
574
+ GitIndex,
575
+ IndexEntry,
576
+ IndexError,
577
+ compareEntries,
578
+ modeFromStat,
579
+ WRITE_VERSION,
580
+ FIXED_ENTRY_PREFIX
581
+ };