UNPKG

446 kBJavaScriptView Raw
1import AsyncLock from 'async-lock';
2import Hash from 'sha.js/sha1.js';
3import crc32 from 'crc-32';
4import pako from 'pako';
5import pify from 'pify';
6import ignore from 'ignore';
7import cleanGitRef from 'clean-git-ref';
8import diff3Merge from 'diff3';
9
10/**
11 * @typedef {Object} GitProgressEvent
12 * @property {string} phase
13 * @property {number} loaded
14 * @property {number} total
15 */
16
17/**
18 * @callback ProgressCallback
19 * @param {GitProgressEvent} progress
20 * @returns {void | Promise<void>}
21 */
22
23/**
24 * @typedef {Object} GitHttpRequest
25 * @property {string} url - The URL to request
26 * @property {string} [method='GET'] - The HTTP method to use
27 * @property {Object<string, string>} [headers={}] - Headers to include in the HTTP request
28 * @property {Object} [agent] - An HTTP or HTTPS agent that manages connections for the HTTP client (Node.js only)
29 * @property {AsyncIterableIterator<Uint8Array>} [body] - An async iterator of Uint8Arrays that make up the body of POST requests
30 * @property {ProgressCallback} [onProgress] - Reserved for future use (emitting `GitProgressEvent`s)
31 * @property {object} [signal] - Reserved for future use (canceling a request)
32 */
33
34/**
35 * @typedef {Object} GitHttpResponse
36 * @property {string} url - The final URL that was fetched after any redirects
37 * @property {string} [method] - The HTTP method that was used
38 * @property {Object<string, string>} [headers] - HTTP response headers
39 * @property {AsyncIterableIterator<Uint8Array>} [body] - An async iterator of Uint8Arrays that make up the body of the response
40 * @property {number} statusCode - The HTTP status code
41 * @property {string} statusMessage - The HTTP status message
42 */
43
44/**
45 * @callback HttpFetch
46 * @param {GitHttpRequest} request
47 * @returns {Promise<GitHttpResponse>}
48 */
49
50/**
51 * @typedef {Object} HttpClient
52 * @property {HttpFetch} request
53 */
54
55/**
56 * A git commit object.
57 *
58 * @typedef {Object} CommitObject
59 * @property {string} message Commit message
60 * @property {string} tree SHA-1 object id of corresponding file tree
61 * @property {string[]} parent an array of zero or more SHA-1 object ids
62 * @property {Object} author
63 * @property {string} author.name The author's name
64 * @property {string} author.email The author's email
65 * @property {number} author.timestamp UTC Unix timestamp in seconds
66 * @property {number} author.timezoneOffset Timezone difference from UTC in minutes
67 * @property {Object} committer
68 * @property {string} committer.name The committer's name
69 * @property {string} committer.email The committer's email
70 * @property {number} committer.timestamp UTC Unix timestamp in seconds
71 * @property {number} committer.timezoneOffset Timezone difference from UTC in minutes
72 * @property {string} [gpgsig] PGP signature (if present)
73 */
74
75/**
76 * An entry from a git tree object. Files are called 'blobs' and directories are called 'trees'.
77 *
78 * @typedef {Object} TreeEntry
79 * @property {string} mode the 6 digit hexadecimal mode
80 * @property {string} path the name of the file or directory
81 * @property {string} oid the SHA-1 object id of the blob or tree
82 * @property {'commit'|'blob'|'tree'} type the type of object
83 */
84
85/**
86 * A git tree object. Trees represent a directory snapshot.
87 *
88 * @typedef {TreeEntry[]} TreeObject
89 */
90
91/**
92 * A git annotated tag object.
93 *
94 * @typedef {Object} TagObject
95 * @property {string} object SHA-1 object id of object being tagged
96 * @property {'blob' | 'tree' | 'commit' | 'tag'} type the type of the object being tagged
97 * @property {string} tag the tag name
98 * @property {Object} tagger
99 * @property {string} tagger.name the tagger's name
100 * @property {string} tagger.email the tagger's email
101 * @property {number} tagger.timestamp UTC Unix timestamp in seconds
102 * @property {number} tagger.timezoneOffset timezone difference from UTC in minutes
103 * @property {string} message tag message
104 * @property {string} [gpgsig] PGP signature (if present)
105 */
106
107/**
108 * @typedef {Object} ReadCommitResult
109 * @property {string} oid - SHA-1 object id of this commit
110 * @property {CommitObject} commit - the parsed commit object
111 * @property {string} payload - PGP signing payload
112 */
113
114/**
115 * @typedef {Object} ServerRef - This object has the following schema:
116 * @property {string} ref - The name of the ref
117 * @property {string} oid - The SHA-1 object id the ref points to
118 * @property {string} [target] - The target ref pointed to by a symbolic ref
119 * @property {string} [peeled] - If the oid is the SHA-1 object id of an annotated tag, this is the SHA-1 object id that the annotated tag points to
120 */
121
122/**
123 * @typedef Walker
124 * @property {Symbol} Symbol('GitWalkerSymbol')
125 */
126
127/**
128 * Normalized subset of filesystem `stat` data:
129 *
130 * @typedef {Object} Stat
131 * @property {number} ctimeSeconds
132 * @property {number} ctimeNanoseconds
133 * @property {number} mtimeSeconds
134 * @property {number} mtimeNanoseconds
135 * @property {number} dev
136 * @property {number} ino
137 * @property {number} mode
138 * @property {number} uid
139 * @property {number} gid
140 * @property {number} size
141 */
142
143/**
144 * The `WalkerEntry` is an interface that abstracts computing many common tree / blob stats.
145 *
146 * @typedef {Object} WalkerEntry
147 * @property {function(): Promise<'tree'|'blob'|'special'|'commit'>} type
148 * @property {function(): Promise<number>} mode
149 * @property {function(): Promise<string>} oid
150 * @property {function(): Promise<Uint8Array|void>} content
151 * @property {function(): Promise<Stat>} stat
152 */
153
154/**
155 * @typedef {Object} CallbackFsClient
156 * @property {function} readFile - https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback
157 * @property {function} writeFile - https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback
158 * @property {function} unlink - https://nodejs.org/api/fs.html#fs_fs_unlink_path_callback
159 * @property {function} readdir - https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback
160 * @property {function} mkdir - https://nodejs.org/api/fs.html#fs_fs_mkdir_path_mode_callback
161 * @property {function} rmdir - https://nodejs.org/api/fs.html#fs_fs_rmdir_path_callback
162 * @property {function} stat - https://nodejs.org/api/fs.html#fs_fs_stat_path_options_callback
163 * @property {function} lstat - https://nodejs.org/api/fs.html#fs_fs_lstat_path_options_callback
164 * @property {function} [readlink] - https://nodejs.org/api/fs.html#fs_fs_readlink_path_options_callback
165 * @property {function} [symlink] - https://nodejs.org/api/fs.html#fs_fs_symlink_target_path_type_callback
166 * @property {function} [chmod] - https://nodejs.org/api/fs.html#fs_fs_chmod_path_mode_callback
167 */
168
169/**
170 * @typedef {Object} PromiseFsClient
171 * @property {Object} promises
172 * @property {function} promises.readFile - https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_options
173 * @property {function} promises.writeFile - https://nodejs.org/api/fs.html#fs_fspromises_writefile_file_data_options
174 * @property {function} promises.unlink - https://nodejs.org/api/fs.html#fs_fspromises_unlink_path
175 * @property {function} promises.readdir - https://nodejs.org/api/fs.html#fs_fspromises_readdir_path_options
176 * @property {function} promises.mkdir - https://nodejs.org/api/fs.html#fs_fspromises_mkdir_path_options
177 * @property {function} promises.rmdir - https://nodejs.org/api/fs.html#fs_fspromises_rmdir_path
178 * @property {function} promises.stat - https://nodejs.org/api/fs.html#fs_fspromises_stat_path_options
179 * @property {function} promises.lstat - https://nodejs.org/api/fs.html#fs_fspromises_lstat_path_options
180 * @property {function} [promises.readlink] - https://nodejs.org/api/fs.html#fs_fspromises_readlink_path_options
181 * @property {function} [promises.symlink] - https://nodejs.org/api/fs.html#fs_fspromises_symlink_target_path_type
182 * @property {function} [promises.chmod] - https://nodejs.org/api/fs.html#fs_fspromises_chmod_path_mode
183 */
184
185/**
186 * @typedef {CallbackFsClient | PromiseFsClient} FsClient
187 */
188
189/**
190 * @callback MessageCallback
191 * @param {string} message
192 * @returns {void | Promise<void>}
193 */
194
195/**
196 * @typedef {Object} GitAuth
197 * @property {string} [username]
198 * @property {string} [password]
199 * @property {Object<string, string>} [headers]
200 * @property {boolean} [cancel] Tells git to throw a `UserCanceledError` (instead of an `HttpError`).
201 */
202
203/**
204 * @callback AuthCallback
205 * @param {string} url
206 * @param {GitAuth} auth Might have some values if the URL itself originally contained a username or password.
207 * @returns {GitAuth | void | Promise<GitAuth | void>}
208 */
209
210/**
211 * @callback AuthFailureCallback
212 * @param {string} url
213 * @param {GitAuth} auth The credentials that failed
214 * @returns {GitAuth | void | Promise<GitAuth | void>}
215 */
216
217/**
218 * @callback AuthSuccessCallback
219 * @param {string} url
220 * @param {GitAuth} auth
221 * @returns {void | Promise<void>}
222 */
223
224/**
225 * @typedef {Object} SignParams
226 * @property {string} payload - a plaintext message
227 * @property {string} secretKey - an 'ASCII armor' encoded PGP key (technically can actually contain _multiple_ keys)
228 */
229
230/**
231 * @callback SignCallback
232 * @param {SignParams} args
233 * @return {{signature: string} | Promise<{signature: string}>} - an 'ASCII armor' encoded "detached" signature
234 */
235
236/**
237 * @typedef {Object} MergeDriverParams
238 * @property {Array<string>} branches
239 * @property {Array<string>} contents
240 * @property {string} path
241 */
242
243/**
244 * @callback MergeDriverCallback
245 * @param {MergeDriverParams} args
246 * @return {{cleanMerge: boolean, mergedText: string} | Promise<{cleanMerge: boolean, mergedText: string}>}
247 */
248
249/**
250 * @callback WalkerMap
251 * @param {string} filename
252 * @param {WalkerEntry[]} entries
253 * @returns {Promise<any>}
254 */
255
256/**
257 * @callback WalkerReduce
258 * @param {any} parent
259 * @param {any[]} children
260 * @returns {Promise<any>}
261 */
262
263/**
264 * @callback WalkerIterateCallback
265 * @param {WalkerEntry[]} entries
266 * @returns {Promise<any[]>}
267 */
268
269/**
270 * @callback WalkerIterate
271 * @param {WalkerIterateCallback} walk
272 * @param {IterableIterator<WalkerEntry[]>} children
273 * @returns {Promise<any[]>}
274 */
275
276/**
277 * @typedef {Object} RefUpdateStatus
278 * @property {boolean} ok
279 * @property {string} error
280 */
281
282/**
283 * @typedef {Object} PushResult
284 * @property {boolean} ok
285 * @property {?string} error
286 * @property {Object<string, RefUpdateStatus>} refs
287 * @property {Object<string, string>} [headers]
288 */
289
290/**
291 * @typedef {0|1} HeadStatus
292 */
293
294/**
295 * @typedef {0|1|2} WorkdirStatus
296 */
297
298/**
299 * @typedef {0|1|2|3} StageStatus
300 */
301
302/**
303 * @typedef {[string, HeadStatus, WorkdirStatus, StageStatus]} StatusRow
304 */
305
306class BaseError extends Error {
307 constructor(message) {
308 super(message);
309 // Setting this here allows TS to infer that all git errors have a `caller` property and
310 // that its type is string.
311 this.caller = '';
312 }
313
314 toJSON() {
315 // Error objects aren't normally serializable. So we do something about that.
316 return {
317 code: this.code,
318 data: this.data,
319 caller: this.caller,
320 message: this.message,
321 stack: this.stack,
322 }
323 }
324
325 fromJSON(json) {
326 const e = new BaseError(json.message);
327 e.code = json.code;
328 e.data = json.data;
329 e.caller = json.caller;
330 e.stack = json.stack;
331 return e
332 }
333
334 get isIsomorphicGitError() {
335 return true
336 }
337}
338
339class UnmergedPathsError extends BaseError {
340 /**
341 * @param {Array<string>} filepaths
342 */
343 constructor(filepaths) {
344 super(
345 `Modifying the index is not possible because you have unmerged files: ${filepaths.toString}. Fix them up in the work tree, and then use 'git add/rm as appropriate to mark resolution and make a commit.`
346 );
347 this.code = this.name = UnmergedPathsError.code;
348 this.data = { filepaths };
349 }
350}
351/** @type {'UnmergedPathsError'} */
352UnmergedPathsError.code = 'UnmergedPathsError';
353
354class InternalError extends BaseError {
355 /**
356 * @param {string} message
357 */
358 constructor(message) {
359 super(
360 `An internal error caused this command to fail. Please file a bug report at https://github.com/isomorphic-git/isomorphic-git/issues with this error message: ${message}`
361 );
362 this.code = this.name = InternalError.code;
363 this.data = { message };
364 }
365}
366/** @type {'InternalError'} */
367InternalError.code = 'InternalError';
368
369class UnsafeFilepathError extends BaseError {
370 /**
371 * @param {string} filepath
372 */
373 constructor(filepath) {
374 super(`The filepath "${filepath}" contains unsafe character sequences`);
375 this.code = this.name = UnsafeFilepathError.code;
376 this.data = { filepath };
377 }
378}
379/** @type {'UnsafeFilepathError'} */
380UnsafeFilepathError.code = 'UnsafeFilepathError';
381
382// Modeled after https://github.com/tjfontaine/node-buffercursor
383// but with the goal of being much lighter weight.
384class BufferCursor {
385 constructor(buffer) {
386 this.buffer = buffer;
387 this._start = 0;
388 }
389
390 eof() {
391 return this._start >= this.buffer.length
392 }
393
394 tell() {
395 return this._start
396 }
397
398 seek(n) {
399 this._start = n;
400 }
401
402 slice(n) {
403 const r = this.buffer.slice(this._start, this._start + n);
404 this._start += n;
405 return r
406 }
407
408 toString(enc, length) {
409 const r = this.buffer.toString(enc, this._start, this._start + length);
410 this._start += length;
411 return r
412 }
413
414 write(value, length, enc) {
415 const r = this.buffer.write(value, this._start, length, enc);
416 this._start += length;
417 return r
418 }
419
420 copy(source, start, end) {
421 const r = source.copy(this.buffer, this._start, start, end);
422 this._start += r;
423 return r
424 }
425
426 readUInt8() {
427 const r = this.buffer.readUInt8(this._start);
428 this._start += 1;
429 return r
430 }
431
432 writeUInt8(value) {
433 const r = this.buffer.writeUInt8(value, this._start);
434 this._start += 1;
435 return r
436 }
437
438 readUInt16BE() {
439 const r = this.buffer.readUInt16BE(this._start);
440 this._start += 2;
441 return r
442 }
443
444 writeUInt16BE(value) {
445 const r = this.buffer.writeUInt16BE(value, this._start);
446 this._start += 2;
447 return r
448 }
449
450 readUInt32BE() {
451 const r = this.buffer.readUInt32BE(this._start);
452 this._start += 4;
453 return r
454 }
455
456 writeUInt32BE(value) {
457 const r = this.buffer.writeUInt32BE(value, this._start);
458 this._start += 4;
459 return r
460 }
461}
462
463function compareStrings(a, b) {
464 // https://stackoverflow.com/a/40355107/2168416
465 return -(a < b) || +(a > b)
466}
467
468function comparePath(a, b) {
469 // https://stackoverflow.com/a/40355107/2168416
470 return compareStrings(a.path, b.path)
471}
472
473/**
474 * From https://github.com/git/git/blob/master/Documentation/technical/index-format.txt
475 *
476 * 32-bit mode, split into (high to low bits)
477 *
478 * 4-bit object type
479 * valid values in binary are 1000 (regular file), 1010 (symbolic link)
480 * and 1110 (gitlink)
481 *
482 * 3-bit unused
483 *
484 * 9-bit unix permission. Only 0755 and 0644 are valid for regular files.
485 * Symbolic links and gitlinks have value 0 in this field.
486 */
487function normalizeMode(mode) {
488 // Note: BrowserFS will use -1 for "unknown"
489 // I need to make it non-negative for these bitshifts to work.
490 let type = mode > 0 ? mode >> 12 : 0;
491 // If it isn't valid, assume it as a "regular file"
492 // 0100 = directory
493 // 1000 = regular file
494 // 1010 = symlink
495 // 1110 = gitlink
496 if (
497 type !== 0b0100 &&
498 type !== 0b1000 &&
499 type !== 0b1010 &&
500 type !== 0b1110
501 ) {
502 type = 0b1000;
503 }
504 let permissions = mode & 0o777;
505 // Is the file executable? then 755. Else 644.
506 if (permissions & 0b001001001) {
507 permissions = 0o755;
508 } else {
509 permissions = 0o644;
510 }
511 // If it's not a regular file, scrub all permissions
512 if (type !== 0b1000) permissions = 0;
513 return (type << 12) + permissions
514}
515
516const MAX_UINT32 = 2 ** 32;
517
518function SecondsNanoseconds(
519 givenSeconds,
520 givenNanoseconds,
521 milliseconds,
522 date
523) {
524 if (givenSeconds !== undefined && givenNanoseconds !== undefined) {
525 return [givenSeconds, givenNanoseconds]
526 }
527 if (milliseconds === undefined) {
528 milliseconds = date.valueOf();
529 }
530 const seconds = Math.floor(milliseconds / 1000);
531 const nanoseconds = (milliseconds - seconds * 1000) * 1000000;
532 return [seconds, nanoseconds]
533}
534
535function normalizeStats(e) {
536 const [ctimeSeconds, ctimeNanoseconds] = SecondsNanoseconds(
537 e.ctimeSeconds,
538 e.ctimeNanoseconds,
539 e.ctimeMs,
540 e.ctime
541 );
542 const [mtimeSeconds, mtimeNanoseconds] = SecondsNanoseconds(
543 e.mtimeSeconds,
544 e.mtimeNanoseconds,
545 e.mtimeMs,
546 e.mtime
547 );
548
549 return {
550 ctimeSeconds: ctimeSeconds % MAX_UINT32,
551 ctimeNanoseconds: ctimeNanoseconds % MAX_UINT32,
552 mtimeSeconds: mtimeSeconds % MAX_UINT32,
553 mtimeNanoseconds: mtimeNanoseconds % MAX_UINT32,
554 dev: e.dev % MAX_UINT32,
555 ino: e.ino % MAX_UINT32,
556 mode: normalizeMode(e.mode % MAX_UINT32),
557 uid: e.uid % MAX_UINT32,
558 gid: e.gid % MAX_UINT32,
559 // size of -1 happens over a BrowserFS HTTP Backend that doesn't serve Content-Length headers
560 // (like the Karma webserver) because BrowserFS HTTP Backend uses HTTP HEAD requests to do fs.stat
561 size: e.size > -1 ? e.size % MAX_UINT32 : 0,
562 }
563}
564
565function toHex(buffer) {
566 let hex = '';
567 for (const byte of new Uint8Array(buffer)) {
568 if (byte < 16) hex += '0';
569 hex += byte.toString(16);
570 }
571 return hex
572}
573
574/* eslint-env node, browser */
575
576let supportsSubtleSHA1 = null;
577
578async function shasum(buffer) {
579 if (supportsSubtleSHA1 === null) {
580 supportsSubtleSHA1 = await testSubtleSHA1();
581 }
582 return supportsSubtleSHA1 ? subtleSHA1(buffer) : shasumSync(buffer)
583}
584
585// This is modeled after @dominictarr's "shasum" module,
586// but without the 'json-stable-stringify' dependency and
587// extra type-casting features.
588function shasumSync(buffer) {
589 return new Hash().update(buffer).digest('hex')
590}
591
592async function subtleSHA1(buffer) {
593 const hash = await crypto.subtle.digest('SHA-1', buffer);
594 return toHex(hash)
595}
596
597async function testSubtleSHA1() {
598 // I'm using a rather crude method of progressive enhancement, because
599 // some browsers that have crypto.subtle.digest don't actually implement SHA-1.
600 try {
601 const hash = await subtleSHA1(new Uint8Array([]));
602 if (hash === 'da39a3ee5e6b4b0d3255bfef95601890afd80709') return true
603 } catch (_) {
604 // no bother
605 }
606 return false
607}
608
609// Extract 1-bit assume-valid, 1-bit extended flag, 2-bit merge state flag, 12-bit path length flag
610function parseCacheEntryFlags(bits) {
611 return {
612 assumeValid: Boolean(bits & 0b1000000000000000),
613 extended: Boolean(bits & 0b0100000000000000),
614 stage: (bits & 0b0011000000000000) >> 12,
615 nameLength: bits & 0b0000111111111111,
616 }
617}
618
619function renderCacheEntryFlags(entry) {
620 const flags = entry.flags;
621 // 1-bit extended flag (must be zero in version 2)
622 flags.extended = false;
623 // 12-bit name length if the length is less than 0xFFF; otherwise 0xFFF
624 // is stored in this field.
625 flags.nameLength = Math.min(Buffer.from(entry.path).length, 0xfff);
626 return (
627 (flags.assumeValid ? 0b1000000000000000 : 0) +
628 (flags.extended ? 0b0100000000000000 : 0) +
629 ((flags.stage & 0b11) << 12) +
630 (flags.nameLength & 0b111111111111)
631 )
632}
633
634class GitIndex {
635 /*::
636 _entries: Map<string, CacheEntry>
637 _dirty: boolean // Used to determine if index needs to be saved to filesystem
638 */
639 constructor(entries, unmergedPaths) {
640 this._dirty = false;
641 this._unmergedPaths = unmergedPaths || new Set();
642 this._entries = entries || new Map();
643 }
644
645 _addEntry(entry) {
646 if (entry.flags.stage === 0) {
647 entry.stages = [entry];
648 this._entries.set(entry.path, entry);
649 this._unmergedPaths.delete(entry.path);
650 } else {
651 let existingEntry = this._entries.get(entry.path);
652 if (!existingEntry) {
653 this._entries.set(entry.path, entry);
654 existingEntry = entry;
655 }
656 existingEntry.stages[entry.flags.stage] = entry;
657 this._unmergedPaths.add(entry.path);
658 }
659 }
660
661 static async from(buffer) {
662 if (Buffer.isBuffer(buffer)) {
663 return GitIndex.fromBuffer(buffer)
664 } else if (buffer === null) {
665 return new GitIndex(null)
666 } else {
667 throw new InternalError('invalid type passed to GitIndex.from')
668 }
669 }
670
671 static async fromBuffer(buffer) {
672 if (buffer.length === 0) {
673 throw new InternalError('Index file is empty (.git/index)')
674 }
675
676 const index = new GitIndex();
677 const reader = new BufferCursor(buffer);
678 const magic = reader.toString('utf8', 4);
679 if (magic !== 'DIRC') {
680 throw new InternalError(`Invalid dircache magic file number: ${magic}`)
681 }
682
683 // Verify shasum after we ensured that the file has a magic number
684 const shaComputed = await shasum(buffer.slice(0, -20));
685 const shaClaimed = buffer.slice(-20).toString('hex');
686 if (shaClaimed !== shaComputed) {
687 throw new InternalError(
688 `Invalid checksum in GitIndex buffer: expected ${shaClaimed} but saw ${shaComputed}`
689 )
690 }
691
692 const version = reader.readUInt32BE();
693 if (version !== 2) {
694 throw new InternalError(`Unsupported dircache version: ${version}`)
695 }
696 const numEntries = reader.readUInt32BE();
697 let i = 0;
698 while (!reader.eof() && i < numEntries) {
699 const entry = {};
700 entry.ctimeSeconds = reader.readUInt32BE();
701 entry.ctimeNanoseconds = reader.readUInt32BE();
702 entry.mtimeSeconds = reader.readUInt32BE();
703 entry.mtimeNanoseconds = reader.readUInt32BE();
704 entry.dev = reader.readUInt32BE();
705 entry.ino = reader.readUInt32BE();
706 entry.mode = reader.readUInt32BE();
707 entry.uid = reader.readUInt32BE();
708 entry.gid = reader.readUInt32BE();
709 entry.size = reader.readUInt32BE();
710 entry.oid = reader.slice(20).toString('hex');
711 const flags = reader.readUInt16BE();
712 entry.flags = parseCacheEntryFlags(flags);
713 // TODO: handle if (version === 3 && entry.flags.extended)
714 const pathlength = buffer.indexOf(0, reader.tell() + 1) - reader.tell();
715 if (pathlength < 1) {
716 throw new InternalError(`Got a path length of: ${pathlength}`)
717 }
718 // TODO: handle pathnames larger than 12 bits
719 entry.path = reader.toString('utf8', pathlength);
720
721 // Prevent malicious paths like "..\foo"
722 if (entry.path.includes('..\\') || entry.path.includes('../')) {
723 throw new UnsafeFilepathError(entry.path)
724 }
725
726 // The next bit is awkward. We expect 1 to 8 null characters
727 // such that the total size of the entry is a multiple of 8 bits.
728 // (Hence subtract 12 bytes for the header.)
729 let padding = 8 - ((reader.tell() - 12) % 8);
730 if (padding === 0) padding = 8;
731 while (padding--) {
732 const tmp = reader.readUInt8();
733 if (tmp !== 0) {
734 throw new InternalError(
735 `Expected 1-8 null characters but got '${tmp}' after ${entry.path}`
736 )
737 } else if (reader.eof()) {
738 throw new InternalError('Unexpected end of file')
739 }
740 }
741 // end of awkward part
742 entry.stages = [];
743
744 index._addEntry(entry);
745
746 i++;
747 }
748 return index
749 }
750
751 get unmergedPaths() {
752 return [...this._unmergedPaths]
753 }
754
755 get entries() {
756 return [...this._entries.values()].sort(comparePath)
757 }
758
759 get entriesMap() {
760 return this._entries
761 }
762
763 get entriesFlat() {
764 return [...this.entries].flatMap(entry => {
765 return entry.stages.length > 1 ? entry.stages.filter(x => x) : entry
766 })
767 }
768
769 *[Symbol.iterator]() {
770 for (const entry of this.entries) {
771 yield entry;
772 }
773 }
774
775 insert({ filepath, stats, oid, stage = 0 }) {
776 if (!stats) {
777 stats = {
778 ctimeSeconds: 0,
779 ctimeNanoseconds: 0,
780 mtimeSeconds: 0,
781 mtimeNanoseconds: 0,
782 dev: 0,
783 ino: 0,
784 mode: 0,
785 uid: 0,
786 gid: 0,
787 size: 0,
788 };
789 }
790 stats = normalizeStats(stats);
791 const bfilepath = Buffer.from(filepath);
792 const entry = {
793 ctimeSeconds: stats.ctimeSeconds,
794 ctimeNanoseconds: stats.ctimeNanoseconds,
795 mtimeSeconds: stats.mtimeSeconds,
796 mtimeNanoseconds: stats.mtimeNanoseconds,
797 dev: stats.dev,
798 ino: stats.ino,
799 // We provide a fallback value for `mode` here because not all fs
800 // implementations assign it, but we use it in GitTree.
801 // '100644' is for a "regular non-executable file"
802 mode: stats.mode || 0o100644,
803 uid: stats.uid,
804 gid: stats.gid,
805 size: stats.size,
806 path: filepath,
807 oid: oid,
808 flags: {
809 assumeValid: false,
810 extended: false,
811 stage,
812 nameLength: bfilepath.length < 0xfff ? bfilepath.length : 0xfff,
813 },
814 stages: [],
815 };
816
817 this._addEntry(entry);
818
819 this._dirty = true;
820 }
821
822 delete({ filepath }) {
823 if (this._entries.has(filepath)) {
824 this._entries.delete(filepath);
825 } else {
826 for (const key of this._entries.keys()) {
827 if (key.startsWith(filepath + '/')) {
828 this._entries.delete(key);
829 }
830 }
831 }
832
833 if (this._unmergedPaths.has(filepath)) {
834 this._unmergedPaths.delete(filepath);
835 }
836 this._dirty = true;
837 }
838
839 clear() {
840 this._entries.clear();
841 this._dirty = true;
842 }
843
844 has({ filepath }) {
845 return this._entries.has(filepath)
846 }
847
848 render() {
849 return this.entries
850 .map(entry => `${entry.mode.toString(8)} ${entry.oid} ${entry.path}`)
851 .join('\n')
852 }
853
854 static async _entryToBuffer(entry) {
855 const bpath = Buffer.from(entry.path);
856 // the fixed length + the filename + at least one null char => align by 8
857 const length = Math.ceil((62 + bpath.length + 1) / 8) * 8;
858 const written = Buffer.alloc(length);
859 const writer = new BufferCursor(written);
860 const stat = normalizeStats(entry);
861 writer.writeUInt32BE(stat.ctimeSeconds);
862 writer.writeUInt32BE(stat.ctimeNanoseconds);
863 writer.writeUInt32BE(stat.mtimeSeconds);
864 writer.writeUInt32BE(stat.mtimeNanoseconds);
865 writer.writeUInt32BE(stat.dev);
866 writer.writeUInt32BE(stat.ino);
867 writer.writeUInt32BE(stat.mode);
868 writer.writeUInt32BE(stat.uid);
869 writer.writeUInt32BE(stat.gid);
870 writer.writeUInt32BE(stat.size);
871 writer.write(entry.oid, 20, 'hex');
872 writer.writeUInt16BE(renderCacheEntryFlags(entry));
873 writer.write(entry.path, bpath.length, 'utf8');
874 return written
875 }
876
877 async toObject() {
878 const header = Buffer.alloc(12);
879 const writer = new BufferCursor(header);
880 writer.write('DIRC', 4, 'utf8');
881 writer.writeUInt32BE(2);
882 writer.writeUInt32BE(this.entriesFlat.length);
883
884 let entryBuffers = [];
885 for (const entry of this.entries) {
886 entryBuffers.push(GitIndex._entryToBuffer(entry));
887 if (entry.stages.length > 1) {
888 for (const stage of entry.stages) {
889 if (stage && stage !== entry) {
890 entryBuffers.push(GitIndex._entryToBuffer(stage));
891 }
892 }
893 }
894 }
895 entryBuffers = await Promise.all(entryBuffers);
896
897 const body = Buffer.concat(entryBuffers);
898 const main = Buffer.concat([header, body]);
899 const sum = await shasum(main);
900 return Buffer.concat([main, Buffer.from(sum, 'hex')])
901 }
902}
903
904function compareStats(entry, stats) {
905 // Comparison based on the description in Paragraph 4 of
906 // https://www.kernel.org/pub/software/scm/git/docs/technical/racy-git.txt
907 const e = normalizeStats(entry);
908 const s = normalizeStats(stats);
909 const staleness =
910 e.mode !== s.mode ||
911 e.mtimeSeconds !== s.mtimeSeconds ||
912 e.ctimeSeconds !== s.ctimeSeconds ||
913 e.uid !== s.uid ||
914 e.gid !== s.gid ||
915 e.ino !== s.ino ||
916 e.size !== s.size;
917 return staleness
918}
919
920// import LockManager from 'travix-lock-manager'
921
922// import Lock from '../utils.js'
923
924// const lm = new LockManager()
925let lock = null;
926
927const IndexCache = Symbol('IndexCache');
928
929function createCache() {
930 return {
931 map: new Map(),
932 stats: new Map(),
933 }
934}
935
936async function updateCachedIndexFile(fs, filepath, cache) {
937 const stat = await fs.lstat(filepath);
938 const rawIndexFile = await fs.read(filepath);
939 const index = await GitIndex.from(rawIndexFile);
940 // cache the GitIndex object so we don't need to re-read it every time.
941 cache.map.set(filepath, index);
942 // Save the stat data for the index so we know whether the cached file is stale (modified by an outside process).
943 cache.stats.set(filepath, stat);
944}
945
946// Determine whether our copy of the index file is stale
947async function isIndexStale(fs, filepath, cache) {
948 const savedStats = cache.stats.get(filepath);
949 if (savedStats === undefined) return true
950 const currStats = await fs.lstat(filepath);
951 if (savedStats === null) return false
952 if (currStats === null) return false
953 return compareStats(savedStats, currStats)
954}
955
956class GitIndexManager {
957 /**
958 *
959 * @param {object} opts
960 * @param {import('../models/FileSystem.js').FileSystem} opts.fs
961 * @param {string} opts.gitdir
962 * @param {object} opts.cache
963 * @param {bool} opts.allowUnmerged
964 * @param {function(GitIndex): any} closure
965 */
966 static async acquire({ fs, gitdir, cache, allowUnmerged = true }, closure) {
967 if (!cache[IndexCache]) cache[IndexCache] = createCache();
968
969 const filepath = `${gitdir}/index`;
970 if (lock === null) lock = new AsyncLock({ maxPending: Infinity });
971 let result;
972 let unmergedPaths = [];
973 await lock.acquire(filepath, async () => {
974 // Acquire a file lock while we're reading the index
975 // to make sure other processes aren't writing to it
976 // simultaneously, which could result in a corrupted index.
977 // const fileLock = await Lock(filepath)
978 if (await isIndexStale(fs, filepath, cache[IndexCache])) {
979 await updateCachedIndexFile(fs, filepath, cache[IndexCache]);
980 }
981 const index = cache[IndexCache].map.get(filepath);
982 unmergedPaths = index.unmergedPaths;
983
984 if (unmergedPaths.length && !allowUnmerged)
985 throw new UnmergedPathsError(unmergedPaths)
986
987 result = await closure(index);
988 if (index._dirty) {
989 // Acquire a file lock while we're writing the index file
990 // let fileLock = await Lock(filepath)
991 const buffer = await index.toObject();
992 await fs.write(filepath, buffer);
993 // Update cached stat value
994 cache[IndexCache].stats.set(filepath, await fs.lstat(filepath));
995 index._dirty = false;
996 }
997 });
998
999 return result
1000 }
1001}
1002
1003function basename(path) {
1004 const last = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
1005 if (last > -1) {
1006 path = path.slice(last + 1);
1007 }
1008 return path
1009}
1010
1011function dirname(path) {
1012 const last = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
1013 if (last === -1) return '.'
1014 if (last === 0) return '/'
1015 return path.slice(0, last)
1016}
1017
1018/*::
1019type Node = {
1020 type: string,
1021 fullpath: string,
1022 basename: string,
1023 metadata: Object, // mode, oid
1024 parent?: Node,
1025 children: Array<Node>
1026}
1027*/
1028
1029function flatFileListToDirectoryStructure(files) {
1030 const inodes = new Map();
1031 const mkdir = function(name) {
1032 if (!inodes.has(name)) {
1033 const dir = {
1034 type: 'tree',
1035 fullpath: name,
1036 basename: basename(name),
1037 metadata: {},
1038 children: [],
1039 };
1040 inodes.set(name, dir);
1041 // This recursively generates any missing parent folders.
1042 // We do it after we've added the inode to the set so that
1043 // we don't recurse infinitely trying to create the root '.' dirname.
1044 dir.parent = mkdir(dirname(name));
1045 if (dir.parent && dir.parent !== dir) dir.parent.children.push(dir);
1046 }
1047 return inodes.get(name)
1048 };
1049
1050 const mkfile = function(name, metadata) {
1051 if (!inodes.has(name)) {
1052 const file = {
1053 type: 'blob',
1054 fullpath: name,
1055 basename: basename(name),
1056 metadata: metadata,
1057 // This recursively generates any missing parent folders.
1058 parent: mkdir(dirname(name)),
1059 children: [],
1060 };
1061 if (file.parent) file.parent.children.push(file);
1062 inodes.set(name, file);
1063 }
1064 return inodes.get(name)
1065 };
1066
1067 mkdir('.');
1068 for (const file of files) {
1069 mkfile(file.path, file);
1070 }
1071 return inodes
1072}
1073
1074/**
1075 *
1076 * @param {number} mode
1077 */
1078function mode2type(mode) {
1079 // prettier-ignore
1080 switch (mode) {
1081 case 0o040000: return 'tree'
1082 case 0o100644: return 'blob'
1083 case 0o100755: return 'blob'
1084 case 0o120000: return 'blob'
1085 case 0o160000: return 'commit'
1086 }
1087 throw new InternalError(`Unexpected GitTree entry mode: ${mode.toString(8)}`)
1088}
1089
1090class GitWalkerIndex {
1091 constructor({ fs, gitdir, cache }) {
1092 this.treePromise = GitIndexManager.acquire(
1093 { fs, gitdir, cache },
1094 async function(index) {
1095 return flatFileListToDirectoryStructure(index.entries)
1096 }
1097 );
1098 const walker = this;
1099 this.ConstructEntry = class StageEntry {
1100 constructor(fullpath) {
1101 this._fullpath = fullpath;
1102 this._type = false;
1103 this._mode = false;
1104 this._stat = false;
1105 this._oid = false;
1106 }
1107
1108 async type() {
1109 return walker.type(this)
1110 }
1111
1112 async mode() {
1113 return walker.mode(this)
1114 }
1115
1116 async stat() {
1117 return walker.stat(this)
1118 }
1119
1120 async content() {
1121 return walker.content(this)
1122 }
1123
1124 async oid() {
1125 return walker.oid(this)
1126 }
1127 };
1128 }
1129
1130 async readdir(entry) {
1131 const filepath = entry._fullpath;
1132 const tree = await this.treePromise;
1133 const inode = tree.get(filepath);
1134 if (!inode) return null
1135 if (inode.type === 'blob') return null
1136 if (inode.type !== 'tree') {
1137 throw new Error(`ENOTDIR: not a directory, scandir '${filepath}'`)
1138 }
1139 const names = inode.children.map(inode => inode.fullpath);
1140 names.sort(compareStrings);
1141 return names
1142 }
1143
1144 async type(entry) {
1145 if (entry._type === false) {
1146 await entry.stat();
1147 }
1148 return entry._type
1149 }
1150
1151 async mode(entry) {
1152 if (entry._mode === false) {
1153 await entry.stat();
1154 }
1155 return entry._mode
1156 }
1157
1158 async stat(entry) {
1159 if (entry._stat === false) {
1160 const tree = await this.treePromise;
1161 const inode = tree.get(entry._fullpath);
1162 if (!inode) {
1163 throw new Error(
1164 `ENOENT: no such file or directory, lstat '${entry._fullpath}'`
1165 )
1166 }
1167 const stats = inode.type === 'tree' ? {} : normalizeStats(inode.metadata);
1168 entry._type = inode.type === 'tree' ? 'tree' : mode2type(stats.mode);
1169 entry._mode = stats.mode;
1170 if (inode.type === 'tree') {
1171 entry._stat = undefined;
1172 } else {
1173 entry._stat = stats;
1174 }
1175 }
1176 return entry._stat
1177 }
1178
1179 async content(_entry) {
1180 // Cannot get content for an index entry
1181 }
1182
1183 async oid(entry) {
1184 if (entry._oid === false) {
1185 const tree = await this.treePromise;
1186 const inode = tree.get(entry._fullpath);
1187 entry._oid = inode.metadata.oid;
1188 }
1189 return entry._oid
1190 }
1191}
1192
1193// This is part of an elaborate system to facilitate code-splitting / tree-shaking.
1194// commands/walk.js can depend on only this, and the actual Walker classes exported
1195// can be opaque - only having a single property (this symbol) that is not enumerable,
1196// and thus the constructor can be passed as an argument to walk while being "unusable"
1197// outside of it.
1198const GitWalkSymbol = Symbol('GitWalkSymbol');
1199
1200// @ts-check
1201
1202/**
1203 * @returns {Walker}
1204 */
1205function STAGE() {
1206 const o = Object.create(null);
1207 Object.defineProperty(o, GitWalkSymbol, {
1208 value: function({ fs, gitdir, cache }) {
1209 return new GitWalkerIndex({ fs, gitdir, cache })
1210 },
1211 });
1212 Object.freeze(o);
1213 return o
1214}
1215
1216// @ts-check
1217
1218class NotFoundError extends BaseError {
1219 /**
1220 * @param {string} what
1221 */
1222 constructor(what) {
1223 super(`Could not find ${what}.`);
1224 this.code = this.name = NotFoundError.code;
1225 this.data = { what };
1226 }
1227}
1228/** @type {'NotFoundError'} */
1229NotFoundError.code = 'NotFoundError';
1230
1231class ObjectTypeError extends BaseError {
1232 /**
1233 * @param {string} oid
1234 * @param {'blob'|'commit'|'tag'|'tree'} actual
1235 * @param {'blob'|'commit'|'tag'|'tree'} expected
1236 * @param {string} [filepath]
1237 */
1238 constructor(oid, actual, expected, filepath) {
1239 super(
1240 `Object ${oid} ${
1241 filepath ? `at ${filepath}` : ''
1242 }was anticipated to be a ${expected} but it is a ${actual}.`
1243 );
1244 this.code = this.name = ObjectTypeError.code;
1245 this.data = { oid, actual, expected, filepath };
1246 }
1247}
1248/** @type {'ObjectTypeError'} */
1249ObjectTypeError.code = 'ObjectTypeError';
1250
1251class InvalidOidError extends BaseError {
1252 /**
1253 * @param {string} value
1254 */
1255 constructor(value) {
1256 super(`Expected a 40-char hex object id but saw "${value}".`);
1257 this.code = this.name = InvalidOidError.code;
1258 this.data = { value };
1259 }
1260}
1261/** @type {'InvalidOidError'} */
1262InvalidOidError.code = 'InvalidOidError';
1263
1264class NoRefspecError extends BaseError {
1265 /**
1266 * @param {string} remote
1267 */
1268 constructor(remote) {
1269 super(`Could not find a fetch refspec for remote "${remote}". Make sure the config file has an entry like the following:
1270[remote "${remote}"]
1271\tfetch = +refs/heads/*:refs/remotes/origin/*
1272`);
1273 this.code = this.name = NoRefspecError.code;
1274 this.data = { remote };
1275 }
1276}
1277/** @type {'NoRefspecError'} */
1278NoRefspecError.code = 'NoRefspecError';
1279
1280class GitPackedRefs {
1281 constructor(text) {
1282 this.refs = new Map();
1283 this.parsedConfig = [];
1284 if (text) {
1285 let key = null;
1286 this.parsedConfig = text
1287 .trim()
1288 .split('\n')
1289 .map(line => {
1290 if (/^\s*#/.test(line)) {
1291 return { line, comment: true }
1292 }
1293 const i = line.indexOf(' ');
1294 if (line.startsWith('^')) {
1295 // This is a oid for the commit associated with the annotated tag immediately preceding this line.
1296 // Trim off the '^'
1297 const value = line.slice(1);
1298 // The tagname^{} syntax is based on the output of `git show-ref --tags -d`
1299 this.refs.set(key + '^{}', value);
1300 return { line, ref: key, peeled: value }
1301 } else {
1302 // This is an oid followed by the ref name
1303 const value = line.slice(0, i);
1304 key = line.slice(i + 1);
1305 this.refs.set(key, value);
1306 return { line, ref: key, oid: value }
1307 }
1308 });
1309 }
1310 return this
1311 }
1312
1313 static from(text) {
1314 return new GitPackedRefs(text)
1315 }
1316
1317 delete(ref) {
1318 this.parsedConfig = this.parsedConfig.filter(entry => entry.ref !== ref);
1319 this.refs.delete(ref);
1320 }
1321
1322 toString() {
1323 return this.parsedConfig.map(({ line }) => line).join('\n') + '\n'
1324 }
1325}
1326
1327class GitRefSpec {
1328 constructor({ remotePath, localPath, force, matchPrefix }) {
1329 Object.assign(this, {
1330 remotePath,
1331 localPath,
1332 force,
1333 matchPrefix,
1334 });
1335 }
1336
1337 static from(refspec) {
1338 const [
1339 forceMatch,
1340 remotePath,
1341 remoteGlobMatch,
1342 localPath,
1343 localGlobMatch,
1344 ] = refspec.match(/^(\+?)(.*?)(\*?):(.*?)(\*?)$/).slice(1);
1345 const force = forceMatch === '+';
1346 const remoteIsGlob = remoteGlobMatch === '*';
1347 const localIsGlob = localGlobMatch === '*';
1348 // validate
1349 // TODO: Make this check more nuanced, and depend on whether this is a fetch refspec or a push refspec
1350 if (remoteIsGlob !== localIsGlob) {
1351 throw new InternalError('Invalid refspec')
1352 }
1353 return new GitRefSpec({
1354 remotePath,
1355 localPath,
1356 force,
1357 matchPrefix: remoteIsGlob,
1358 })
1359 // TODO: We need to run resolveRef on both paths to expand them to their full name.
1360 }
1361
1362 translate(remoteBranch) {
1363 if (this.matchPrefix) {
1364 if (remoteBranch.startsWith(this.remotePath)) {
1365 return this.localPath + remoteBranch.replace(this.remotePath, '')
1366 }
1367 } else {
1368 if (remoteBranch === this.remotePath) return this.localPath
1369 }
1370 return null
1371 }
1372
1373 reverseTranslate(localBranch) {
1374 if (this.matchPrefix) {
1375 if (localBranch.startsWith(this.localPath)) {
1376 return this.remotePath + localBranch.replace(this.localPath, '')
1377 }
1378 } else {
1379 if (localBranch === this.localPath) return this.remotePath
1380 }
1381 return null
1382 }
1383}
1384
1385class GitRefSpecSet {
1386 constructor(rules = []) {
1387 this.rules = rules;
1388 }
1389
1390 static from(refspecs) {
1391 const rules = [];
1392 for (const refspec of refspecs) {
1393 rules.push(GitRefSpec.from(refspec)); // might throw
1394 }
1395 return new GitRefSpecSet(rules)
1396 }
1397
1398 add(refspec) {
1399 const rule = GitRefSpec.from(refspec); // might throw
1400 this.rules.push(rule);
1401 }
1402
1403 translate(remoteRefs) {
1404 const result = [];
1405 for (const rule of this.rules) {
1406 for (const remoteRef of remoteRefs) {
1407 const localRef = rule.translate(remoteRef);
1408 if (localRef) {
1409 result.push([remoteRef, localRef]);
1410 }
1411 }
1412 }
1413 return result
1414 }
1415
1416 translateOne(remoteRef) {
1417 let result = null;
1418 for (const rule of this.rules) {
1419 const localRef = rule.translate(remoteRef);
1420 if (localRef) {
1421 result = localRef;
1422 }
1423 }
1424 return result
1425 }
1426
1427 localNamespaces() {
1428 return this.rules
1429 .filter(rule => rule.matchPrefix)
1430 .map(rule => rule.localPath.replace(/\/$/, ''))
1431 }
1432}
1433
1434function compareRefNames(a, b) {
1435 // https://stackoverflow.com/a/40355107/2168416
1436 const _a = a.replace(/\^\{\}$/, '');
1437 const _b = b.replace(/\^\{\}$/, '');
1438 const tmp = -(_a < _b) || +(_a > _b);
1439 if (tmp === 0) {
1440 return a.endsWith('^{}') ? 1 : -1
1441 }
1442 return tmp
1443}
1444
1445const memo = new Map();
1446function normalizePath(path) {
1447 let normalizedPath = memo.get(path);
1448 if (!normalizedPath) {
1449 normalizedPath = normalizePathInternal(path);
1450 memo.set(path, normalizedPath);
1451 }
1452 return normalizedPath
1453}
1454
1455function normalizePathInternal(path) {
1456 path = path
1457 .split('/./')
1458 .join('/') // Replace '/./' with '/'
1459 .replace(/\/{2,}/g, '/'); // Replace consecutive '/'
1460
1461 if (path === '/.') return '/' // if path === '/.' return '/'
1462 if (path === './') return '.' // if path === './' return '.'
1463
1464 if (path.startsWith('./')) path = path.slice(2); // Remove leading './'
1465 if (path.endsWith('/.')) path = path.slice(0, -2); // Remove trailing '/.'
1466 if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1); // Remove trailing '/'
1467
1468 if (path === '') return '.' // if path === '' return '.'
1469
1470 return path
1471}
1472
1473// For some reason path.posix.join is undefined in webpack
1474
1475function join(...parts) {
1476 return normalizePath(parts.map(normalizePath).join('/'))
1477}
1478
1479// This is straight from parse_unit_factor in config.c of canonical git
1480const num = val => {
1481 val = val.toLowerCase();
1482 let n = parseInt(val);
1483 if (val.endsWith('k')) n *= 1024;
1484 if (val.endsWith('m')) n *= 1024 * 1024;
1485 if (val.endsWith('g')) n *= 1024 * 1024 * 1024;
1486 return n
1487};
1488
1489// This is straight from git_parse_maybe_bool_text in config.c of canonical git
1490const bool = val => {
1491 val = val.trim().toLowerCase();
1492 if (val === 'true' || val === 'yes' || val === 'on') return true
1493 if (val === 'false' || val === 'no' || val === 'off') return false
1494 throw Error(
1495 `Expected 'true', 'false', 'yes', 'no', 'on', or 'off', but got ${val}`
1496 )
1497};
1498
1499const schema = {
1500 core: {
1501 filemode: bool,
1502 bare: bool,
1503 logallrefupdates: bool,
1504 symlinks: bool,
1505 ignorecase: bool,
1506 bigFileThreshold: num,
1507 },
1508};
1509
1510// https://git-scm.com/docs/git-config#_syntax
1511
1512// section starts with [ and ends with ]
1513// section is alphanumeric (ASCII) with - and .
1514// section is case insensitive
1515// subsection is optionnal
1516// subsection is specified after section and one or more spaces
1517// subsection is specified between double quotes
1518const SECTION_LINE_REGEX = /^\[([A-Za-z0-9-.]+)(?: "(.*)")?\]$/;
1519const SECTION_REGEX = /^[A-Za-z0-9-.]+$/;
1520
1521// variable lines contain a name, and equal sign and then a value
1522// variable lines can also only contain a name (the implicit value is a boolean true)
1523// variable name is alphanumeric (ASCII) with -
1524// variable name starts with an alphabetic character
1525// variable name is case insensitive
1526const VARIABLE_LINE_REGEX = /^([A-Za-z][A-Za-z-]*)(?: *= *(.*))?$/;
1527const VARIABLE_NAME_REGEX = /^[A-Za-z][A-Za-z-]*$/;
1528
1529const VARIABLE_VALUE_COMMENT_REGEX = /^(.*?)( *[#;].*)$/;
1530
1531const extractSectionLine = line => {
1532 const matches = SECTION_LINE_REGEX.exec(line);
1533 if (matches != null) {
1534 const [section, subsection] = matches.slice(1);
1535 return [section, subsection]
1536 }
1537 return null
1538};
1539
1540const extractVariableLine = line => {
1541 const matches = VARIABLE_LINE_REGEX.exec(line);
1542 if (matches != null) {
1543 const [name, rawValue = 'true'] = matches.slice(1);
1544 const valueWithoutComments = removeComments(rawValue);
1545 const valueWithoutQuotes = removeQuotes(valueWithoutComments);
1546 return [name, valueWithoutQuotes]
1547 }
1548 return null
1549};
1550
1551const removeComments = rawValue => {
1552 const commentMatches = VARIABLE_VALUE_COMMENT_REGEX.exec(rawValue);
1553 if (commentMatches == null) {
1554 return rawValue
1555 }
1556 const [valueWithoutComment, comment] = commentMatches.slice(1);
1557 // if odd number of quotes before and after comment => comment is escaped
1558 if (
1559 hasOddNumberOfQuotes(valueWithoutComment) &&
1560 hasOddNumberOfQuotes(comment)
1561 ) {
1562 return `${valueWithoutComment}${comment}`
1563 }
1564 return valueWithoutComment
1565};
1566
1567const hasOddNumberOfQuotes = text => {
1568 const numberOfQuotes = (text.match(/(?:^|[^\\])"/g) || []).length;
1569 return numberOfQuotes % 2 !== 0
1570};
1571
1572const removeQuotes = text => {
1573 return text.split('').reduce((newText, c, idx, text) => {
1574 const isQuote = c === '"' && text[idx - 1] !== '\\';
1575 const isEscapeForQuote = c === '\\' && text[idx + 1] === '"';
1576 if (isQuote || isEscapeForQuote) {
1577 return newText
1578 }
1579 return newText + c
1580 }, '')
1581};
1582
1583const lower = text => {
1584 return text != null ? text.toLowerCase() : null
1585};
1586
1587const getPath = (section, subsection, name) => {
1588 return [lower(section), subsection, lower(name)]
1589 .filter(a => a != null)
1590 .join('.')
1591};
1592
1593const normalizePath$1 = path => {
1594 const pathSegments = path.split('.');
1595 const section = pathSegments.shift();
1596 const name = pathSegments.pop();
1597 const subsection = pathSegments.length ? pathSegments.join('.') : undefined;
1598
1599 return {
1600 section,
1601 subsection,
1602 name,
1603 path: getPath(section, subsection, name),
1604 sectionPath: getPath(section, subsection, null),
1605 }
1606};
1607
1608const findLastIndex = (array, callback) => {
1609 return array.reduce((lastIndex, item, index) => {
1610 return callback(item) ? index : lastIndex
1611 }, -1)
1612};
1613
1614// Note: there are a LOT of edge cases that aren't covered (e.g. keys in sections that also
1615// have subsections, [include] directives, etc.
1616class GitConfig {
1617 constructor(text) {
1618 let section = null;
1619 let subsection = null;
1620 this.parsedConfig = text.split('\n').map(line => {
1621 let name = null;
1622 let value = null;
1623
1624 const trimmedLine = line.trim();
1625 const extractedSection = extractSectionLine(trimmedLine);
1626 const isSection = extractedSection != null;
1627 if (isSection) {
1628 ;[section, subsection] = extractedSection;
1629 } else {
1630 const extractedVariable = extractVariableLine(trimmedLine);
1631 const isVariable = extractedVariable != null;
1632 if (isVariable) {
1633 ;[name, value] = extractedVariable;
1634 }
1635 }
1636
1637 const path = getPath(section, subsection, name);
1638 return { line, isSection, section, subsection, name, value, path }
1639 });
1640 }
1641
1642 static from(text) {
1643 return new GitConfig(text)
1644 }
1645
1646 async get(path, getall = false) {
1647 const normalizedPath = normalizePath$1(path).path;
1648 const allValues = this.parsedConfig
1649 .filter(config => config.path === normalizedPath)
1650 .map(({ section, name, value }) => {
1651 const fn = schema[section] && schema[section][name];
1652 return fn ? fn(value) : value
1653 });
1654 return getall ? allValues : allValues.pop()
1655 }
1656
1657 async getall(path) {
1658 return this.get(path, true)
1659 }
1660
1661 async getSubsections(section) {
1662 return this.parsedConfig
1663 .filter(config => config.section === section && config.isSection)
1664 .map(config => config.subsection)
1665 }
1666
1667 async deleteSection(section, subsection) {
1668 this.parsedConfig = this.parsedConfig.filter(
1669 config =>
1670 !(config.section === section && config.subsection === subsection)
1671 );
1672 }
1673
1674 async append(path, value) {
1675 return this.set(path, value, true)
1676 }
1677
1678 async set(path, value, append = false) {
1679 const {
1680 section,
1681 subsection,
1682 name,
1683 path: normalizedPath,
1684 sectionPath,
1685 } = normalizePath$1(path);
1686 const configIndex = findLastIndex(
1687 this.parsedConfig,
1688 config => config.path === normalizedPath
1689 );
1690 if (value == null) {
1691 if (configIndex !== -1) {
1692 this.parsedConfig.splice(configIndex, 1);
1693 }
1694 } else {
1695 if (configIndex !== -1) {
1696 const config = this.parsedConfig[configIndex];
1697 // Name should be overwritten in case the casing changed
1698 const modifiedConfig = Object.assign({}, config, {
1699 name,
1700 value,
1701 modified: true,
1702 });
1703 if (append) {
1704 this.parsedConfig.splice(configIndex + 1, 0, modifiedConfig);
1705 } else {
1706 this.parsedConfig[configIndex] = modifiedConfig;
1707 }
1708 } else {
1709 const sectionIndex = this.parsedConfig.findIndex(
1710 config => config.path === sectionPath
1711 );
1712 const newConfig = {
1713 section,
1714 subsection,
1715 name,
1716 value,
1717 modified: true,
1718 path: normalizedPath,
1719 };
1720 if (SECTION_REGEX.test(section) && VARIABLE_NAME_REGEX.test(name)) {
1721 if (sectionIndex >= 0) {
1722 // Reuse existing section
1723 this.parsedConfig.splice(sectionIndex + 1, 0, newConfig);
1724 } else {
1725 // Add a new section
1726 const newSection = {
1727 section,
1728 subsection,
1729 modified: true,
1730 path: sectionPath,
1731 };
1732 this.parsedConfig.push(newSection, newConfig);
1733 }
1734 }
1735 }
1736 }
1737 }
1738
1739 toString() {
1740 return this.parsedConfig
1741 .map(({ line, section, subsection, name, value, modified = false }) => {
1742 if (!modified) {
1743 return line
1744 }
1745 if (name != null && value != null) {
1746 if (typeof value === 'string' && /[#;]/.test(value)) {
1747 // A `#` or `;` symbol denotes a comment, so we have to wrap it in double quotes
1748 return `\t${name} = "${value}"`
1749 }
1750 return `\t${name} = ${value}`
1751 }
1752 if (subsection != null) {
1753 return `[${section} "${subsection}"]`
1754 }
1755 return `[${section}]`
1756 })
1757 .join('\n')
1758 }
1759}
1760
1761class GitConfigManager {
1762 static async get({ fs, gitdir }) {
1763 // We can improve efficiency later if needed.
1764 // TODO: read from full list of git config files
1765 const text = await fs.read(`${gitdir}/config`, { encoding: 'utf8' });
1766 return GitConfig.from(text)
1767 }
1768
1769 static async save({ fs, gitdir, config }) {
1770 // We can improve efficiency later if needed.
1771 // TODO: handle saving to the correct global/user/repo location
1772 await fs.write(`${gitdir}/config`, config.toString(), {
1773 encoding: 'utf8',
1774 });
1775 }
1776}
1777
1778// This is a convenience wrapper for reading and writing files in the 'refs' directory.
1779
1780// @see https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions
1781const refpaths = ref => [
1782 `${ref}`,
1783 `refs/${ref}`,
1784 `refs/tags/${ref}`,
1785 `refs/heads/${ref}`,
1786 `refs/remotes/${ref}`,
1787 `refs/remotes/${ref}/HEAD`,
1788];
1789
1790// @see https://git-scm.com/docs/gitrepository-layout
1791const GIT_FILES = ['config', 'description', 'index', 'shallow', 'commondir'];
1792
1793class GitRefManager {
1794 static async updateRemoteRefs({
1795 fs,
1796 gitdir,
1797 remote,
1798 refs,
1799 symrefs,
1800 tags,
1801 refspecs = undefined,
1802 prune = false,
1803 pruneTags = false,
1804 }) {
1805 // Validate input
1806 for (const value of refs.values()) {
1807 if (!value.match(/[0-9a-f]{40}/)) {
1808 throw new InvalidOidError(value)
1809 }
1810 }
1811 const config = await GitConfigManager.get({ fs, gitdir });
1812 if (!refspecs) {
1813 refspecs = await config.getall(`remote.${remote}.fetch`);
1814 if (refspecs.length === 0) {
1815 throw new NoRefspecError(remote)
1816 }
1817 // There's some interesting behavior with HEAD that doesn't follow the refspec.
1818 refspecs.unshift(`+HEAD:refs/remotes/${remote}/HEAD`);
1819 }
1820 const refspec = GitRefSpecSet.from(refspecs);
1821 const actualRefsToWrite = new Map();
1822 // Delete all current tags if the pruneTags argument is true.
1823 if (pruneTags) {
1824 const tags = await GitRefManager.listRefs({
1825 fs,
1826 gitdir,
1827 filepath: 'refs/tags',
1828 });
1829 await GitRefManager.deleteRefs({
1830 fs,
1831 gitdir,
1832 refs: tags.map(tag => `refs/tags/${tag}`),
1833 });
1834 }
1835 // Add all tags if the fetch tags argument is true.
1836 if (tags) {
1837 for (const serverRef of refs.keys()) {
1838 if (serverRef.startsWith('refs/tags') && !serverRef.endsWith('^{}')) {
1839 // Git's behavior is to only fetch tags that do not conflict with tags already present.
1840 if (!(await GitRefManager.exists({ fs, gitdir, ref: serverRef }))) {
1841 // Always use the object id of the tag itself, and not the peeled object id.
1842 const oid = refs.get(serverRef);
1843 actualRefsToWrite.set(serverRef, oid);
1844 }
1845 }
1846 }
1847 }
1848 // Combine refs and symrefs giving symrefs priority
1849 const refTranslations = refspec.translate([...refs.keys()]);
1850 for (const [serverRef, translatedRef] of refTranslations) {
1851 const value = refs.get(serverRef);
1852 actualRefsToWrite.set(translatedRef, value);
1853 }
1854 const symrefTranslations = refspec.translate([...symrefs.keys()]);
1855 for (const [serverRef, translatedRef] of symrefTranslations) {
1856 const value = symrefs.get(serverRef);
1857 const symtarget = refspec.translateOne(value);
1858 if (symtarget) {
1859 actualRefsToWrite.set(translatedRef, `ref: ${symtarget}`);
1860 }
1861 }
1862 // If `prune` argument is true, clear out the existing local refspec roots
1863 const pruned = [];
1864 if (prune) {
1865 for (const filepath of refspec.localNamespaces()) {
1866 const refs = (
1867 await GitRefManager.listRefs({
1868 fs,
1869 gitdir,
1870 filepath,
1871 })
1872 ).map(file => `${filepath}/${file}`);
1873 for (const ref of refs) {
1874 if (!actualRefsToWrite.has(ref)) {
1875 pruned.push(ref);
1876 }
1877 }
1878 }
1879 if (pruned.length > 0) {
1880 await GitRefManager.deleteRefs({ fs, gitdir, refs: pruned });
1881 }
1882 }
1883 // Update files
1884 // TODO: For large repos with a history of thousands of pull requests
1885 // (i.e. gitlab-ce) it would be vastly more efficient to write them
1886 // to .git/packed-refs.
1887 // The trick is to make sure we a) don't write a packed ref that is
1888 // already shadowed by a loose ref and b) don't loose any refs already
1889 // in packed-refs. Doing this efficiently may be difficult. A
1890 // solution that might work is
1891 // a) load the current packed-refs file
1892 // b) add actualRefsToWrite, overriding the existing values if present
1893 // c) enumerate all the loose refs currently in .git/refs/remotes/${remote}
1894 // d) overwrite their value with the new value.
1895 // Examples of refs we need to avoid writing in loose format for efficieny's sake
1896 // are .git/refs/remotes/origin/refs/remotes/remote_mirror_3059
1897 // and .git/refs/remotes/origin/refs/merge-requests
1898 for (const [key, value] of actualRefsToWrite) {
1899 await fs.write(join(gitdir, key), `${value.trim()}\n`, 'utf8');
1900 }
1901 return { pruned }
1902 }
1903
1904 // TODO: make this less crude?
1905 static async writeRef({ fs, gitdir, ref, value }) {
1906 // Validate input
1907 if (!value.match(/[0-9a-f]{40}/)) {
1908 throw new InvalidOidError(value)
1909 }
1910 await fs.write(join(gitdir, ref), `${value.trim()}\n`, 'utf8');
1911 }
1912
1913 static async writeSymbolicRef({ fs, gitdir, ref, value }) {
1914 await fs.write(join(gitdir, ref), 'ref: ' + `${value.trim()}\n`, 'utf8');
1915 }
1916
1917 static async deleteRef({ fs, gitdir, ref }) {
1918 return GitRefManager.deleteRefs({ fs, gitdir, refs: [ref] })
1919 }
1920
1921 static async deleteRefs({ fs, gitdir, refs }) {
1922 // Delete regular ref
1923 await Promise.all(refs.map(ref => fs.rm(join(gitdir, ref))));
1924 // Delete any packed ref
1925 let text = await fs.read(`${gitdir}/packed-refs`, { encoding: 'utf8' });
1926 const packed = GitPackedRefs.from(text);
1927 const beforeSize = packed.refs.size;
1928 for (const ref of refs) {
1929 if (packed.refs.has(ref)) {
1930 packed.delete(ref);
1931 }
1932 }
1933 if (packed.refs.size < beforeSize) {
1934 text = packed.toString();
1935 await fs.write(`${gitdir}/packed-refs`, text, { encoding: 'utf8' });
1936 }
1937 }
1938
1939 /**
1940 * @param {object} args
1941 * @param {import('../models/FileSystem.js').FileSystem} args.fs
1942 * @param {string} args.gitdir
1943 * @param {string} args.ref
1944 * @param {number} [args.depth]
1945 * @returns {Promise<string>}
1946 */
1947 static async resolve({ fs, gitdir, ref, depth = undefined }) {
1948 if (depth !== undefined) {
1949 depth--;
1950 if (depth === -1) {
1951 return ref
1952 }
1953 }
1954 let sha;
1955 // Is it a ref pointer?
1956 if (ref.startsWith('ref: ')) {
1957 ref = ref.slice('ref: '.length);
1958 return GitRefManager.resolve({ fs, gitdir, ref, depth })
1959 }
1960 // Is it a complete and valid SHA?
1961 if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
1962 return ref
1963 }
1964 // We need to alternate between the file system and the packed-refs
1965 const packedMap = await GitRefManager.packedRefs({ fs, gitdir });
1966 // Look in all the proper paths, in this order
1967 const allpaths = refpaths(ref).filter(p => !GIT_FILES.includes(p)); // exclude git system files (#709)
1968
1969 for (const ref of allpaths) {
1970 sha =
1971 (await fs.read(`${gitdir}/${ref}`, { encoding: 'utf8' })) ||
1972 packedMap.get(ref);
1973 if (sha) {
1974 return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
1975 }
1976 }
1977 // Do we give up?
1978 throw new NotFoundError(ref)
1979 }
1980
1981 static async exists({ fs, gitdir, ref }) {
1982 try {
1983 await GitRefManager.expand({ fs, gitdir, ref });
1984 return true
1985 } catch (err) {
1986 return false
1987 }
1988 }
1989
1990 static async expand({ fs, gitdir, ref }) {
1991 // Is it a complete and valid SHA?
1992 if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
1993 return ref
1994 }
1995 // We need to alternate between the file system and the packed-refs
1996 const packedMap = await GitRefManager.packedRefs({ fs, gitdir });
1997 // Look in all the proper paths, in this order
1998 const allpaths = refpaths(ref);
1999 for (const ref of allpaths) {
2000 if (await fs.exists(`${gitdir}/${ref}`)) return ref
2001 if (packedMap.has(ref)) return ref
2002 }
2003 // Do we give up?
2004 throw new NotFoundError(ref)
2005 }
2006
2007 static async expandAgainstMap({ ref, map }) {
2008 // Look in all the proper paths, in this order
2009 const allpaths = refpaths(ref);
2010 for (const ref of allpaths) {
2011 if (await map.has(ref)) return ref
2012 }
2013 // Do we give up?
2014 throw new NotFoundError(ref)
2015 }
2016
2017 static resolveAgainstMap({ ref, fullref = ref, depth = undefined, map }) {
2018 if (depth !== undefined) {
2019 depth--;
2020 if (depth === -1) {
2021 return { fullref, oid: ref }
2022 }
2023 }
2024 // Is it a ref pointer?
2025 if (ref.startsWith('ref: ')) {
2026 ref = ref.slice('ref: '.length);
2027 return GitRefManager.resolveAgainstMap({ ref, fullref, depth, map })
2028 }
2029 // Is it a complete and valid SHA?
2030 if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
2031 return { fullref, oid: ref }
2032 }
2033 // Look in all the proper paths, in this order
2034 const allpaths = refpaths(ref);
2035 for (const ref of allpaths) {
2036 const sha = map.get(ref);
2037 if (sha) {
2038 return GitRefManager.resolveAgainstMap({
2039 ref: sha.trim(),
2040 fullref: ref,
2041 depth,
2042 map,
2043 })
2044 }
2045 }
2046 // Do we give up?
2047 throw new NotFoundError(ref)
2048 }
2049
2050 static async packedRefs({ fs, gitdir }) {
2051 const text = await fs.read(`${gitdir}/packed-refs`, { encoding: 'utf8' });
2052 const packed = GitPackedRefs.from(text);
2053 return packed.refs
2054 }
2055
2056 // List all the refs that match the `filepath` prefix
2057 static async listRefs({ fs, gitdir, filepath }) {
2058 const packedMap = GitRefManager.packedRefs({ fs, gitdir });
2059 let files = null;
2060 try {
2061 files = await fs.readdirDeep(`${gitdir}/${filepath}`);
2062 files = files.map(x => x.replace(`${gitdir}/${filepath}/`, ''));
2063 } catch (err) {
2064 files = [];
2065 }
2066
2067 for (let key of (await packedMap).keys()) {
2068 // filter by prefix
2069 if (key.startsWith(filepath)) {
2070 // remove prefix
2071 key = key.replace(filepath + '/', '');
2072 // Don't include duplicates; the loose files have precedence anyway
2073 if (!files.includes(key)) {
2074 files.push(key);
2075 }
2076 }
2077 }
2078 // since we just appended things onto an array, we need to sort them now
2079 files.sort(compareRefNames);
2080 return files
2081 }
2082
2083 static async listBranches({ fs, gitdir, remote }) {
2084 if (remote) {
2085 return GitRefManager.listRefs({
2086 fs,
2087 gitdir,
2088 filepath: `refs/remotes/${remote}`,
2089 })
2090 } else {
2091 return GitRefManager.listRefs({ fs, gitdir, filepath: `refs/heads` })
2092 }
2093 }
2094
2095 static async listTags({ fs, gitdir }) {
2096 const tags = await GitRefManager.listRefs({
2097 fs,
2098 gitdir,
2099 filepath: `refs/tags`,
2100 });
2101 return tags.filter(x => !x.endsWith('^{}'))
2102 }
2103}
2104
2105function compareTreeEntryPath(a, b) {
2106 // Git sorts tree entries as if there is a trailing slash on directory names.
2107 return compareStrings(appendSlashIfDir(a), appendSlashIfDir(b))
2108}
2109
2110function appendSlashIfDir(entry) {
2111 return entry.mode === '040000' ? entry.path + '/' : entry.path
2112}
2113
2114/**
2115 *
2116 * @typedef {Object} TreeEntry
2117 * @property {string} mode - the 6 digit hexadecimal mode
2118 * @property {string} path - the name of the file or directory
2119 * @property {string} oid - the SHA-1 object id of the blob or tree
2120 * @property {'commit'|'blob'|'tree'} type - the type of object
2121 */
2122
2123function mode2type$1(mode) {
2124 // prettier-ignore
2125 switch (mode) {
2126 case '040000': return 'tree'
2127 case '100644': return 'blob'
2128 case '100755': return 'blob'
2129 case '120000': return 'blob'
2130 case '160000': return 'commit'
2131 }
2132 throw new InternalError(`Unexpected GitTree entry mode: ${mode}`)
2133}
2134
2135function parseBuffer(buffer) {
2136 const _entries = [];
2137 let cursor = 0;
2138 while (cursor < buffer.length) {
2139 const space = buffer.indexOf(32, cursor);
2140 if (space === -1) {
2141 throw new InternalError(
2142 `GitTree: Error parsing buffer at byte location ${cursor}: Could not find the next space character.`
2143 )
2144 }
2145 const nullchar = buffer.indexOf(0, cursor);
2146 if (nullchar === -1) {
2147 throw new InternalError(
2148 `GitTree: Error parsing buffer at byte location ${cursor}: Could not find the next null character.`
2149 )
2150 }
2151 let mode = buffer.slice(cursor, space).toString('utf8');
2152 if (mode === '40000') mode = '040000'; // makes it line up neater in printed output
2153 const type = mode2type$1(mode);
2154 const path = buffer.slice(space + 1, nullchar).toString('utf8');
2155
2156 // Prevent malicious git repos from writing to "..\foo" on clone etc
2157 if (path.includes('\\') || path.includes('/')) {
2158 throw new UnsafeFilepathError(path)
2159 }
2160
2161 const oid = buffer.slice(nullchar + 1, nullchar + 21).toString('hex');
2162 cursor = nullchar + 21;
2163 _entries.push({ mode, path, oid, type });
2164 }
2165 return _entries
2166}
2167
2168function limitModeToAllowed(mode) {
2169 if (typeof mode === 'number') {
2170 mode = mode.toString(8);
2171 }
2172 // tree
2173 if (mode.match(/^0?4.*/)) return '040000' // Directory
2174 if (mode.match(/^1006.*/)) return '100644' // Regular non-executable file
2175 if (mode.match(/^1007.*/)) return '100755' // Regular executable file
2176 if (mode.match(/^120.*/)) return '120000' // Symbolic link
2177 if (mode.match(/^160.*/)) return '160000' // Commit (git submodule reference)
2178 throw new InternalError(`Could not understand file mode: ${mode}`)
2179}
2180
2181function nudgeIntoShape(entry) {
2182 if (!entry.oid && entry.sha) {
2183 entry.oid = entry.sha; // Github
2184 }
2185 entry.mode = limitModeToAllowed(entry.mode); // index
2186 if (!entry.type) {
2187 entry.type = mode2type$1(entry.mode); // index
2188 }
2189 return entry
2190}
2191
2192class GitTree {
2193 constructor(entries) {
2194 if (Buffer.isBuffer(entries)) {
2195 this._entries = parseBuffer(entries);
2196 } else if (Array.isArray(entries)) {
2197 this._entries = entries.map(nudgeIntoShape);
2198 } else {
2199 throw new InternalError('invalid type passed to GitTree constructor')
2200 }
2201 // Tree entries are not sorted alphabetically in the usual sense (see `compareTreeEntryPath`)
2202 // but it is important later on that these be sorted in the same order as they would be returned from readdir.
2203 this._entries.sort(comparePath);
2204 }
2205
2206 static from(tree) {
2207 return new GitTree(tree)
2208 }
2209
2210 render() {
2211 return this._entries
2212 .map(entry => `${entry.mode} ${entry.type} ${entry.oid} ${entry.path}`)
2213 .join('\n')
2214 }
2215
2216 toObject() {
2217 // Adjust the sort order to match git's
2218 const entries = [...this._entries];
2219 entries.sort(compareTreeEntryPath);
2220 return Buffer.concat(
2221 entries.map(entry => {
2222 const mode = Buffer.from(entry.mode.replace(/^0/, ''));
2223 const space = Buffer.from(' ');
2224 const path = Buffer.from(entry.path, 'utf8');
2225 const nullchar = Buffer.from([0]);
2226 const oid = Buffer.from(entry.oid, 'hex');
2227 return Buffer.concat([mode, space, path, nullchar, oid])
2228 })
2229 )
2230 }
2231
2232 /**
2233 * @returns {TreeEntry[]}
2234 */
2235 entries() {
2236 return this._entries
2237 }
2238
2239 *[Symbol.iterator]() {
2240 for (const entry of this._entries) {
2241 yield entry;
2242 }
2243 }
2244}
2245
2246class GitObject {
2247 static wrap({ type, object }) {
2248 return Buffer.concat([
2249 Buffer.from(`${type} ${object.byteLength.toString()}\x00`),
2250 Buffer.from(object),
2251 ])
2252 }
2253
2254 static unwrap(buffer) {
2255 const s = buffer.indexOf(32); // first space
2256 const i = buffer.indexOf(0); // first null value
2257 const type = buffer.slice(0, s).toString('utf8'); // get type of object
2258 const length = buffer.slice(s + 1, i).toString('utf8'); // get type of object
2259 const actualLength = buffer.length - (i + 1);
2260 // verify length
2261 if (parseInt(length) !== actualLength) {
2262 throw new InternalError(
2263 `Length mismatch: expected ${length} bytes but got ${actualLength} instead.`
2264 )
2265 }
2266 return {
2267 type,
2268 object: Buffer.from(buffer.slice(i + 1)),
2269 }
2270 }
2271}
2272
2273async function readObjectLoose({ fs, gitdir, oid }) {
2274 const source = `objects/${oid.slice(0, 2)}/${oid.slice(2)}`;
2275 const file = await fs.read(`${gitdir}/${source}`);
2276 if (!file) {
2277 return null
2278 }
2279 return { object: file, format: 'deflated', source }
2280}
2281
2282/**
2283 * @param {Buffer} delta
2284 * @param {Buffer} source
2285 * @returns {Buffer}
2286 */
2287function applyDelta(delta, source) {
2288 const reader = new BufferCursor(delta);
2289 const sourceSize = readVarIntLE(reader);
2290
2291 if (sourceSize !== source.byteLength) {
2292 throw new InternalError(
2293 `applyDelta expected source buffer to be ${sourceSize} bytes but the provided buffer was ${source.length} bytes`
2294 )
2295 }
2296 const targetSize = readVarIntLE(reader);
2297 let target;
2298
2299 const firstOp = readOp(reader, source);
2300 // Speed optimization - return raw buffer if it's just single simple copy
2301 if (firstOp.byteLength === targetSize) {
2302 target = firstOp;
2303 } else {
2304 // Otherwise, allocate a fresh buffer and slices
2305 target = Buffer.alloc(targetSize);
2306 const writer = new BufferCursor(target);
2307 writer.copy(firstOp);
2308
2309 while (!reader.eof()) {
2310 writer.copy(readOp(reader, source));
2311 }
2312
2313 const tell = writer.tell();
2314 if (targetSize !== tell) {
2315 throw new InternalError(
2316 `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
2317 )
2318 }
2319 }
2320 return target
2321}
2322
2323function readVarIntLE(reader) {
2324 let result = 0;
2325 let shift = 0;
2326 let byte = null;
2327 do {
2328 byte = reader.readUInt8();
2329 result |= (byte & 0b01111111) << shift;
2330 shift += 7;
2331 } while (byte & 0b10000000)
2332 return result
2333}
2334
2335function readCompactLE(reader, flags, size) {
2336 let result = 0;
2337 let shift = 0;
2338 while (size--) {
2339 if (flags & 0b00000001) {
2340 result |= reader.readUInt8() << shift;
2341 }
2342 flags >>= 1;
2343 shift += 8;
2344 }
2345 return result
2346}
2347
2348function readOp(reader, source) {
2349 /** @type {number} */
2350 const byte = reader.readUInt8();
2351 const COPY = 0b10000000;
2352 const OFFS = 0b00001111;
2353 const SIZE = 0b01110000;
2354 if (byte & COPY) {
2355 // copy consists of 4 byte offset, 3 byte size (in LE order)
2356 const offset = readCompactLE(reader, byte & OFFS, 4);
2357 let size = readCompactLE(reader, (byte & SIZE) >> 4, 3);
2358 // Yup. They really did this optimization.
2359 if (size === 0) size = 0x10000;
2360 return source.slice(offset, offset + size)
2361 } else {
2362 // insert
2363 return reader.slice(byte)
2364 }
2365}
2366
2367// Convert a value to an Async Iterator
2368// This will be easier with async generator functions.
2369function fromValue(value) {
2370 let queue = [value];
2371 return {
2372 next() {
2373 return Promise.resolve({ done: queue.length === 0, value: queue.pop() })
2374 },
2375 return() {
2376 queue = [];
2377 return {}
2378 },
2379 [Symbol.asyncIterator]() {
2380 return this
2381 },
2382 }
2383}
2384
2385function getIterator(iterable) {
2386 if (iterable[Symbol.asyncIterator]) {
2387 return iterable[Symbol.asyncIterator]()
2388 }
2389 if (iterable[Symbol.iterator]) {
2390 return iterable[Symbol.iterator]()
2391 }
2392 if (iterable.next) {
2393 return iterable
2394 }
2395 return fromValue(iterable)
2396}
2397
2398// inspired by 'gartal' but lighter-weight and more battle-tested.
2399class StreamReader {
2400 constructor(stream) {
2401 this.stream = getIterator(stream);
2402 this.buffer = null;
2403 this.cursor = 0;
2404 this.undoCursor = 0;
2405 this.started = false;
2406 this._ended = false;
2407 this._discardedBytes = 0;
2408 }
2409
2410 eof() {
2411 return this._ended && this.cursor === this.buffer.length
2412 }
2413
2414 tell() {
2415 return this._discardedBytes + this.cursor
2416 }
2417
2418 async byte() {
2419 if (this.eof()) return
2420 if (!this.started) await this._init();
2421 if (this.cursor === this.buffer.length) {
2422 await this._loadnext();
2423 if (this._ended) return
2424 }
2425 this._moveCursor(1);
2426 return this.buffer[this.undoCursor]
2427 }
2428
2429 async chunk() {
2430 if (this.eof()) return
2431 if (!this.started) await this._init();
2432 if (this.cursor === this.buffer.length) {
2433 await this._loadnext();
2434 if (this._ended) return
2435 }
2436 this._moveCursor(this.buffer.length);
2437 return this.buffer.slice(this.undoCursor, this.cursor)
2438 }
2439
2440 async read(n) {
2441 if (this.eof()) return
2442 if (!this.started) await this._init();
2443 if (this.cursor + n > this.buffer.length) {
2444 this._trim();
2445 await this._accumulate(n);
2446 }
2447 this._moveCursor(n);
2448 return this.buffer.slice(this.undoCursor, this.cursor)
2449 }
2450
2451 async skip(n) {
2452 if (this.eof()) return
2453 if (!this.started) await this._init();
2454 if (this.cursor + n > this.buffer.length) {
2455 this._trim();
2456 await this._accumulate(n);
2457 }
2458 this._moveCursor(n);
2459 }
2460
2461 async undo() {
2462 this.cursor = this.undoCursor;
2463 }
2464
2465 async _next() {
2466 this.started = true;
2467 let { done, value } = await this.stream.next();
2468 if (done) {
2469 this._ended = true;
2470 if (!value) return Buffer.alloc(0)
2471 }
2472 if (value) {
2473 value = Buffer.from(value);
2474 }
2475 return value
2476 }
2477
2478 _trim() {
2479 // Throw away parts of the buffer we don't need anymore
2480 // assert(this.cursor <= this.buffer.length)
2481 this.buffer = this.buffer.slice(this.undoCursor);
2482 this.cursor -= this.undoCursor;
2483 this._discardedBytes += this.undoCursor;
2484 this.undoCursor = 0;
2485 }
2486
2487 _moveCursor(n) {
2488 this.undoCursor = this.cursor;
2489 this.cursor += n;
2490 if (this.cursor > this.buffer.length) {
2491 this.cursor = this.buffer.length;
2492 }
2493 }
2494
2495 async _accumulate(n) {
2496 if (this._ended) return
2497 // Expand the buffer until we have N bytes of data
2498 // or we've reached the end of the stream
2499 const buffers = [this.buffer];
2500 while (this.cursor + n > lengthBuffers(buffers)) {
2501 const nextbuffer = await this._next();
2502 if (this._ended) break
2503 buffers.push(nextbuffer);
2504 }
2505 this.buffer = Buffer.concat(buffers);
2506 }
2507
2508 async _loadnext() {
2509 this._discardedBytes += this.buffer.length;
2510 this.undoCursor = 0;
2511 this.cursor = 0;
2512 this.buffer = await this._next();
2513 }
2514
2515 async _init() {
2516 this.buffer = await this._next();
2517 }
2518}
2519
2520// This helper function helps us postpone concatenating buffers, which
2521// would create intermediate buffer objects,
2522function lengthBuffers(buffers) {
2523 return buffers.reduce((acc, buffer) => acc + buffer.length, 0)
2524}
2525
2526// My version of git-list-pack - roughly 15x faster than the original
2527
2528async function listpack(stream, onData) {
2529 const reader = new StreamReader(stream);
2530 let PACK = await reader.read(4);
2531 PACK = PACK.toString('utf8');
2532 if (PACK !== 'PACK') {
2533 throw new InternalError(`Invalid PACK header '${PACK}'`)
2534 }
2535
2536 let version = await reader.read(4);
2537 version = version.readUInt32BE(0);
2538 if (version !== 2) {
2539 throw new InternalError(`Invalid packfile version: ${version}`)
2540 }
2541
2542 let numObjects = await reader.read(4);
2543 numObjects = numObjects.readUInt32BE(0);
2544 // If (for some godforsaken reason) this is an empty packfile, abort now.
2545 if (numObjects < 1) return
2546
2547 while (!reader.eof() && numObjects--) {
2548 const offset = reader.tell();
2549 const { type, length, ofs, reference } = await parseHeader(reader);
2550 const inflator = new pako.Inflate();
2551 while (!inflator.result) {
2552 const chunk = await reader.chunk();
2553 if (!chunk) break
2554 inflator.push(chunk, false);
2555 if (inflator.err) {
2556 throw new InternalError(`Pako error: ${inflator.msg}`)
2557 }
2558 if (inflator.result) {
2559 if (inflator.result.length !== length) {
2560 throw new InternalError(
2561 `Inflated object size is different from that stated in packfile.`
2562 )
2563 }
2564
2565 // Backtrack parser to where deflated data ends
2566 await reader.undo();
2567 await reader.read(chunk.length - inflator.strm.avail_in);
2568 const end = reader.tell();
2569 await onData({
2570 data: inflator.result,
2571 type,
2572 num: numObjects,
2573 offset,
2574 end,
2575 reference,
2576 ofs,
2577 });
2578 }
2579 }
2580 }
2581}
2582
2583async function parseHeader(reader) {
2584 // Object type is encoded in bits 654
2585 let byte = await reader.byte();
2586 const type = (byte >> 4) & 0b111;
2587 // The length encoding get complicated.
2588 // Last four bits of length is encoded in bits 3210
2589 let length = byte & 0b1111;
2590 // Whether the next byte is part of the variable-length encoded number
2591 // is encoded in bit 7
2592 if (byte & 0b10000000) {
2593 let shift = 4;
2594 do {
2595 byte = await reader.byte();
2596 length |= (byte & 0b01111111) << shift;
2597 shift += 7;
2598 } while (byte & 0b10000000)
2599 }
2600 // Handle deltified objects
2601 let ofs;
2602 let reference;
2603 if (type === 6) {
2604 let shift = 0;
2605 ofs = 0;
2606 const bytes = [];
2607 do {
2608 byte = await reader.byte();
2609 ofs |= (byte & 0b01111111) << shift;
2610 shift += 7;
2611 bytes.push(byte);
2612 } while (byte & 0b10000000)
2613 reference = Buffer.from(bytes);
2614 }
2615 if (type === 7) {
2616 const buf = await reader.read(20);
2617 reference = buf;
2618 }
2619 return { type, length, ofs, reference }
2620}
2621
2622/* eslint-env node, browser */
2623
2624let supportsDecompressionStream = false;
2625
2626async function inflate(buffer) {
2627 if (supportsDecompressionStream === null) {
2628 supportsDecompressionStream = testDecompressionStream();
2629 }
2630 return supportsDecompressionStream
2631 ? browserInflate(buffer)
2632 : pako.inflate(buffer)
2633}
2634
2635async function browserInflate(buffer) {
2636 const ds = new DecompressionStream('deflate');
2637 const d = new Blob([buffer]).stream().pipeThrough(ds);
2638 return new Uint8Array(await new Response(d).arrayBuffer())
2639}
2640
2641function testDecompressionStream() {
2642 try {
2643 const ds = new DecompressionStream('deflate');
2644 if (ds) return true
2645 } catch (_) {
2646 // no bother
2647 }
2648 return false
2649}
2650
2651function decodeVarInt(reader) {
2652 const bytes = [];
2653 let byte = 0;
2654 let multibyte = 0;
2655 do {
2656 byte = reader.readUInt8();
2657 // We keep bits 6543210
2658 const lastSeven = byte & 0b01111111;
2659 bytes.push(lastSeven);
2660 // Whether the next byte is part of the variable-length encoded number
2661 // is encoded in bit 7
2662 multibyte = byte & 0b10000000;
2663 } while (multibyte)
2664 // Now that all the bytes are in big-endian order,
2665 // alternate shifting the bits left by 7 and OR-ing the next byte.
2666 // And... do a weird increment-by-one thing that I don't quite understand.
2667 return bytes.reduce((a, b) => ((a + 1) << 7) | b, -1)
2668}
2669
2670// I'm pretty much copying this one from the git C source code,
2671// because it makes no sense.
2672function otherVarIntDecode(reader, startWith) {
2673 let result = startWith;
2674 let shift = 4;
2675 let byte = null;
2676 do {
2677 byte = reader.readUInt8();
2678 result |= (byte & 0b01111111) << shift;
2679 shift += 7;
2680 } while (byte & 0b10000000)
2681 return result
2682}
2683
2684class GitPackIndex {
2685 constructor(stuff) {
2686 Object.assign(this, stuff);
2687 this.offsetCache = {};
2688 }
2689
2690 static async fromIdx({ idx, getExternalRefDelta }) {
2691 const reader = new BufferCursor(idx);
2692 const magic = reader.slice(4).toString('hex');
2693 // Check for IDX v2 magic number
2694 if (magic !== 'ff744f63') {
2695 return // undefined
2696 }
2697 const version = reader.readUInt32BE();
2698 if (version !== 2) {
2699 throw new InternalError(
2700 `Unable to read version ${version} packfile IDX. (Only version 2 supported)`
2701 )
2702 }
2703 if (idx.byteLength > 2048 * 1024 * 1024) {
2704 throw new InternalError(
2705 `To keep implementation simple, I haven't implemented the layer 5 feature needed to support packfiles > 2GB in size.`
2706 )
2707 }
2708 // Skip over fanout table
2709 reader.seek(reader.tell() + 4 * 255);
2710 // Get hashes
2711 const size = reader.readUInt32BE();
2712 const hashes = [];
2713 for (let i = 0; i < size; i++) {
2714 const hash = reader.slice(20).toString('hex');
2715 hashes[i] = hash;
2716 }
2717 reader.seek(reader.tell() + 4 * size);
2718 // Skip over CRCs
2719 // Get offsets
2720 const offsets = new Map();
2721 for (let i = 0; i < size; i++) {
2722 offsets.set(hashes[i], reader.readUInt32BE());
2723 }
2724 const packfileSha = reader.slice(20).toString('hex');
2725 return new GitPackIndex({
2726 hashes,
2727 crcs: {},
2728 offsets,
2729 packfileSha,
2730 getExternalRefDelta,
2731 })
2732 }
2733
2734 static async fromPack({ pack, getExternalRefDelta, onProgress }) {
2735 const listpackTypes = {
2736 1: 'commit',
2737 2: 'tree',
2738 3: 'blob',
2739 4: 'tag',
2740 6: 'ofs-delta',
2741 7: 'ref-delta',
2742 };
2743 const offsetToObject = {};
2744
2745 // Older packfiles do NOT use the shasum of the pack itself,
2746 // so it is recommended to just use whatever bytes are in the trailer.
2747 // Source: https://github.com/git/git/commit/1190a1acf800acdcfd7569f87ac1560e2d077414
2748 const packfileSha = pack.slice(-20).toString('hex');
2749
2750 const hashes = [];
2751 const crcs = {};
2752 const offsets = new Map();
2753 let totalObjectCount = null;
2754 let lastPercent = null;
2755
2756 await listpack([pack], async ({ data, type, reference, offset, num }) => {
2757 if (totalObjectCount === null) totalObjectCount = num;
2758 const percent = Math.floor(
2759 ((totalObjectCount - num) * 100) / totalObjectCount
2760 );
2761 if (percent !== lastPercent) {
2762 if (onProgress) {
2763 await onProgress({
2764 phase: 'Receiving objects',
2765 loaded: totalObjectCount - num,
2766 total: totalObjectCount,
2767 });
2768 }
2769 }
2770 lastPercent = percent;
2771 // Change type from a number to a meaningful string
2772 type = listpackTypes[type];
2773
2774 if (['commit', 'tree', 'blob', 'tag'].includes(type)) {
2775 offsetToObject[offset] = {
2776 type,
2777 offset,
2778 };
2779 } else if (type === 'ofs-delta') {
2780 offsetToObject[offset] = {
2781 type,
2782 offset,
2783 };
2784 } else if (type === 'ref-delta') {
2785 offsetToObject[offset] = {
2786 type,
2787 offset,
2788 };
2789 }
2790 });
2791
2792 // We need to know the lengths of the slices to compute the CRCs.
2793 const offsetArray = Object.keys(offsetToObject).map(Number);
2794 for (const [i, start] of offsetArray.entries()) {
2795 const end =
2796 i + 1 === offsetArray.length ? pack.byteLength - 20 : offsetArray[i + 1];
2797 const o = offsetToObject[start];
2798 const crc = crc32.buf(pack.slice(start, end)) >>> 0;
2799 o.end = end;
2800 o.crc = crc;
2801 }
2802
2803 // We don't have the hashes yet. But we can generate them using the .readSlice function!
2804 const p = new GitPackIndex({
2805 pack: Promise.resolve(pack),
2806 packfileSha,
2807 crcs,
2808 hashes,
2809 offsets,
2810 getExternalRefDelta,
2811 });
2812
2813 // Resolve deltas and compute the oids
2814 lastPercent = null;
2815 let count = 0;
2816 const objectsByDepth = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
2817 for (let offset in offsetToObject) {
2818 offset = Number(offset);
2819 const percent = Math.floor((count * 100) / totalObjectCount);
2820 if (percent !== lastPercent) {
2821 if (onProgress) {
2822 await onProgress({
2823 phase: 'Resolving deltas',
2824 loaded: count,
2825 total: totalObjectCount,
2826 });
2827 }
2828 }
2829 count++;
2830 lastPercent = percent;
2831
2832 const o = offsetToObject[offset];
2833 if (o.oid) continue
2834 try {
2835 p.readDepth = 0;
2836 p.externalReadDepth = 0;
2837 const { type, object } = await p.readSlice({ start: offset });
2838 objectsByDepth[p.readDepth] += 1;
2839 const oid = await shasum(GitObject.wrap({ type, object }));
2840 o.oid = oid;
2841 hashes.push(oid);
2842 offsets.set(oid, offset);
2843 crcs[oid] = o.crc;
2844 } catch (err) {
2845 continue
2846 }
2847 }
2848
2849 hashes.sort();
2850 return p
2851 }
2852
2853 async toBuffer() {
2854 const buffers = [];
2855 const write = (str, encoding) => {
2856 buffers.push(Buffer.from(str, encoding));
2857 };
2858 // Write out IDX v2 magic number
2859 write('ff744f63', 'hex');
2860 // Write out version number 2
2861 write('00000002', 'hex');
2862 // Write fanout table
2863 const fanoutBuffer = new BufferCursor(Buffer.alloc(256 * 4));
2864 for (let i = 0; i < 256; i++) {
2865 let count = 0;
2866 for (const hash of this.hashes) {
2867 if (parseInt(hash.slice(0, 2), 16) <= i) count++;
2868 }
2869 fanoutBuffer.writeUInt32BE(count);
2870 }
2871 buffers.push(fanoutBuffer.buffer);
2872 // Write out hashes
2873 for (const hash of this.hashes) {
2874 write(hash, 'hex');
2875 }
2876 // Write out crcs
2877 const crcsBuffer = new BufferCursor(Buffer.alloc(this.hashes.length * 4));
2878 for (const hash of this.hashes) {
2879 crcsBuffer.writeUInt32BE(this.crcs[hash]);
2880 }
2881 buffers.push(crcsBuffer.buffer);
2882 // Write out offsets
2883 const offsetsBuffer = new BufferCursor(Buffer.alloc(this.hashes.length * 4));
2884 for (const hash of this.hashes) {
2885 offsetsBuffer.writeUInt32BE(this.offsets.get(hash));
2886 }
2887 buffers.push(offsetsBuffer.buffer);
2888 // Write out packfile checksum
2889 write(this.packfileSha, 'hex');
2890 // Write out shasum
2891 const totalBuffer = Buffer.concat(buffers);
2892 const sha = await shasum(totalBuffer);
2893 const shaBuffer = Buffer.alloc(20);
2894 shaBuffer.write(sha, 'hex');
2895 return Buffer.concat([totalBuffer, shaBuffer])
2896 }
2897
2898 async load({ pack }) {
2899 this.pack = pack;
2900 }
2901
2902 async unload() {
2903 this.pack = null;
2904 }
2905
2906 async read({ oid }) {
2907 if (!this.offsets.get(oid)) {
2908 if (this.getExternalRefDelta) {
2909 this.externalReadDepth++;
2910 return this.getExternalRefDelta(oid)
2911 } else {
2912 throw new InternalError(`Could not read object ${oid} from packfile`)
2913 }
2914 }
2915 const start = this.offsets.get(oid);
2916 return this.readSlice({ start })
2917 }
2918
2919 async readSlice({ start }) {
2920 if (this.offsetCache[start]) {
2921 return Object.assign({}, this.offsetCache[start])
2922 }
2923 this.readDepth++;
2924 const types = {
2925 0b0010000: 'commit',
2926 0b0100000: 'tree',
2927 0b0110000: 'blob',
2928 0b1000000: 'tag',
2929 0b1100000: 'ofs_delta',
2930 0b1110000: 'ref_delta',
2931 };
2932 if (!this.pack) {
2933 throw new InternalError(
2934 'Tried to read from a GitPackIndex with no packfile loaded into memory'
2935 )
2936 }
2937 const raw = (await this.pack).slice(start);
2938 const reader = new BufferCursor(raw);
2939 const byte = reader.readUInt8();
2940 // Object type is encoded in bits 654
2941 const btype = byte & 0b1110000;
2942 let type = types[btype];
2943 if (type === undefined) {
2944 throw new InternalError('Unrecognized type: 0b' + btype.toString(2))
2945 }
2946 // The length encoding get complicated.
2947 // Last four bits of length is encoded in bits 3210
2948 const lastFour = byte & 0b1111;
2949 let length = lastFour;
2950 // Whether the next byte is part of the variable-length encoded number
2951 // is encoded in bit 7
2952 const multibyte = byte & 0b10000000;
2953 if (multibyte) {
2954 length = otherVarIntDecode(reader, lastFour);
2955 }
2956 let base = null;
2957 let object = null;
2958 // Handle deltified objects
2959 if (type === 'ofs_delta') {
2960 const offset = decodeVarInt(reader);
2961 const baseOffset = start - offset
2962 ;({ object: base, type } = await this.readSlice({ start: baseOffset }));
2963 }
2964 if (type === 'ref_delta') {
2965 const oid = reader.slice(20).toString('hex')
2966 ;({ object: base, type } = await this.read({ oid }));
2967 }
2968 // Handle undeltified objects
2969 const buffer = raw.slice(reader.tell());
2970 object = Buffer.from(await inflate(buffer));
2971 // Assert that the object length is as expected.
2972 if (object.byteLength !== length) {
2973 throw new InternalError(
2974 `Packfile told us object would have length ${length} but it had length ${object.byteLength}`
2975 )
2976 }
2977 if (base) {
2978 object = Buffer.from(applyDelta(object, base));
2979 }
2980 // Cache the result based on depth.
2981 if (this.readDepth > 3) {
2982 // hand tuned for speed / memory usage tradeoff
2983 this.offsetCache[start] = { type, object };
2984 }
2985 return { type, format: 'content', object }
2986 }
2987}
2988
2989const PackfileCache = Symbol('PackfileCache');
2990
2991async function loadPackIndex({
2992 fs,
2993 filename,
2994 getExternalRefDelta,
2995 emitter,
2996 emitterPrefix,
2997}) {
2998 const idx = await fs.read(filename);
2999 return GitPackIndex.fromIdx({ idx, getExternalRefDelta })
3000}
3001
3002function readPackIndex({
3003 fs,
3004 cache,
3005 filename,
3006 getExternalRefDelta,
3007 emitter,
3008 emitterPrefix,
3009}) {
3010 // Try to get the packfile index from the in-memory cache
3011 if (!cache[PackfileCache]) cache[PackfileCache] = new Map();
3012 let p = cache[PackfileCache].get(filename);
3013 if (!p) {
3014 p = loadPackIndex({
3015 fs,
3016 filename,
3017 getExternalRefDelta,
3018 emitter,
3019 emitterPrefix,
3020 });
3021 cache[PackfileCache].set(filename, p);
3022 }
3023 return p
3024}
3025
3026async function readObjectPacked({
3027 fs,
3028 cache,
3029 gitdir,
3030 oid,
3031 format = 'content',
3032 getExternalRefDelta,
3033}) {
3034 // Check to see if it's in a packfile.
3035 // Iterate through all the .idx files
3036 let list = await fs.readdir(join(gitdir, 'objects/pack'));
3037 list = list.filter(x => x.endsWith('.idx'));
3038 for (const filename of list) {
3039 const indexFile = `${gitdir}/objects/pack/${filename}`;
3040 const p = await readPackIndex({
3041 fs,
3042 cache,
3043 filename: indexFile,
3044 getExternalRefDelta,
3045 });
3046 if (p.error) throw new InternalError(p.error)
3047 // If the packfile DOES have the oid we're looking for...
3048 if (p.offsets.has(oid)) {
3049 // Get the resolved git object from the packfile
3050 if (!p.pack) {
3051 const packFile = indexFile.replace(/idx$/, 'pack');
3052 p.pack = fs.read(packFile);
3053 }
3054 const result = await p.read({ oid, getExternalRefDelta });
3055 result.format = 'content';
3056 result.source = `objects/pack/${filename.replace(/idx$/, 'pack')}`;
3057 return result
3058 }
3059 }
3060 // Failed to find it
3061 return null
3062}
3063
3064/**
3065 * @param {object} args
3066 * @param {import('../models/FileSystem.js').FileSystem} args.fs
3067 * @param {any} args.cache
3068 * @param {string} args.gitdir
3069 * @param {string} args.oid
3070 * @param {string} [args.format]
3071 */
3072async function _readObject({
3073 fs,
3074 cache,
3075 gitdir,
3076 oid,
3077 format = 'content',
3078}) {
3079 // Curry the current read method so that the packfile un-deltification
3080 // process can acquire external ref-deltas.
3081 const getExternalRefDelta = oid => _readObject({ fs, cache, gitdir, oid });
3082
3083 let result;
3084 // Empty tree - hard-coded so we can use it as a shorthand.
3085 // Note: I think the canonical git implementation must do this too because
3086 // `git cat-file -t 4b825dc642cb6eb9a060e54bf8d69288fbee4904` prints "tree" even in empty repos.
3087 if (oid === '4b825dc642cb6eb9a060e54bf8d69288fbee4904') {
3088 result = { format: 'wrapped', object: Buffer.from(`tree 0\x00`) };
3089 }
3090 // Look for it in the loose object directory.
3091 if (!result) {
3092 result = await readObjectLoose({ fs, gitdir, oid });
3093 }
3094 // Check to see if it's in a packfile.
3095 if (!result) {
3096 result = await readObjectPacked({
3097 fs,
3098 cache,
3099 gitdir,
3100 oid,
3101 getExternalRefDelta,
3102 });
3103 }
3104 // Finally
3105 if (!result) {
3106 throw new NotFoundError(oid)
3107 }
3108
3109 if (format === 'deflated') {
3110 return result
3111 }
3112
3113 if (result.format === 'deflated') {
3114 result.object = Buffer.from(await inflate(result.object));
3115 result.format = 'wrapped';
3116 }
3117
3118 if (result.format === 'wrapped') {
3119 if (format === 'wrapped' && result.format === 'wrapped') {
3120 return result
3121 }
3122 const sha = await shasum(result.object);
3123 if (sha !== oid) {
3124 throw new InternalError(
3125 `SHA check failed! Expected ${oid}, computed ${sha}`
3126 )
3127 }
3128 const { object, type } = GitObject.unwrap(result.object);
3129 result.type = type;
3130 result.object = object;
3131 result.format = 'content';
3132 }
3133
3134 if (result.format === 'content') {
3135 if (format === 'content') return result
3136 return
3137 }
3138
3139 throw new InternalError(`invalid format "${result.format}"`)
3140}
3141
3142class AlreadyExistsError extends BaseError {
3143 /**
3144 * @param {'note'|'remote'|'tag'|'branch'} noun
3145 * @param {string} where
3146 * @param {boolean} canForce
3147 */
3148 constructor(noun, where, canForce = true) {
3149 super(
3150 `Failed to create ${noun} at ${where} because it already exists.${
3151 canForce
3152 ? ` (Hint: use 'force: true' parameter to overwrite existing ${noun}.)`
3153 : ''
3154 }`
3155 );
3156 this.code = this.name = AlreadyExistsError.code;
3157 this.data = { noun, where, canForce };
3158 }
3159}
3160/** @type {'AlreadyExistsError'} */
3161AlreadyExistsError.code = 'AlreadyExistsError';
3162
3163class AmbiguousError extends BaseError {
3164 /**
3165 * @param {'oids'|'refs'} nouns
3166 * @param {string} short
3167 * @param {string[]} matches
3168 */
3169 constructor(nouns, short, matches) {
3170 super(
3171 `Found multiple ${nouns} matching "${short}" (${matches.join(
3172 ', '
3173 )}). Use a longer abbreviation length to disambiguate them.`
3174 );
3175 this.code = this.name = AmbiguousError.code;
3176 this.data = { nouns, short, matches };
3177 }
3178}
3179/** @type {'AmbiguousError'} */
3180AmbiguousError.code = 'AmbiguousError';
3181
3182class CheckoutConflictError extends BaseError {
3183 /**
3184 * @param {string[]} filepaths
3185 */
3186 constructor(filepaths) {
3187 super(
3188 `Your local changes to the following files would be overwritten by checkout: ${filepaths.join(
3189 ', '
3190 )}`
3191 );
3192 this.code = this.name = CheckoutConflictError.code;
3193 this.data = { filepaths };
3194 }
3195}
3196/** @type {'CheckoutConflictError'} */
3197CheckoutConflictError.code = 'CheckoutConflictError';
3198
3199class CommitNotFetchedError extends BaseError {
3200 /**
3201 * @param {string} ref
3202 * @param {string} oid
3203 */
3204 constructor(ref, oid) {
3205 super(
3206 `Failed to checkout "${ref}" because commit ${oid} is not available locally. Do a git fetch to make the branch available locally.`
3207 );
3208 this.code = this.name = CommitNotFetchedError.code;
3209 this.data = { ref, oid };
3210 }
3211}
3212/** @type {'CommitNotFetchedError'} */
3213CommitNotFetchedError.code = 'CommitNotFetchedError';
3214
3215class EmptyServerResponseError extends BaseError {
3216 constructor() {
3217 super(`Empty response from git server.`);
3218 this.code = this.name = EmptyServerResponseError.code;
3219 this.data = {};
3220 }
3221}
3222/** @type {'EmptyServerResponseError'} */
3223EmptyServerResponseError.code = 'EmptyServerResponseError';
3224
3225class FastForwardError extends BaseError {
3226 constructor() {
3227 super(`A simple fast-forward merge was not possible.`);
3228 this.code = this.name = FastForwardError.code;
3229 this.data = {};
3230 }
3231}
3232/** @type {'FastForwardError'} */
3233FastForwardError.code = 'FastForwardError';
3234
3235class GitPushError extends BaseError {
3236 /**
3237 * @param {string} prettyDetails
3238 * @param {PushResult} result
3239 */
3240 constructor(prettyDetails, result) {
3241 super(`One or more branches were not updated: ${prettyDetails}`);
3242 this.code = this.name = GitPushError.code;
3243 this.data = { prettyDetails, result };
3244 }
3245}
3246/** @type {'GitPushError'} */
3247GitPushError.code = 'GitPushError';
3248
3249class HttpError extends BaseError {
3250 /**
3251 * @param {number} statusCode
3252 * @param {string} statusMessage
3253 * @param {string} response
3254 */
3255 constructor(statusCode, statusMessage, response) {
3256 super(`HTTP Error: ${statusCode} ${statusMessage}`);
3257 this.code = this.name = HttpError.code;
3258 this.data = { statusCode, statusMessage, response };
3259 }
3260}
3261/** @type {'HttpError'} */
3262HttpError.code = 'HttpError';
3263
3264class InvalidFilepathError extends BaseError {
3265 /**
3266 * @param {'leading-slash'|'trailing-slash'|'directory'} [reason]
3267 */
3268 constructor(reason) {
3269 let message = 'invalid filepath';
3270 if (reason === 'leading-slash' || reason === 'trailing-slash') {
3271 message = `"filepath" parameter should not include leading or trailing directory separators because these can cause problems on some platforms.`;
3272 } else if (reason === 'directory') {
3273 message = `"filepath" should not be a directory.`;
3274 }
3275 super(message);
3276 this.code = this.name = InvalidFilepathError.code;
3277 this.data = { reason };
3278 }
3279}
3280/** @type {'InvalidFilepathError'} */
3281InvalidFilepathError.code = 'InvalidFilepathError';
3282
3283class InvalidRefNameError extends BaseError {
3284 /**
3285 * @param {string} ref
3286 * @param {string} suggestion
3287 * @param {boolean} canForce
3288 */
3289 constructor(ref, suggestion) {
3290 super(
3291 `"${ref}" would be an invalid git reference. (Hint: a valid alternative would be "${suggestion}".)`
3292 );
3293 this.code = this.name = InvalidRefNameError.code;
3294 this.data = { ref, suggestion };
3295 }
3296}
3297/** @type {'InvalidRefNameError'} */
3298InvalidRefNameError.code = 'InvalidRefNameError';
3299
3300class MaxDepthError extends BaseError {
3301 /**
3302 * @param {number} depth
3303 */
3304 constructor(depth) {
3305 super(`Maximum search depth of ${depth} exceeded.`);
3306 this.code = this.name = MaxDepthError.code;
3307 this.data = { depth };
3308 }
3309}
3310/** @type {'MaxDepthError'} */
3311MaxDepthError.code = 'MaxDepthError';
3312
3313class MergeNotSupportedError extends BaseError {
3314 constructor() {
3315 super(`Merges with conflicts are not supported yet.`);
3316 this.code = this.name = MergeNotSupportedError.code;
3317 this.data = {};
3318 }
3319}
3320/** @type {'MergeNotSupportedError'} */
3321MergeNotSupportedError.code = 'MergeNotSupportedError';
3322
3323class MergeConflictError extends BaseError {
3324 /**
3325 * @param {Array<string>} filepaths
3326 * @param {Array<string>} bothModified
3327 * @param {Array<string>} deleteByUs
3328 * @param {Array<string>} deleteByTheirs
3329 */
3330 constructor(filepaths, bothModified, deleteByUs, deleteByTheirs) {
3331 super(
3332 `Automatic merge failed with one or more merge conflicts in the following files: ${filepaths.toString()}. Fix conflicts then commit the result.`
3333 );
3334 this.code = this.name = MergeConflictError.code;
3335 this.data = { filepaths, bothModified, deleteByUs, deleteByTheirs };
3336 }
3337}
3338/** @type {'MergeConflictError'} */
3339MergeConflictError.code = 'MergeConflictError';
3340
3341class MissingNameError extends BaseError {
3342 /**
3343 * @param {'author'|'committer'|'tagger'} role
3344 */
3345 constructor(role) {
3346 super(
3347 `No name was provided for ${role} in the argument or in the .git/config file.`
3348 );
3349 this.code = this.name = MissingNameError.code;
3350 this.data = { role };
3351 }
3352}
3353/** @type {'MissingNameError'} */
3354MissingNameError.code = 'MissingNameError';
3355
3356class MissingParameterError extends BaseError {
3357 /**
3358 * @param {string} parameter
3359 */
3360 constructor(parameter) {
3361 super(
3362 `The function requires a "${parameter}" parameter but none was provided.`
3363 );
3364 this.code = this.name = MissingParameterError.code;
3365 this.data = { parameter };
3366 }
3367}
3368/** @type {'MissingParameterError'} */
3369MissingParameterError.code = 'MissingParameterError';
3370
3371class MultipleGitError extends BaseError {
3372 /**
3373 * @param {Error[]} errors
3374 * @param {string} message
3375 */
3376 constructor(errors) {
3377 super(
3378 `There are multiple errors that were thrown by the method. Please refer to the "errors" property to see more`
3379 );
3380 this.code = this.name = MultipleGitError.code;
3381 this.data = { errors };
3382 this.errors = errors;
3383 }
3384}
3385/** @type {'MultipleGitError'} */
3386MultipleGitError.code = 'MultipleGitError';
3387
3388class ParseError extends BaseError {
3389 /**
3390 * @param {string} expected
3391 * @param {string} actual
3392 */
3393 constructor(expected, actual) {
3394 super(`Expected "${expected}" but received "${actual}".`);
3395 this.code = this.name = ParseError.code;
3396 this.data = { expected, actual };
3397 }
3398}
3399/** @type {'ParseError'} */
3400ParseError.code = 'ParseError';
3401
3402class PushRejectedError extends BaseError {
3403 /**
3404 * @param {'not-fast-forward'|'tag-exists'} reason
3405 */
3406 constructor(reason) {
3407 let message = '';
3408 if (reason === 'not-fast-forward') {
3409 message = ' because it was not a simple fast-forward';
3410 } else if (reason === 'tag-exists') {
3411 message = ' because tag already exists';
3412 }
3413 super(`Push rejected${message}. Use "force: true" to override.`);
3414 this.code = this.name = PushRejectedError.code;
3415 this.data = { reason };
3416 }
3417}
3418/** @type {'PushRejectedError'} */
3419PushRejectedError.code = 'PushRejectedError';
3420
3421class RemoteCapabilityError extends BaseError {
3422 /**
3423 * @param {'shallow'|'deepen-since'|'deepen-not'|'deepen-relative'} capability
3424 * @param {'depth'|'since'|'exclude'|'relative'} parameter
3425 */
3426 constructor(capability, parameter) {
3427 super(
3428 `Remote does not support the "${capability}" so the "${parameter}" parameter cannot be used.`
3429 );
3430 this.code = this.name = RemoteCapabilityError.code;
3431 this.data = { capability, parameter };
3432 }
3433}
3434/** @type {'RemoteCapabilityError'} */
3435RemoteCapabilityError.code = 'RemoteCapabilityError';
3436
3437class SmartHttpError extends BaseError {
3438 /**
3439 * @param {string} preview
3440 * @param {string} response
3441 */
3442 constructor(preview, response) {
3443 super(
3444 `Remote did not reply using the "smart" HTTP protocol. Expected "001e# service=git-upload-pack" but received: ${preview}`
3445 );
3446 this.code = this.name = SmartHttpError.code;
3447 this.data = { preview, response };
3448 }
3449}
3450/** @type {'SmartHttpError'} */
3451SmartHttpError.code = 'SmartHttpError';
3452
3453class UnknownTransportError extends BaseError {
3454 /**
3455 * @param {string} url
3456 * @param {string} transport
3457 * @param {string} [suggestion]
3458 */
3459 constructor(url, transport, suggestion) {
3460 super(
3461 `Git remote "${url}" uses an unrecognized transport protocol: "${transport}"`
3462 );
3463 this.code = this.name = UnknownTransportError.code;
3464 this.data = { url, transport, suggestion };
3465 }
3466}
3467/** @type {'UnknownTransportError'} */
3468UnknownTransportError.code = 'UnknownTransportError';
3469
3470class UrlParseError extends BaseError {
3471 /**
3472 * @param {string} url
3473 */
3474 constructor(url) {
3475 super(`Cannot parse remote URL: "${url}"`);
3476 this.code = this.name = UrlParseError.code;
3477 this.data = { url };
3478 }
3479}
3480/** @type {'UrlParseError'} */
3481UrlParseError.code = 'UrlParseError';
3482
3483class UserCanceledError extends BaseError {
3484 constructor() {
3485 super(`The operation was canceled.`);
3486 this.code = this.name = UserCanceledError.code;
3487 this.data = {};
3488 }
3489}
3490/** @type {'UserCanceledError'} */
3491UserCanceledError.code = 'UserCanceledError';
3492
3493class IndexResetError extends BaseError {
3494 /**
3495 * @param {Array<string>} filepaths
3496 */
3497 constructor(filepath) {
3498 super(
3499 `Could not merge index: Entry for '${filepath}' is not up to date. Either reset the index entry to HEAD, or stage your unstaged chages.`
3500 );
3501 this.code = this.name = IndexResetError.code;
3502 this.data = { filepath };
3503 }
3504}
3505/** @type {'IndexResetError'} */
3506IndexResetError.code = 'IndexResetError';
3507
3508
3509
3510var Errors = /*#__PURE__*/Object.freeze({
3511 __proto__: null,
3512 AlreadyExistsError: AlreadyExistsError,
3513 AmbiguousError: AmbiguousError,
3514 CheckoutConflictError: CheckoutConflictError,
3515 CommitNotFetchedError: CommitNotFetchedError,
3516 EmptyServerResponseError: EmptyServerResponseError,
3517 FastForwardError: FastForwardError,
3518 GitPushError: GitPushError,
3519 HttpError: HttpError,
3520 InternalError: InternalError,
3521 InvalidFilepathError: InvalidFilepathError,
3522 InvalidOidError: InvalidOidError,
3523 InvalidRefNameError: InvalidRefNameError,
3524 MaxDepthError: MaxDepthError,
3525 MergeNotSupportedError: MergeNotSupportedError,
3526 MergeConflictError: MergeConflictError,
3527 MissingNameError: MissingNameError,
3528 MissingParameterError: MissingParameterError,
3529 MultipleGitError: MultipleGitError,
3530 NoRefspecError: NoRefspecError,
3531 NotFoundError: NotFoundError,
3532 ObjectTypeError: ObjectTypeError,
3533 ParseError: ParseError,
3534 PushRejectedError: PushRejectedError,
3535 RemoteCapabilityError: RemoteCapabilityError,
3536 SmartHttpError: SmartHttpError,
3537 UnknownTransportError: UnknownTransportError,
3538 UnsafeFilepathError: UnsafeFilepathError,
3539 UrlParseError: UrlParseError,
3540 UserCanceledError: UserCanceledError,
3541 UnmergedPathsError: UnmergedPathsError,
3542 IndexResetError: IndexResetError
3543});
3544
3545function formatAuthor({ name, email, timestamp, timezoneOffset }) {
3546 timezoneOffset = formatTimezoneOffset(timezoneOffset);
3547 return `${name} <${email}> ${timestamp} ${timezoneOffset}`
3548}
3549
3550// The amount of effort that went into crafting these cases to handle
3551// -0 (just so we don't lose that information when parsing and reconstructing)
3552// but can also default to +0 was extraordinary.
3553
3554function formatTimezoneOffset(minutes) {
3555 const sign = simpleSign(negateExceptForZero(minutes));
3556 minutes = Math.abs(minutes);
3557 const hours = Math.floor(minutes / 60);
3558 minutes -= hours * 60;
3559 let strHours = String(hours);
3560 let strMinutes = String(minutes);
3561 if (strHours.length < 2) strHours = '0' + strHours;
3562 if (strMinutes.length < 2) strMinutes = '0' + strMinutes;
3563 return (sign === -1 ? '-' : '+') + strHours + strMinutes
3564}
3565
3566function simpleSign(n) {
3567 return Math.sign(n) || (Object.is(n, -0) ? -1 : 1)
3568}
3569
3570function negateExceptForZero(n) {
3571 return n === 0 ? n : -n
3572}
3573
3574function normalizeNewlines(str) {
3575 // remove all <CR>
3576 str = str.replace(/\r/g, '');
3577 // no extra newlines up front
3578 str = str.replace(/^\n+/, '');
3579 // and a single newline at the end
3580 str = str.replace(/\n+$/, '') + '\n';
3581 return str
3582}
3583
3584function parseAuthor(author) {
3585 const [, name, email, timestamp, offset] = author.match(
3586 /^(.*) <(.*)> (.*) (.*)$/
3587 );
3588 return {
3589 name: name,
3590 email: email,
3591 timestamp: Number(timestamp),
3592 timezoneOffset: parseTimezoneOffset(offset),
3593 }
3594}
3595
3596// The amount of effort that went into crafting these cases to handle
3597// -0 (just so we don't lose that information when parsing and reconstructing)
3598// but can also default to +0 was extraordinary.
3599
3600function parseTimezoneOffset(offset) {
3601 let [, sign, hours, minutes] = offset.match(/(\+|-)(\d\d)(\d\d)/);
3602 minutes = (sign === '+' ? 1 : -1) * (Number(hours) * 60 + Number(minutes));
3603 return negateExceptForZero$1(minutes)
3604}
3605
3606function negateExceptForZero$1(n) {
3607 return n === 0 ? n : -n
3608}
3609
3610class GitAnnotatedTag {
3611 constructor(tag) {
3612 if (typeof tag === 'string') {
3613 this._tag = tag;
3614 } else if (Buffer.isBuffer(tag)) {
3615 this._tag = tag.toString('utf8');
3616 } else if (typeof tag === 'object') {
3617 this._tag = GitAnnotatedTag.render(tag);
3618 } else {
3619 throw new InternalError(
3620 'invalid type passed to GitAnnotatedTag constructor'
3621 )
3622 }
3623 }
3624
3625 static from(tag) {
3626 return new GitAnnotatedTag(tag)
3627 }
3628
3629 static render(obj) {
3630 return `object ${obj.object}
3631type ${obj.type}
3632tag ${obj.tag}
3633tagger ${formatAuthor(obj.tagger)}
3634
3635${obj.message}
3636${obj.gpgsig ? obj.gpgsig : ''}`
3637 }
3638
3639 justHeaders() {
3640 return this._tag.slice(0, this._tag.indexOf('\n\n'))
3641 }
3642
3643 message() {
3644 const tag = this.withoutSignature();
3645 return tag.slice(tag.indexOf('\n\n') + 2)
3646 }
3647
3648 parse() {
3649 return Object.assign(this.headers(), {
3650 message: this.message(),
3651 gpgsig: this.gpgsig(),
3652 })
3653 }
3654
3655 render() {
3656 return this._tag
3657 }
3658
3659 headers() {
3660 const headers = this.justHeaders().split('\n');
3661 const hs = [];
3662 for (const h of headers) {
3663 if (h[0] === ' ') {
3664 // combine with previous header (without space indent)
3665 hs[hs.length - 1] += '\n' + h.slice(1);
3666 } else {
3667 hs.push(h);
3668 }
3669 }
3670 const obj = {};
3671 for (const h of hs) {
3672 const key = h.slice(0, h.indexOf(' '));
3673 const value = h.slice(h.indexOf(' ') + 1);
3674 if (Array.isArray(obj[key])) {
3675 obj[key].push(value);
3676 } else {
3677 obj[key] = value;
3678 }
3679 }
3680 if (obj.tagger) {
3681 obj.tagger = parseAuthor(obj.tagger);
3682 }
3683 if (obj.committer) {
3684 obj.committer = parseAuthor(obj.committer);
3685 }
3686 return obj
3687 }
3688
3689 withoutSignature() {
3690 const tag = normalizeNewlines(this._tag);
3691 if (tag.indexOf('\n-----BEGIN PGP SIGNATURE-----') === -1) return tag
3692 return tag.slice(0, tag.lastIndexOf('\n-----BEGIN PGP SIGNATURE-----'))
3693 }
3694
3695 gpgsig() {
3696 if (this._tag.indexOf('\n-----BEGIN PGP SIGNATURE-----') === -1) return
3697 const signature = this._tag.slice(
3698 this._tag.indexOf('-----BEGIN PGP SIGNATURE-----'),
3699 this._tag.indexOf('-----END PGP SIGNATURE-----') +
3700 '-----END PGP SIGNATURE-----'.length
3701 );
3702 return normalizeNewlines(signature)
3703 }
3704
3705 payload() {
3706 return this.withoutSignature() + '\n'
3707 }
3708
3709 toObject() {
3710 return Buffer.from(this._tag, 'utf8')
3711 }
3712
3713 static async sign(tag, sign, secretKey) {
3714 const payload = tag.payload();
3715 let { signature } = await sign({ payload, secretKey });
3716 // renormalize the line endings to the one true line-ending
3717 signature = normalizeNewlines(signature);
3718 const signedTag = payload + signature;
3719 // return a new tag object
3720 return GitAnnotatedTag.from(signedTag)
3721 }
3722}
3723
3724function indent(str) {
3725 return (
3726 str
3727 .trim()
3728 .split('\n')
3729 .map(x => ' ' + x)
3730 .join('\n') + '\n'
3731 )
3732}
3733
3734function outdent(str) {
3735 return str
3736 .split('\n')
3737 .map(x => x.replace(/^ /, ''))
3738 .join('\n')
3739}
3740
3741class GitCommit {
3742 constructor(commit) {
3743 if (typeof commit === 'string') {
3744 this._commit = commit;
3745 } else if (Buffer.isBuffer(commit)) {
3746 this._commit = commit.toString('utf8');
3747 } else if (typeof commit === 'object') {
3748 this._commit = GitCommit.render(commit);
3749 } else {
3750 throw new InternalError('invalid type passed to GitCommit constructor')
3751 }
3752 }
3753
3754 static fromPayloadSignature({ payload, signature }) {
3755 const headers = GitCommit.justHeaders(payload);
3756 const message = GitCommit.justMessage(payload);
3757 const commit = normalizeNewlines(
3758 headers + '\ngpgsig' + indent(signature) + '\n' + message
3759 );
3760 return new GitCommit(commit)
3761 }
3762
3763 static from(commit) {
3764 return new GitCommit(commit)
3765 }
3766
3767 toObject() {
3768 return Buffer.from(this._commit, 'utf8')
3769 }
3770
3771 // Todo: allow setting the headers and message
3772 headers() {
3773 return this.parseHeaders()
3774 }
3775
3776 // Todo: allow setting the headers and message
3777 message() {
3778 return GitCommit.justMessage(this._commit)
3779 }
3780
3781 parse() {
3782 return Object.assign({ message: this.message() }, this.headers())
3783 }
3784
3785 static justMessage(commit) {
3786 return normalizeNewlines(commit.slice(commit.indexOf('\n\n') + 2))
3787 }
3788
3789 static justHeaders(commit) {
3790 return commit.slice(0, commit.indexOf('\n\n'))
3791 }
3792
3793 parseHeaders() {
3794 const headers = GitCommit.justHeaders(this._commit).split('\n');
3795 const hs = [];
3796 for (const h of headers) {
3797 if (h[0] === ' ') {
3798 // combine with previous header (without space indent)
3799 hs[hs.length - 1] += '\n' + h.slice(1);
3800 } else {
3801 hs.push(h);
3802 }
3803 }
3804 const obj = {
3805 parent: [],
3806 };
3807 for (const h of hs) {
3808 const key = h.slice(0, h.indexOf(' '));
3809 const value = h.slice(h.indexOf(' ') + 1);
3810 if (Array.isArray(obj[key])) {
3811 obj[key].push(value);
3812 } else {
3813 obj[key] = value;
3814 }
3815 }
3816 if (obj.author) {
3817 obj.author = parseAuthor(obj.author);
3818 }
3819 if (obj.committer) {
3820 obj.committer = parseAuthor(obj.committer);
3821 }
3822 return obj
3823 }
3824
3825 static renderHeaders(obj) {
3826 let headers = '';
3827 if (obj.tree) {
3828 headers += `tree ${obj.tree}\n`;
3829 } else {
3830 headers += `tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n`; // the null tree
3831 }
3832 if (obj.parent) {
3833 if (obj.parent.length === undefined) {
3834 throw new InternalError(`commit 'parent' property should be an array`)
3835 }
3836 for (const p of obj.parent) {
3837 headers += `parent ${p}\n`;
3838 }
3839 }
3840 const author = obj.author;
3841 headers += `author ${formatAuthor(author)}\n`;
3842 const committer = obj.committer || obj.author;
3843 headers += `committer ${formatAuthor(committer)}\n`;
3844 if (obj.gpgsig) {
3845 headers += 'gpgsig' + indent(obj.gpgsig);
3846 }
3847 return headers
3848 }
3849
3850 static render(obj) {
3851 return GitCommit.renderHeaders(obj) + '\n' + normalizeNewlines(obj.message)
3852 }
3853
3854 render() {
3855 return this._commit
3856 }
3857
3858 withoutSignature() {
3859 const commit = normalizeNewlines(this._commit);
3860 if (commit.indexOf('\ngpgsig') === -1) return commit
3861 const headers = commit.slice(0, commit.indexOf('\ngpgsig'));
3862 const message = commit.slice(
3863 commit.indexOf('-----END PGP SIGNATURE-----\n') +
3864 '-----END PGP SIGNATURE-----\n'.length
3865 );
3866 return normalizeNewlines(headers + '\n' + message)
3867 }
3868
3869 isolateSignature() {
3870 const signature = this._commit.slice(
3871 this._commit.indexOf('-----BEGIN PGP SIGNATURE-----'),
3872 this._commit.indexOf('-----END PGP SIGNATURE-----') +
3873 '-----END PGP SIGNATURE-----'.length
3874 );
3875 return outdent(signature)
3876 }
3877
3878 static async sign(commit, sign, secretKey) {
3879 const payload = commit.withoutSignature();
3880 const message = GitCommit.justMessage(commit._commit);
3881 let { signature } = await sign({ payload, secretKey });
3882 // renormalize the line endings to the one true line-ending
3883 signature = normalizeNewlines(signature);
3884 const headers = GitCommit.justHeaders(commit._commit);
3885 const signedCommit =
3886 headers + '\n' + 'gpgsig' + indent(signature) + '\n' + message;
3887 // return a new commit object
3888 return GitCommit.from(signedCommit)
3889 }
3890}
3891
3892async function resolveTree({ fs, cache, gitdir, oid }) {
3893 // Empty tree - bypass `readObject`
3894 if (oid === '4b825dc642cb6eb9a060e54bf8d69288fbee4904') {
3895 return { tree: GitTree.from([]), oid }
3896 }
3897 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
3898 // Resolve annotated tag objects to whatever
3899 if (type === 'tag') {
3900 oid = GitAnnotatedTag.from(object).parse().object;
3901 return resolveTree({ fs, cache, gitdir, oid })
3902 }
3903 // Resolve commits to trees
3904 if (type === 'commit') {
3905 oid = GitCommit.from(object).parse().tree;
3906 return resolveTree({ fs, cache, gitdir, oid })
3907 }
3908 if (type !== 'tree') {
3909 throw new ObjectTypeError(oid, type, 'tree')
3910 }
3911 return { tree: GitTree.from(object), oid }
3912}
3913
3914class GitWalkerRepo {
3915 constructor({ fs, gitdir, ref, cache }) {
3916 this.fs = fs;
3917 this.cache = cache;
3918 this.gitdir = gitdir;
3919 this.mapPromise = (async () => {
3920 const map = new Map();
3921 let oid;
3922 try {
3923 oid = await GitRefManager.resolve({ fs, gitdir, ref });
3924 } catch (e) {
3925 if (e instanceof NotFoundError) {
3926 // Handle fresh branches with no commits
3927 oid = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
3928 }
3929 }
3930 const tree = await resolveTree({ fs, cache: this.cache, gitdir, oid });
3931 tree.type = 'tree';
3932 tree.mode = '40000';
3933 map.set('.', tree);
3934 return map
3935 })();
3936 const walker = this;
3937 this.ConstructEntry = class TreeEntry {
3938 constructor(fullpath) {
3939 this._fullpath = fullpath;
3940 this._type = false;
3941 this._mode = false;
3942 this._stat = false;
3943 this._content = false;
3944 this._oid = false;
3945 }
3946
3947 async type() {
3948 return walker.type(this)
3949 }
3950
3951 async mode() {
3952 return walker.mode(this)
3953 }
3954
3955 async stat() {
3956 return walker.stat(this)
3957 }
3958
3959 async content() {
3960 return walker.content(this)
3961 }
3962
3963 async oid() {
3964 return walker.oid(this)
3965 }
3966 };
3967 }
3968
3969 async readdir(entry) {
3970 const filepath = entry._fullpath;
3971 const { fs, cache, gitdir } = this;
3972 const map = await this.mapPromise;
3973 const obj = map.get(filepath);
3974 if (!obj) throw new Error(`No obj for ${filepath}`)
3975 const oid = obj.oid;
3976 if (!oid) throw new Error(`No oid for obj ${JSON.stringify(obj)}`)
3977 if (obj.type !== 'tree') {
3978 // TODO: support submodules (type === 'commit')
3979 return null
3980 }
3981 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
3982 if (type !== obj.type) {
3983 throw new ObjectTypeError(oid, type, obj.type)
3984 }
3985 const tree = GitTree.from(object);
3986 // cache all entries
3987 for (const entry of tree) {
3988 map.set(join(filepath, entry.path), entry);
3989 }
3990 return tree.entries().map(entry => join(filepath, entry.path))
3991 }
3992
3993 async type(entry) {
3994 if (entry._type === false) {
3995 const map = await this.mapPromise;
3996 const { type } = map.get(entry._fullpath);
3997 entry._type = type;
3998 }
3999 return entry._type
4000 }
4001
4002 async mode(entry) {
4003 if (entry._mode === false) {
4004 const map = await this.mapPromise;
4005 const { mode } = map.get(entry._fullpath);
4006 entry._mode = normalizeMode(parseInt(mode, 8));
4007 }
4008 return entry._mode
4009 }
4010
4011 async stat(_entry) {}
4012
4013 async content(entry) {
4014 if (entry._content === false) {
4015 const map = await this.mapPromise;
4016 const { fs, cache, gitdir } = this;
4017 const obj = map.get(entry._fullpath);
4018 const oid = obj.oid;
4019 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
4020 if (type !== 'blob') {
4021 entry._content = undefined;
4022 } else {
4023 entry._content = new Uint8Array(object);
4024 }
4025 }
4026 return entry._content
4027 }
4028
4029 async oid(entry) {
4030 if (entry._oid === false) {
4031 const map = await this.mapPromise;
4032 const obj = map.get(entry._fullpath);
4033 entry._oid = obj.oid;
4034 }
4035 return entry._oid
4036 }
4037}
4038
4039// @ts-check
4040
4041/**
4042 * @param {object} args
4043 * @param {string} [args.ref='HEAD']
4044 * @returns {Walker}
4045 */
4046function TREE({ ref = 'HEAD' } = {}) {
4047 const o = Object.create(null);
4048 Object.defineProperty(o, GitWalkSymbol, {
4049 value: function({ fs, gitdir, cache }) {
4050 return new GitWalkerRepo({ fs, gitdir, ref, cache })
4051 },
4052 });
4053 Object.freeze(o);
4054 return o
4055}
4056
4057// @ts-check
4058
4059class GitWalkerFs {
4060 constructor({ fs, dir, gitdir, cache }) {
4061 this.fs = fs;
4062 this.cache = cache;
4063 this.dir = dir;
4064 this.gitdir = gitdir;
4065 const walker = this;
4066 this.ConstructEntry = class WorkdirEntry {
4067 constructor(fullpath) {
4068 this._fullpath = fullpath;
4069 this._type = false;
4070 this._mode = false;
4071 this._stat = false;
4072 this._content = false;
4073 this._oid = false;
4074 }
4075
4076 async type() {
4077 return walker.type(this)
4078 }
4079
4080 async mode() {
4081 return walker.mode(this)
4082 }
4083
4084 async stat() {
4085 return walker.stat(this)
4086 }
4087
4088 async content() {
4089 return walker.content(this)
4090 }
4091
4092 async oid() {
4093 return walker.oid(this)
4094 }
4095 };
4096 }
4097
4098 async readdir(entry) {
4099 const filepath = entry._fullpath;
4100 const { fs, dir } = this;
4101 const names = await fs.readdir(join(dir, filepath));
4102 if (names === null) return null
4103 return names.map(name => join(filepath, name))
4104 }
4105
4106 async type(entry) {
4107 if (entry._type === false) {
4108 await entry.stat();
4109 }
4110 return entry._type
4111 }
4112
4113 async mode(entry) {
4114 if (entry._mode === false) {
4115 await entry.stat();
4116 }
4117 return entry._mode
4118 }
4119
4120 async stat(entry) {
4121 if (entry._stat === false) {
4122 const { fs, dir } = this;
4123 let stat = await fs.lstat(`${dir}/${entry._fullpath}`);
4124 if (!stat) {
4125 throw new Error(
4126 `ENOENT: no such file or directory, lstat '${entry._fullpath}'`
4127 )
4128 }
4129 let type = stat.isDirectory() ? 'tree' : 'blob';
4130 if (type === 'blob' && !stat.isFile() && !stat.isSymbolicLink()) {
4131 type = 'special';
4132 }
4133 entry._type = type;
4134 stat = normalizeStats(stat);
4135 entry._mode = stat.mode;
4136 // workaround for a BrowserFS edge case
4137 if (stat.size === -1 && entry._actualSize) {
4138 stat.size = entry._actualSize;
4139 }
4140 entry._stat = stat;
4141 }
4142 return entry._stat
4143 }
4144
4145 async content(entry) {
4146 if (entry._content === false) {
4147 const { fs, dir } = this;
4148 if ((await entry.type()) === 'tree') {
4149 entry._content = undefined;
4150 } else {
4151 const content = await fs.read(`${dir}/${entry._fullpath}`);
4152 // workaround for a BrowserFS edge case
4153 entry._actualSize = content.length;
4154 if (entry._stat && entry._stat.size === -1) {
4155 entry._stat.size = entry._actualSize;
4156 }
4157 entry._content = new Uint8Array(content);
4158 }
4159 }
4160 return entry._content
4161 }
4162
4163 async oid(entry) {
4164 if (entry._oid === false) {
4165 const { fs, gitdir, cache } = this;
4166 let oid;
4167 // See if we can use the SHA1 hash in the index.
4168 await GitIndexManager.acquire({ fs, gitdir, cache }, async function(
4169 index
4170 ) {
4171 const stage = index.entriesMap.get(entry._fullpath);
4172 const stats = await entry.stat();
4173 if (!stage || compareStats(stats, stage)) {
4174 const content = await entry.content();
4175 if (content === undefined) {
4176 oid = undefined;
4177 } else {
4178 oid = await shasum(
4179 GitObject.wrap({ type: 'blob', object: await entry.content() })
4180 );
4181 // Update the stats in the index so we will get a "cache hit" next time
4182 // 1) if we can (because the oid and mode are the same)
4183 // 2) and only if we need to (because other stats differ)
4184 if (
4185 stage &&
4186 oid === stage.oid &&
4187 stats.mode === stage.mode &&
4188 compareStats(stats, stage)
4189 ) {
4190 index.insert({
4191 filepath: entry._fullpath,
4192 stats,
4193 oid: oid,
4194 });
4195 }
4196 }
4197 } else {
4198 // Use the index SHA1 rather than compute it
4199 oid = stage.oid;
4200 }
4201 });
4202 entry._oid = oid;
4203 }
4204 return entry._oid
4205 }
4206}
4207
4208// @ts-check
4209
4210/**
4211 * @returns {Walker}
4212 */
4213function WORKDIR() {
4214 const o = Object.create(null);
4215 Object.defineProperty(o, GitWalkSymbol, {
4216 value: function({ fs, dir, gitdir, cache }) {
4217 return new GitWalkerFs({ fs, dir, gitdir, cache })
4218 },
4219 });
4220 Object.freeze(o);
4221 return o
4222}
4223
4224// @ts-check
4225
4226// https://dev.to/namirsab/comment/2050
4227function arrayRange(start, end) {
4228 const length = end - start;
4229 return Array.from({ length }, (_, i) => start + i)
4230}
4231
4232// TODO: Should I just polyfill Array.flat?
4233const flat =
4234 typeof Array.prototype.flat === 'undefined'
4235 ? entries => entries.reduce((acc, x) => acc.concat(x), [])
4236 : entries => entries.flat();
4237
4238// This is convenient for computing unions/joins of sorted lists.
4239class RunningMinimum {
4240 constructor() {
4241 // Using a getter for 'value' would just bloat the code.
4242 // You know better than to set it directly right?
4243 this.value = null;
4244 }
4245
4246 consider(value) {
4247 if (value === null || value === undefined) return
4248 if (this.value === null) {
4249 this.value = value;
4250 } else if (value < this.value) {
4251 this.value = value;
4252 }
4253 }
4254
4255 reset() {
4256 this.value = null;
4257 }
4258}
4259
4260// Take an array of length N of
4261// iterators of length Q_n
4262// of strings
4263// and return an iterator of length max(Q_n) for all n
4264// of arrays of length N
4265// of string|null who all have the same string value
4266function* unionOfIterators(sets) {
4267 /* NOTE: We can assume all arrays are sorted.
4268 * Indexes are sorted because they are defined that way:
4269 *
4270 * > Index entries are sorted in ascending order on the name field,
4271 * > interpreted as a string of unsigned bytes (i.e. memcmp() order, no
4272 * > localization, no special casing of directory separator '/'). Entries
4273 * > with the same name are sorted by their stage field.
4274 *
4275 * Trees should be sorted because they are created directly from indexes.
4276 * They definitely should be sorted, or else they wouldn't have a unique SHA1.
4277 * So that would be very naughty on the part of the tree-creator.
4278 *
4279 * Lastly, the working dir entries are sorted because I choose to sort them
4280 * in my FileSystem.readdir() implementation.
4281 */
4282
4283 // Init
4284 const min = new RunningMinimum();
4285 let minimum;
4286 const heads = [];
4287 const numsets = sets.length;
4288 for (let i = 0; i < numsets; i++) {
4289 // Abuse the fact that iterators continue to return 'undefined' for value
4290 // once they are done
4291 heads[i] = sets[i].next().value;
4292 if (heads[i] !== undefined) {
4293 min.consider(heads[i]);
4294 }
4295 }
4296 if (min.value === null) return
4297 // Iterate
4298 while (true) {
4299 const result = [];
4300 minimum = min.value;
4301 min.reset();
4302 for (let i = 0; i < numsets; i++) {
4303 if (heads[i] !== undefined && heads[i] === minimum) {
4304 result[i] = heads[i];
4305 heads[i] = sets[i].next().value;
4306 } else {
4307 // A little hacky, but eh
4308 result[i] = null;
4309 }
4310 if (heads[i] !== undefined) {
4311 min.consider(heads[i]);
4312 }
4313 }
4314 yield result;
4315 if (min.value === null) return
4316 }
4317}
4318
4319// @ts-check
4320
4321/**
4322 * @param {object} args
4323 * @param {import('../models/FileSystem.js').FileSystem} args.fs
4324 * @param {object} args.cache
4325 * @param {string} [args.dir]
4326 * @param {string} [args.gitdir=join(dir,'.git')]
4327 * @param {Walker[]} args.trees
4328 * @param {WalkerMap} [args.map]
4329 * @param {WalkerReduce} [args.reduce]
4330 * @param {WalkerIterate} [args.iterate]
4331 *
4332 * @returns {Promise<any>} The finished tree-walking result
4333 *
4334 * @see {WalkerMap}
4335 *
4336 */
4337async function _walk({
4338 fs,
4339 cache,
4340 dir,
4341 gitdir,
4342 trees,
4343 // @ts-ignore
4344 map = async (_, entry) => entry,
4345 // The default reducer is a flatmap that filters out undefineds.
4346 reduce = async (parent, children) => {
4347 const flatten = flat(children);
4348 if (parent !== undefined) flatten.unshift(parent);
4349 return flatten
4350 },
4351 // The default iterate function walks all children concurrently
4352 iterate = (walk, children) => Promise.all([...children].map(walk)),
4353}) {
4354 const walkers = trees.map(proxy =>
4355 proxy[GitWalkSymbol]({ fs, dir, gitdir, cache })
4356 );
4357
4358 const root = new Array(walkers.length).fill('.');
4359 const range = arrayRange(0, walkers.length);
4360 const unionWalkerFromReaddir = async entries => {
4361 range.map(i => {
4362 entries[i] = entries[i] && new walkers[i].ConstructEntry(entries[i]);
4363 });
4364 const subdirs = await Promise.all(
4365 range.map(i => (entries[i] ? walkers[i].readdir(entries[i]) : []))
4366 );
4367 // Now process child directories
4368 const iterators = subdirs
4369 .map(array => (array === null ? [] : array))
4370 .map(array => array[Symbol.iterator]());
4371 return {
4372 entries,
4373 children: unionOfIterators(iterators),
4374 }
4375 };
4376
4377 const walk = async root => {
4378 const { entries, children } = await unionWalkerFromReaddir(root);
4379 const fullpath = entries.find(entry => entry && entry._fullpath)._fullpath;
4380 const parent = await map(fullpath, entries);
4381 if (parent !== null) {
4382 let walkedChildren = await iterate(walk, children);
4383 walkedChildren = walkedChildren.filter(x => x !== undefined);
4384 return reduce(parent, walkedChildren)
4385 }
4386 };
4387 return walk(root)
4388}
4389
4390/**
4391 * Removes the directory at the specified filepath recursively. Used internally to replicate the behavior of
4392 * fs.promises.rm({ recursive: true, force: true }) from Node.js 14 and above when not available. If the provided
4393 * filepath resolves to a file, it will be removed.
4394 *
4395 * @param {import('../models/FileSystem.js').FileSystem} fs
4396 * @param {string} filepath - The file or directory to remove.
4397 */
4398async function rmRecursive(fs, filepath) {
4399 const entries = await fs.readdir(filepath);
4400 if (entries == null) {
4401 await fs.rm(filepath);
4402 } else if (entries.length) {
4403 await Promise.all(
4404 entries.map(entry => {
4405 const subpath = join(filepath, entry);
4406 return fs.lstat(subpath).then(stat => {
4407 if (!stat) return
4408 return stat.isDirectory() ? rmRecursive(fs, subpath) : fs.rm(subpath)
4409 })
4410 })
4411 ).then(() => fs.rmdir(filepath));
4412 } else {
4413 await fs.rmdir(filepath);
4414 }
4415}
4416
4417function isPromiseLike(obj) {
4418 return isObject(obj) && isFunction(obj.then) && isFunction(obj.catch)
4419}
4420
4421function isObject(obj) {
4422 return obj && typeof obj === 'object'
4423}
4424
4425function isFunction(obj) {
4426 return typeof obj === 'function'
4427}
4428
4429function isPromiseFs(fs) {
4430 const test = targetFs => {
4431 try {
4432 // If readFile returns a promise then we can probably assume the other
4433 // commands do as well
4434 return targetFs.readFile().catch(e => e)
4435 } catch (e) {
4436 return e
4437 }
4438 };
4439 return isPromiseLike(test(fs))
4440}
4441
4442// List of commands all filesystems are expected to provide. `rm` is not
4443// included since it may not exist and must be handled as a special case
4444const commands = [
4445 'readFile',
4446 'writeFile',
4447 'mkdir',
4448 'rmdir',
4449 'unlink',
4450 'stat',
4451 'lstat',
4452 'readdir',
4453 'readlink',
4454 'symlink',
4455];
4456
4457function bindFs(target, fs) {
4458 if (isPromiseFs(fs)) {
4459 for (const command of commands) {
4460 target[`_${command}`] = fs[command].bind(fs);
4461 }
4462 } else {
4463 for (const command of commands) {
4464 target[`_${command}`] = pify(fs[command].bind(fs));
4465 }
4466 }
4467
4468 // Handle the special case of `rm`
4469 if (isPromiseFs(fs)) {
4470 if (fs.rm) target._rm = fs.rm.bind(fs);
4471 else if (fs.rmdir.length > 1) target._rm = fs.rmdir.bind(fs);
4472 else target._rm = rmRecursive.bind(null, target);
4473 } else {
4474 if (fs.rm) target._rm = pify(fs.rm.bind(fs));
4475 else if (fs.rmdir.length > 2) target._rm = pify(fs.rmdir.bind(fs));
4476 else target._rm = rmRecursive.bind(null, target);
4477 }
4478}
4479
4480/**
4481 * This is just a collection of helper functions really. At least that's how it started.
4482 */
4483class FileSystem {
4484 constructor(fs) {
4485 if (typeof fs._original_unwrapped_fs !== 'undefined') return fs
4486
4487 const promises = Object.getOwnPropertyDescriptor(fs, 'promises');
4488 if (promises && promises.enumerable) {
4489 bindFs(this, fs.promises);
4490 } else {
4491 bindFs(this, fs);
4492 }
4493 this._original_unwrapped_fs = fs;
4494 }
4495
4496 /**
4497 * Return true if a file exists, false if it doesn't exist.
4498 * Rethrows errors that aren't related to file existance.
4499 */
4500 async exists(filepath, options = {}) {
4501 try {
4502 await this._stat(filepath);
4503 return true
4504 } catch (err) {
4505 if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
4506 return false
4507 } else {
4508 console.log('Unhandled error in "FileSystem.exists()" function', err);
4509 throw err
4510 }
4511 }
4512 }
4513
4514 /**
4515 * Return the contents of a file if it exists, otherwise returns null.
4516 *
4517 * @param {string} filepath
4518 * @param {object} [options]
4519 *
4520 * @returns {Promise<Buffer|string|null>}
4521 */
4522 async read(filepath, options = {}) {
4523 try {
4524 let buffer = await this._readFile(filepath, options);
4525 // Convert plain ArrayBuffers to Buffers
4526 if (typeof buffer !== 'string') {
4527 buffer = Buffer.from(buffer);
4528 }
4529 return buffer
4530 } catch (err) {
4531 return null
4532 }
4533 }
4534
4535 /**
4536 * Write a file (creating missing directories if need be) without throwing errors.
4537 *
4538 * @param {string} filepath
4539 * @param {Buffer|Uint8Array|string} contents
4540 * @param {object|string} [options]
4541 */
4542 async write(filepath, contents, options = {}) {
4543 try {
4544 await this._writeFile(filepath, contents, options);
4545 return
4546 } catch (err) {
4547 // Hmm. Let's try mkdirp and try again.
4548 await this.mkdir(dirname(filepath));
4549 await this._writeFile(filepath, contents, options);
4550 }
4551 }
4552
4553 /**
4554 * Make a directory (or series of nested directories) without throwing an error if it already exists.
4555 */
4556 async mkdir(filepath, _selfCall = false) {
4557 try {
4558 await this._mkdir(filepath);
4559 return
4560 } catch (err) {
4561 // If err is null then operation succeeded!
4562 if (err === null) return
4563 // If the directory already exists, that's OK!
4564 if (err.code === 'EEXIST') return
4565 // Avoid infinite loops of failure
4566 if (_selfCall) throw err
4567 // If we got a "no such file or directory error" backup and try again.
4568 if (err.code === 'ENOENT') {
4569 const parent = dirname(filepath);
4570 // Check to see if we've gone too far
4571 if (parent === '.' || parent === '/' || parent === filepath) throw err
4572 // Infinite recursion, what could go wrong?
4573 await this.mkdir(parent);
4574 await this.mkdir(filepath, true);
4575 }
4576 }
4577 }
4578
4579 /**
4580 * Delete a file without throwing an error if it is already deleted.
4581 */
4582 async rm(filepath) {
4583 try {
4584 await this._unlink(filepath);
4585 } catch (err) {
4586 if (err.code !== 'ENOENT') throw err
4587 }
4588 }
4589
4590 /**
4591 * Delete a directory without throwing an error if it is already deleted.
4592 */
4593 async rmdir(filepath, opts) {
4594 try {
4595 if (opts && opts.recursive) {
4596 await this._rm(filepath, opts);
4597 } else {
4598 await this._rmdir(filepath);
4599 }
4600 } catch (err) {
4601 if (err.code !== 'ENOENT') throw err
4602 }
4603 }
4604
4605 /**
4606 * Read a directory without throwing an error is the directory doesn't exist
4607 */
4608 async readdir(filepath) {
4609 try {
4610 const names = await this._readdir(filepath);
4611 // Ordering is not guaranteed, and system specific (Windows vs Unix)
4612 // so we must sort them ourselves.
4613 names.sort(compareStrings);
4614 return names
4615 } catch (err) {
4616 if (err.code === 'ENOTDIR') return null
4617 return []
4618 }
4619 }
4620
4621 /**
4622 * Return a flast list of all the files nested inside a directory
4623 *
4624 * Based on an elegant concurrent recursive solution from SO
4625 * https://stackoverflow.com/a/45130990/2168416
4626 */
4627 async readdirDeep(dir) {
4628 const subdirs = await this._readdir(dir);
4629 const files = await Promise.all(
4630 subdirs.map(async subdir => {
4631 const res = dir + '/' + subdir;
4632 return (await this._stat(res)).isDirectory()
4633 ? this.readdirDeep(res)
4634 : res
4635 })
4636 );
4637 return files.reduce((a, f) => a.concat(f), [])
4638 }
4639
4640 /**
4641 * Return the Stats of a file/symlink if it exists, otherwise returns null.
4642 * Rethrows errors that aren't related to file existance.
4643 */
4644 async lstat(filename) {
4645 try {
4646 const stats = await this._lstat(filename);
4647 return stats
4648 } catch (err) {
4649 if (err.code === 'ENOENT') {
4650 return null
4651 }
4652 throw err
4653 }
4654 }
4655
4656 /**
4657 * Reads the contents of a symlink if it exists, otherwise returns null.
4658 * Rethrows errors that aren't related to file existance.
4659 */
4660 async readlink(filename, opts = { encoding: 'buffer' }) {
4661 // Note: FileSystem.readlink returns a buffer by default
4662 // so we can dump it into GitObject.write just like any other file.
4663 try {
4664 const link = await this._readlink(filename, opts);
4665 return Buffer.isBuffer(link) ? link : Buffer.from(link)
4666 } catch (err) {
4667 if (err.code === 'ENOENT') {
4668 return null
4669 }
4670 throw err
4671 }
4672 }
4673
4674 /**
4675 * Write the contents of buffer to a symlink.
4676 */
4677 async writelink(filename, buffer) {
4678 return this._symlink(buffer.toString('utf8'), filename)
4679 }
4680}
4681
4682function assertParameter(name, value) {
4683 if (value === undefined) {
4684 throw new MissingParameterError(name)
4685 }
4686}
4687
4688// @ts-check
4689/**
4690 *
4691 * @param {WalkerEntry} entry
4692 * @param {WalkerEntry} base
4693 *
4694 */
4695async function modified(entry, base) {
4696 if (!entry && !base) return false
4697 if (entry && !base) return true
4698 if (!entry && base) return true
4699 if ((await entry.type()) === 'tree' && (await base.type()) === 'tree') {
4700 return false
4701 }
4702 if (
4703 (await entry.type()) === (await base.type()) &&
4704 (await entry.mode()) === (await base.mode()) &&
4705 (await entry.oid()) === (await base.oid())
4706 ) {
4707 return false
4708 }
4709 return true
4710}
4711
4712// @ts-check
4713
4714/**
4715 * Abort a merge in progress.
4716 *
4717 * Based on the behavior of git reset --merge, i.e. "Resets the index and updates the files in the working tree that are different between <commit> and HEAD, but keeps those which are different between the index and working tree (i.e. which have changes which have not been added). If a file that is different between <commit> and the index has unstaged changes, reset is aborted."
4718 *
4719 * Essentially, abortMerge will reset any files affected by merge conflicts to their last known good version at HEAD.
4720 * Any unstaged changes are saved and any staged changes are reset as well.
4721 *
4722 * NOTE: The behavior of this command differs slightly from canonical git in that an error will be thrown if a file exists in the index and nowhere else.
4723 * Canonical git will reset the file and continue aborting the merge in this case.
4724 *
4725 * **WARNING:** Running git merge with non-trivial uncommitted changes is discouraged: while possible, it may leave you in a state that is hard to back out of in the case of a conflict.
4726 * If there were uncommitted changes when the merge started (and especially if those changes were further modified after the merge was started), `git.abortMerge` will in some cases be unable to reconstruct the original (pre-merge) changes.
4727 *
4728 * @param {object} args
4729 * @param {FsClient} args.fs - a file system implementation
4730 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
4731 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
4732 * @param {string} [args.commit='HEAD'] - commit to reset the index and worktree to, defaults to HEAD
4733 * @param {object} [args.cache] - a [cache](cache.md) object
4734 *
4735 * @returns {Promise<void>} Resolves successfully once the git index has been updated
4736 *
4737 */
4738async function abortMerge({
4739 fs: _fs,
4740 dir,
4741 gitdir = join(dir, '.git'),
4742 commit = 'HEAD',
4743 cache = {},
4744}) {
4745 try {
4746 assertParameter('fs', _fs);
4747 assertParameter('dir', dir);
4748 assertParameter('gitdir', gitdir);
4749
4750 const fs = new FileSystem(_fs);
4751 const trees = [TREE({ ref: commit }), WORKDIR(), STAGE()];
4752 let unmergedPaths = [];
4753
4754 await GitIndexManager.acquire({ fs, gitdir, cache }, async function(index) {
4755 unmergedPaths = index.unmergedPaths;
4756 });
4757
4758 const results = await _walk({
4759 fs,
4760 cache,
4761 dir,
4762 gitdir,
4763 trees,
4764 map: async function(path, [head, workdir, index]) {
4765 const staged = !(await modified(workdir, index));
4766 const unmerged = unmergedPaths.includes(path);
4767 const unmodified = !(await modified(index, head));
4768
4769 if (staged || unmerged) {
4770 return head
4771 ? {
4772 path,
4773 mode: await head.mode(),
4774 oid: await head.oid(),
4775 type: await head.type(),
4776 content: await head.content(),
4777 }
4778 : undefined
4779 }
4780
4781 if (unmodified) return false
4782 else throw new IndexResetError(path)
4783 },
4784 });
4785
4786 await GitIndexManager.acquire({ fs, gitdir, cache }, async function(index) {
4787 // Reset paths in index and worktree, this can't be done in _walk because the
4788 // STAGE walker acquires its own index lock.
4789
4790 for (const entry of results) {
4791 if (entry === false) continue
4792
4793 // entry is not false, so from here we can assume index = workdir
4794 if (!entry) {
4795 await fs.rmdir(`${dir}/${entry.path}`, { recursive: true });
4796 index.delete({ filepath: entry.path });
4797 continue
4798 }
4799
4800 if (entry.type === 'blob') {
4801 const content = new TextDecoder().decode(entry.content);
4802 await fs.write(`${dir}/${entry.path}`, content, { mode: entry.mode });
4803 index.insert({
4804 filepath: entry.path,
4805 oid: entry.oid,
4806 stage: 0,
4807 });
4808 }
4809 }
4810 });
4811 } catch (err) {
4812 err.caller = 'git.abortMerge';
4813 throw err
4814 }
4815}
4816
4817// I'm putting this in a Manager because I reckon it could benefit
4818// from a LOT of cacheing.
4819class GitIgnoreManager {
4820 static async isIgnored({ fs, dir, gitdir = join(dir, '.git'), filepath }) {
4821 // ALWAYS ignore ".git" folders.
4822 if (basename(filepath) === '.git') return true
4823 // '.' is not a valid gitignore entry, so '.' is never ignored
4824 if (filepath === '.') return false
4825 // Check and load exclusion rules from project exclude file (.git/info/exclude)
4826 let excludes = '';
4827 const excludesFile = join(gitdir, 'info', 'exclude');
4828 if (await fs.exists(excludesFile)) {
4829 excludes = await fs.read(excludesFile, 'utf8');
4830 }
4831 // Find all the .gitignore files that could affect this file
4832 const pairs = [
4833 {
4834 gitignore: join(dir, '.gitignore'),
4835 filepath,
4836 },
4837 ];
4838 const pieces = filepath.split('/').filter(Boolean);
4839 for (let i = 1; i < pieces.length; i++) {
4840 const folder = pieces.slice(0, i).join('/');
4841 const file = pieces.slice(i).join('/');
4842 pairs.push({
4843 gitignore: join(dir, folder, '.gitignore'),
4844 filepath: file,
4845 });
4846 }
4847 let ignoredStatus = false;
4848 for (const p of pairs) {
4849 let file;
4850 try {
4851 file = await fs.read(p.gitignore, 'utf8');
4852 } catch (err) {
4853 if (err.code === 'NOENT') continue
4854 }
4855 const ign = ignore().add(excludes);
4856 ign.add(file);
4857 // If the parent directory is excluded, we are done.
4858 // "It is not possible to re-include a file if a parent directory of that file is excluded. Git doesn’t list excluded directories for performance reasons, so any patterns on contained files have no effect, no matter where they are defined."
4859 // source: https://git-scm.com/docs/gitignore
4860 const parentdir = dirname(p.filepath);
4861 if (parentdir !== '.' && ign.ignores(parentdir)) return true
4862 // If the file is currently ignored, test for UNignoring.
4863 if (ignoredStatus) {
4864 ignoredStatus = !ign.test(p.filepath).unignored;
4865 } else {
4866 ignoredStatus = ign.test(p.filepath).ignored;
4867 }
4868 }
4869 return ignoredStatus
4870 }
4871}
4872
4873async function writeObjectLoose({ fs, gitdir, object, format, oid }) {
4874 if (format !== 'deflated') {
4875 throw new InternalError(
4876 'GitObjectStoreLoose expects objects to write to be in deflated format'
4877 )
4878 }
4879 const source = `objects/${oid.slice(0, 2)}/${oid.slice(2)}`;
4880 const filepath = `${gitdir}/${source}`;
4881 // Don't overwrite existing git objects - this helps avoid EPERM errors.
4882 // Although I don't know how we'd fix corrupted objects then. Perhaps delete them
4883 // on read?
4884 if (!(await fs.exists(filepath))) await fs.write(filepath, object);
4885}
4886
4887/* eslint-env node, browser */
4888
4889let supportsCompressionStream = null;
4890
4891async function deflate(buffer) {
4892 if (supportsCompressionStream === null) {
4893 supportsCompressionStream = testCompressionStream();
4894 }
4895 return supportsCompressionStream
4896 ? browserDeflate(buffer)
4897 : pako.deflate(buffer)
4898}
4899
4900async function browserDeflate(buffer) {
4901 const cs = new CompressionStream('deflate');
4902 const c = new Blob([buffer]).stream().pipeThrough(cs);
4903 return new Uint8Array(await new Response(c).arrayBuffer())
4904}
4905
4906function testCompressionStream() {
4907 try {
4908 const cs = new CompressionStream('deflate');
4909 // Test if `Blob.stream` is present. React Native does not have the `stream` method
4910 const stream = new Blob([]).stream();
4911 stream.cancel();
4912 return !!cs
4913 } catch (_) {
4914 // no bother
4915 }
4916 return false
4917}
4918
4919async function _writeObject({
4920 fs,
4921 gitdir,
4922 type,
4923 object,
4924 format = 'content',
4925 oid = undefined,
4926 dryRun = false,
4927}) {
4928 if (format !== 'deflated') {
4929 if (format !== 'wrapped') {
4930 object = GitObject.wrap({ type, object });
4931 }
4932 oid = await shasum(object);
4933 object = Buffer.from(await deflate(object));
4934 }
4935 if (!dryRun) {
4936 await writeObjectLoose({ fs, gitdir, object, format: 'deflated', oid });
4937 }
4938 return oid
4939}
4940
4941function posixifyPathBuffer(buffer) {
4942 let idx;
4943 while (~(idx = buffer.indexOf(92))) buffer[idx] = 47;
4944 return buffer
4945}
4946
4947// @ts-check
4948
4949/**
4950 * Add a file to the git index (aka staging area)
4951 *
4952 * @param {object} args
4953 * @param {FsClient} args.fs - a file system implementation
4954 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
4955 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
4956 * @param {string|string[]} args.filepath - The path to the file to add to the index
4957 * @param {object} [args.cache] - a [cache](cache.md) object
4958 * @param {boolean} [args.force=false] - add to index even if matches gitignore. Think `git add --force`
4959 * @param {boolean} [args.parallel=false] - process each input file in parallel. Parallel processing will result in more memory consumption but less process time
4960 *
4961 * @returns {Promise<void>} Resolves successfully once the git index has been updated
4962 *
4963 * @example
4964 * await fs.promises.writeFile('/tutorial/README.md', `# TEST`)
4965 * await git.add({ fs, dir: '/tutorial', filepath: 'README.md' })
4966 * console.log('done')
4967 *
4968 */
4969async function add({
4970 fs: _fs,
4971 dir,
4972 gitdir = join(dir, '.git'),
4973 filepath,
4974 cache = {},
4975 force = false,
4976 parallel = true,
4977}) {
4978 try {
4979 assertParameter('fs', _fs);
4980 assertParameter('dir', dir);
4981 assertParameter('gitdir', gitdir);
4982 assertParameter('filepath', filepath);
4983
4984 const fs = new FileSystem(_fs);
4985 await GitIndexManager.acquire({ fs, gitdir, cache }, async index => {
4986 return addToIndex({
4987 dir,
4988 gitdir,
4989 fs,
4990 filepath,
4991 index,
4992 force,
4993 parallel,
4994 })
4995 });
4996 } catch (err) {
4997 err.caller = 'git.add';
4998 throw err
4999 }
5000}
5001
5002async function addToIndex({
5003 dir,
5004 gitdir,
5005 fs,
5006 filepath,
5007 index,
5008 force,
5009 parallel,
5010}) {
5011 // TODO: Should ignore UNLESS it's already in the index.
5012 filepath = Array.isArray(filepath) ? filepath : [filepath];
5013 const promises = filepath.map(async currentFilepath => {
5014 if (!force) {
5015 const ignored = await GitIgnoreManager.isIgnored({
5016 fs,
5017 dir,
5018 gitdir,
5019 filepath: currentFilepath,
5020 });
5021 if (ignored) return
5022 }
5023 const stats = await fs.lstat(join(dir, currentFilepath));
5024 if (!stats) throw new NotFoundError(currentFilepath)
5025
5026 if (stats.isDirectory()) {
5027 const children = await fs.readdir(join(dir, currentFilepath));
5028 if (parallel) {
5029 const promises = children.map(child =>
5030 addToIndex({
5031 dir,
5032 gitdir,
5033 fs,
5034 filepath: [join(currentFilepath, child)],
5035 index,
5036 force,
5037 parallel,
5038 })
5039 );
5040 await Promise.all(promises);
5041 } else {
5042 for (const child of children) {
5043 await addToIndex({
5044 dir,
5045 gitdir,
5046 fs,
5047 filepath: [join(currentFilepath, child)],
5048 index,
5049 force,
5050 parallel,
5051 });
5052 }
5053 }
5054 } else {
5055 const object = stats.isSymbolicLink()
5056 ? await fs.readlink(join(dir, currentFilepath)).then(posixifyPathBuffer)
5057 : await fs.read(join(dir, currentFilepath));
5058 if (object === null) throw new NotFoundError(currentFilepath)
5059 const oid = await _writeObject({ fs, gitdir, type: 'blob', object });
5060 index.insert({ filepath: currentFilepath, stats, oid });
5061 }
5062 });
5063
5064 const settledPromises = await Promise.allSettled(promises);
5065 const rejectedPromises = settledPromises
5066 .filter(settle => settle.status === 'rejected')
5067 .map(settle => settle.reason);
5068 if (rejectedPromises.length > 1) {
5069 throw new MultipleGitError(rejectedPromises)
5070 }
5071 if (rejectedPromises.length === 1) {
5072 throw rejectedPromises[0]
5073 }
5074
5075 const fulfilledPromises = settledPromises
5076 .filter(settle => settle.status === 'fulfilled' && settle.value)
5077 .map(settle => settle.value);
5078
5079 return fulfilledPromises
5080}
5081
5082// @ts-check
5083
5084/**
5085 *
5086 * @param {Object} args
5087 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5088 * @param {object} args.cache
5089 * @param {SignCallback} [args.onSign]
5090 * @param {string} args.gitdir
5091 * @param {string} args.message
5092 * @param {Object} args.author
5093 * @param {string} args.author.name
5094 * @param {string} args.author.email
5095 * @param {number} args.author.timestamp
5096 * @param {number} args.author.timezoneOffset
5097 * @param {Object} args.committer
5098 * @param {string} args.committer.name
5099 * @param {string} args.committer.email
5100 * @param {number} args.committer.timestamp
5101 * @param {number} args.committer.timezoneOffset
5102 * @param {string} [args.signingKey]
5103 * @param {boolean} [args.dryRun = false]
5104 * @param {boolean} [args.noUpdateBranch = false]
5105 * @param {string} [args.ref]
5106 * @param {string[]} [args.parent]
5107 * @param {string} [args.tree]
5108 *
5109 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly created commit.
5110 */
5111async function _commit({
5112 fs,
5113 cache,
5114 onSign,
5115 gitdir,
5116 message,
5117 author,
5118 committer,
5119 signingKey,
5120 dryRun = false,
5121 noUpdateBranch = false,
5122 ref,
5123 parent,
5124 tree,
5125}) {
5126 if (!ref) {
5127 ref = await GitRefManager.resolve({
5128 fs,
5129 gitdir,
5130 ref: 'HEAD',
5131 depth: 2,
5132 });
5133 }
5134
5135 return GitIndexManager.acquire(
5136 { fs, gitdir, cache, allowUnmerged: false },
5137 async function(index) {
5138 const inodes = flatFileListToDirectoryStructure(index.entries);
5139 const inode = inodes.get('.');
5140 if (!tree) {
5141 tree = await constructTree({ fs, gitdir, inode, dryRun });
5142 }
5143 if (!parent) {
5144 try {
5145 parent = [
5146 await GitRefManager.resolve({
5147 fs,
5148 gitdir,
5149 ref,
5150 }),
5151 ];
5152 } catch (err) {
5153 // Probably an initial commit
5154 parent = [];
5155 }
5156 } else {
5157 // ensure that the parents are oids, not refs
5158 parent = await Promise.all(
5159 parent.map(p => {
5160 return GitRefManager.resolve({ fs, gitdir, ref: p })
5161 })
5162 );
5163 }
5164
5165 let comm = GitCommit.from({
5166 tree,
5167 parent,
5168 author,
5169 committer,
5170 message,
5171 });
5172 if (signingKey) {
5173 comm = await GitCommit.sign(comm, onSign, signingKey);
5174 }
5175 const oid = await _writeObject({
5176 fs,
5177 gitdir,
5178 type: 'commit',
5179 object: comm.toObject(),
5180 dryRun,
5181 });
5182 if (!noUpdateBranch && !dryRun) {
5183 // Update branch pointer
5184 await GitRefManager.writeRef({
5185 fs,
5186 gitdir,
5187 ref,
5188 value: oid,
5189 });
5190 }
5191 return oid
5192 }
5193 )
5194}
5195
5196async function constructTree({ fs, gitdir, inode, dryRun }) {
5197 // use depth first traversal
5198 const children = inode.children;
5199 for (const inode of children) {
5200 if (inode.type === 'tree') {
5201 inode.metadata.mode = '040000';
5202 inode.metadata.oid = await constructTree({ fs, gitdir, inode, dryRun });
5203 }
5204 }
5205 const entries = children.map(inode => ({
5206 mode: inode.metadata.mode,
5207 path: inode.basename,
5208 oid: inode.metadata.oid,
5209 type: inode.type,
5210 }));
5211 const tree = GitTree.from(entries);
5212 const oid = await _writeObject({
5213 fs,
5214 gitdir,
5215 type: 'tree',
5216 object: tree.toObject(),
5217 dryRun,
5218 });
5219 return oid
5220}
5221
5222// @ts-check
5223
5224async function resolveFilepath({ fs, cache, gitdir, oid, filepath }) {
5225 // Ensure there are no leading or trailing directory separators.
5226 // I was going to do this automatically, but then found that the Git Terminal for Windows
5227 // auto-expands --filepath=/src/utils to --filepath=C:/Users/Will/AppData/Local/Programs/Git/src/utils
5228 // so I figured it would be wise to promote the behavior in the application layer not just the library layer.
5229 if (filepath.startsWith('/')) {
5230 throw new InvalidFilepathError('leading-slash')
5231 } else if (filepath.endsWith('/')) {
5232 throw new InvalidFilepathError('trailing-slash')
5233 }
5234 const _oid = oid;
5235 const result = await resolveTree({ fs, cache, gitdir, oid });
5236 const tree = result.tree;
5237 if (filepath === '') {
5238 oid = result.oid;
5239 } else {
5240 const pathArray = filepath.split('/');
5241 oid = await _resolveFilepath({
5242 fs,
5243 cache,
5244 gitdir,
5245 tree,
5246 pathArray,
5247 oid: _oid,
5248 filepath,
5249 });
5250 }
5251 return oid
5252}
5253
5254async function _resolveFilepath({
5255 fs,
5256 cache,
5257 gitdir,
5258 tree,
5259 pathArray,
5260 oid,
5261 filepath,
5262}) {
5263 const name = pathArray.shift();
5264 for (const entry of tree) {
5265 if (entry.path === name) {
5266 if (pathArray.length === 0) {
5267 return entry.oid
5268 } else {
5269 const { type, object } = await _readObject({
5270 fs,
5271 cache,
5272 gitdir,
5273 oid: entry.oid,
5274 });
5275 if (type !== 'tree') {
5276 throw new ObjectTypeError(oid, type, 'tree', filepath)
5277 }
5278 tree = GitTree.from(object);
5279 return _resolveFilepath({
5280 fs,
5281 cache,
5282 gitdir,
5283 tree,
5284 pathArray,
5285 oid,
5286 filepath,
5287 })
5288 }
5289 }
5290 }
5291 throw new NotFoundError(`file or directory found at "${oid}:${filepath}"`)
5292}
5293
5294// @ts-check
5295
5296/**
5297 *
5298 * @typedef {Object} ReadTreeResult - The object returned has the following schema:
5299 * @property {string} oid - SHA-1 object id of this tree
5300 * @property {TreeObject} tree - the parsed tree object
5301 */
5302
5303/**
5304 * @param {object} args
5305 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5306 * @param {any} args.cache
5307 * @param {string} args.gitdir
5308 * @param {string} args.oid
5309 * @param {string} [args.filepath]
5310 *
5311 * @returns {Promise<ReadTreeResult>}
5312 */
5313async function _readTree({
5314 fs,
5315 cache,
5316 gitdir,
5317 oid,
5318 filepath = undefined,
5319}) {
5320 if (filepath !== undefined) {
5321 oid = await resolveFilepath({ fs, cache, gitdir, oid, filepath });
5322 }
5323 const { tree, oid: treeOid } = await resolveTree({ fs, cache, gitdir, oid });
5324 const result = {
5325 oid: treeOid,
5326 tree: tree.entries(),
5327 };
5328 return result
5329}
5330
5331// @ts-check
5332
5333/**
5334 * @param {object} args
5335 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5336 * @param {string} args.gitdir
5337 * @param {TreeObject} args.tree
5338 *
5339 * @returns {Promise<string>}
5340 */
5341async function _writeTree({ fs, gitdir, tree }) {
5342 // Convert object to buffer
5343 const object = GitTree.from(tree).toObject();
5344 const oid = await _writeObject({
5345 fs,
5346 gitdir,
5347 type: 'tree',
5348 object,
5349 format: 'content',
5350 });
5351 return oid
5352}
5353
5354// @ts-check
5355
5356/**
5357 * @param {object} args
5358 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5359 * @param {object} args.cache
5360 * @param {SignCallback} [args.onSign]
5361 * @param {string} args.gitdir
5362 * @param {string} args.ref
5363 * @param {string} args.oid
5364 * @param {string|Uint8Array} args.note
5365 * @param {boolean} [args.force]
5366 * @param {Object} args.author
5367 * @param {string} args.author.name
5368 * @param {string} args.author.email
5369 * @param {number} args.author.timestamp
5370 * @param {number} args.author.timezoneOffset
5371 * @param {Object} args.committer
5372 * @param {string} args.committer.name
5373 * @param {string} args.committer.email
5374 * @param {number} args.committer.timestamp
5375 * @param {number} args.committer.timezoneOffset
5376 * @param {string} [args.signingKey]
5377 *
5378 * @returns {Promise<string>}
5379 */
5380
5381async function _addNote({
5382 fs,
5383 cache,
5384 onSign,
5385 gitdir,
5386 ref,
5387 oid,
5388 note,
5389 force,
5390 author,
5391 committer,
5392 signingKey,
5393}) {
5394 // Get the current note commit
5395 let parent;
5396 try {
5397 parent = await GitRefManager.resolve({ gitdir, fs, ref });
5398 } catch (err) {
5399 if (!(err instanceof NotFoundError)) {
5400 throw err
5401 }
5402 }
5403
5404 // I'm using the "empty tree" magic number here for brevity
5405 const result = await _readTree({
5406 fs,
5407 cache,
5408 gitdir,
5409 oid: parent || '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
5410 });
5411 let tree = result.tree;
5412
5413 // Handle the case where a note already exists
5414 if (force) {
5415 tree = tree.filter(entry => entry.path !== oid);
5416 } else {
5417 for (const entry of tree) {
5418 if (entry.path === oid) {
5419 throw new AlreadyExistsError('note', oid)
5420 }
5421 }
5422 }
5423
5424 // Create the note blob
5425 if (typeof note === 'string') {
5426 note = Buffer.from(note, 'utf8');
5427 }
5428 const noteOid = await _writeObject({
5429 fs,
5430 gitdir,
5431 type: 'blob',
5432 object: note,
5433 format: 'content',
5434 });
5435
5436 // Create the new note tree
5437 tree.push({ mode: '100644', path: oid, oid: noteOid, type: 'blob' });
5438 const treeOid = await _writeTree({
5439 fs,
5440 gitdir,
5441 tree,
5442 });
5443
5444 // Create the new note commit
5445 const commitOid = await _commit({
5446 fs,
5447 cache,
5448 onSign,
5449 gitdir,
5450 ref,
5451 tree: treeOid,
5452 parent: parent && [parent],
5453 message: `Note added by 'isomorphic-git addNote'\n`,
5454 author,
5455 committer,
5456 signingKey,
5457 });
5458
5459 return commitOid
5460}
5461
5462// @ts-check
5463
5464/**
5465 * @param {Object} args
5466 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5467 * @param {string} args.gitdir
5468 * @param {string} args.path
5469 *
5470 * @returns {Promise<any>} Resolves with the config value
5471 *
5472 * @example
5473 * // Read config value
5474 * let value = await git.getConfig({
5475 * dir: '$input((/))',
5476 * path: '$input((user.name))'
5477 * })
5478 * console.log(value)
5479 *
5480 */
5481async function _getConfig({ fs, gitdir, path }) {
5482 const config = await GitConfigManager.get({ fs, gitdir });
5483 return config.get(path)
5484}
5485
5486/**
5487 *
5488 * @returns {Promise<void | {name: string, email: string, date: Date, timestamp: number, timezoneOffset: number }>}
5489 */
5490async function normalizeAuthorObject({ fs, gitdir, author = {} }) {
5491 let { name, email, timestamp, timezoneOffset } = author;
5492 name = name || (await _getConfig({ fs, gitdir, path: 'user.name' }));
5493 email = email || (await _getConfig({ fs, gitdir, path: 'user.email' })) || '';
5494
5495 if (name === undefined) {
5496 return undefined
5497 }
5498
5499 timestamp = timestamp != null ? timestamp : Math.floor(Date.now() / 1000);
5500 timezoneOffset =
5501 timezoneOffset != null
5502 ? timezoneOffset
5503 : new Date(timestamp * 1000).getTimezoneOffset();
5504
5505 return { name, email, timestamp, timezoneOffset }
5506}
5507
5508/**
5509 *
5510 * @returns {Promise<void | {name: string, email: string, timestamp: number, timezoneOffset: number }>}
5511 */
5512async function normalizeCommitterObject({
5513 fs,
5514 gitdir,
5515 author,
5516 committer,
5517}) {
5518 committer = Object.assign({}, committer || author);
5519 // Match committer's date to author's one, if omitted
5520 if (author) {
5521 committer.timestamp = committer.timestamp || author.timestamp;
5522 committer.timezoneOffset = committer.timezoneOffset || author.timezoneOffset;
5523 }
5524 committer = await normalizeAuthorObject({ fs, gitdir, author: committer });
5525 return committer
5526}
5527
5528// @ts-check
5529
5530/**
5531 * Add or update an object note
5532 *
5533 * @param {object} args
5534 * @param {FsClient} args.fs - a file system implementation
5535 * @param {SignCallback} [args.onSign] - a PGP signing implementation
5536 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
5537 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
5538 * @param {string} [args.ref] - The notes ref to look under
5539 * @param {string} args.oid - The SHA-1 object id of the object to add the note to.
5540 * @param {string|Uint8Array} args.note - The note to add
5541 * @param {boolean} [args.force] - Over-write note if it already exists.
5542 * @param {Object} [args.author] - The details about the author.
5543 * @param {string} [args.author.name] - Default is `user.name` config.
5544 * @param {string} [args.author.email] - Default is `user.email` config.
5545 * @param {number} [args.author.timestamp=Math.floor(Date.now()/1000)] - Set the author timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
5546 * @param {number} [args.author.timezoneOffset] - Set the author timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
5547 * @param {Object} [args.committer = author] - The details about the note committer, in the same format as the author parameter. If not specified, the author details are used.
5548 * @param {string} [args.committer.name] - Default is `user.name` config.
5549 * @param {string} [args.committer.email] - Default is `user.email` config.
5550 * @param {number} [args.committer.timestamp=Math.floor(Date.now()/1000)] - Set the committer timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
5551 * @param {number} [args.committer.timezoneOffset] - Set the committer timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
5552 * @param {string} [args.signingKey] - Sign the note commit using this private PGP key.
5553 * @param {object} [args.cache] - a [cache](cache.md) object
5554 *
5555 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the commit object for the added note.
5556 */
5557
5558async function addNote({
5559 fs: _fs,
5560 onSign,
5561 dir,
5562 gitdir = join(dir, '.git'),
5563 ref = 'refs/notes/commits',
5564 oid,
5565 note,
5566 force,
5567 author: _author,
5568 committer: _committer,
5569 signingKey,
5570 cache = {},
5571}) {
5572 try {
5573 assertParameter('fs', _fs);
5574 assertParameter('gitdir', gitdir);
5575 assertParameter('oid', oid);
5576 assertParameter('note', note);
5577 if (signingKey) {
5578 assertParameter('onSign', onSign);
5579 }
5580 const fs = new FileSystem(_fs);
5581
5582 const author = await normalizeAuthorObject({ fs, gitdir, author: _author });
5583 if (!author) throw new MissingNameError('author')
5584
5585 const committer = await normalizeCommitterObject({
5586 fs,
5587 gitdir,
5588 author,
5589 committer: _committer,
5590 });
5591 if (!committer) throw new MissingNameError('committer')
5592
5593 return await _addNote({
5594 fs: new FileSystem(fs),
5595 cache,
5596 onSign,
5597 gitdir,
5598 ref,
5599 oid,
5600 note,
5601 force,
5602 author,
5603 committer,
5604 signingKey,
5605 })
5606 } catch (err) {
5607 err.caller = 'git.addNote';
5608 throw err
5609 }
5610}
5611
5612// @ts-check
5613
5614/**
5615 * @param {object} args
5616 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5617 * @param {string} args.gitdir
5618 * @param {string} args.remote
5619 * @param {string} args.url
5620 * @param {boolean} args.force
5621 *
5622 * @returns {Promise<void>}
5623 *
5624 */
5625async function _addRemote({ fs, gitdir, remote, url, force }) {
5626 if (remote !== cleanGitRef.clean(remote)) {
5627 throw new InvalidRefNameError(remote, cleanGitRef.clean(remote))
5628 }
5629 const config = await GitConfigManager.get({ fs, gitdir });
5630 if (!force) {
5631 // Check that setting it wouldn't overwrite.
5632 const remoteNames = await config.getSubsections('remote');
5633 if (remoteNames.includes(remote)) {
5634 // Throw an error if it would overwrite an existing remote,
5635 // but not if it's simply setting the same value again.
5636 if (url !== (await config.get(`remote.${remote}.url`))) {
5637 throw new AlreadyExistsError('remote', remote)
5638 }
5639 }
5640 }
5641 await config.set(`remote.${remote}.url`, url);
5642 await config.set(
5643 `remote.${remote}.fetch`,
5644 `+refs/heads/*:refs/remotes/${remote}/*`
5645 );
5646 await GitConfigManager.save({ fs, gitdir, config });
5647}
5648
5649// @ts-check
5650
5651/**
5652 * Add or update a remote
5653 *
5654 * @param {object} args
5655 * @param {FsClient} args.fs - a file system implementation
5656 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
5657 * @param {string} [args.gitdir] - [required] The [git directory](dir-vs-gitdir.md) path
5658 * @param {string} args.remote - The name of the remote
5659 * @param {string} args.url - The URL of the remote
5660 * @param {boolean} [args.force = false] - Instead of throwing an error if a remote named `remote` already exists, overwrite the existing remote.
5661 *
5662 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
5663 *
5664 * @example
5665 * await git.addRemote({
5666 * fs,
5667 * dir: '/tutorial',
5668 * remote: 'upstream',
5669 * url: 'https://github.com/isomorphic-git/isomorphic-git'
5670 * })
5671 * console.log('done')
5672 *
5673 */
5674async function addRemote({
5675 fs,
5676 dir,
5677 gitdir = join(dir, '.git'),
5678 remote,
5679 url,
5680 force = false,
5681}) {
5682 try {
5683 assertParameter('fs', fs);
5684 assertParameter('gitdir', gitdir);
5685 assertParameter('remote', remote);
5686 assertParameter('url', url);
5687 return await _addRemote({
5688 fs: new FileSystem(fs),
5689 gitdir,
5690 remote,
5691 url,
5692 force,
5693 })
5694 } catch (err) {
5695 err.caller = 'git.addRemote';
5696 throw err
5697 }
5698}
5699
5700// @ts-check
5701
5702/**
5703 * Create an annotated tag.
5704 *
5705 * @param {object} args
5706 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5707 * @param {any} args.cache
5708 * @param {SignCallback} [args.onSign]
5709 * @param {string} args.gitdir
5710 * @param {string} args.ref
5711 * @param {string} [args.message = ref]
5712 * @param {string} [args.object = 'HEAD']
5713 * @param {object} [args.tagger]
5714 * @param {string} args.tagger.name
5715 * @param {string} args.tagger.email
5716 * @param {number} args.tagger.timestamp
5717 * @param {number} args.tagger.timezoneOffset
5718 * @param {string} [args.gpgsig]
5719 * @param {string} [args.signingKey]
5720 * @param {boolean} [args.force = false]
5721 *
5722 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
5723 *
5724 * @example
5725 * await git.annotatedTag({
5726 * dir: '$input((/))',
5727 * ref: '$input((test-tag))',
5728 * message: '$input((This commit is awesome))',
5729 * tagger: {
5730 * name: '$input((Mr. Test))',
5731 * email: '$input((mrtest@example.com))'
5732 * }
5733 * })
5734 * console.log('done')
5735 *
5736 */
5737async function _annotatedTag({
5738 fs,
5739 cache,
5740 onSign,
5741 gitdir,
5742 ref,
5743 tagger,
5744 message = ref,
5745 gpgsig,
5746 object,
5747 signingKey,
5748 force = false,
5749}) {
5750 ref = ref.startsWith('refs/tags/') ? ref : `refs/tags/${ref}`;
5751
5752 if (!force && (await GitRefManager.exists({ fs, gitdir, ref }))) {
5753 throw new AlreadyExistsError('tag', ref)
5754 }
5755
5756 // Resolve passed value
5757 const oid = await GitRefManager.resolve({
5758 fs,
5759 gitdir,
5760 ref: object || 'HEAD',
5761 });
5762
5763 const { type } = await _readObject({ fs, cache, gitdir, oid });
5764 let tagObject = GitAnnotatedTag.from({
5765 object: oid,
5766 type,
5767 tag: ref.replace('refs/tags/', ''),
5768 tagger,
5769 message,
5770 gpgsig,
5771 });
5772 if (signingKey) {
5773 tagObject = await GitAnnotatedTag.sign(tagObject, onSign, signingKey);
5774 }
5775 const value = await _writeObject({
5776 fs,
5777 gitdir,
5778 type: 'tag',
5779 object: tagObject.toObject(),
5780 });
5781
5782 await GitRefManager.writeRef({ fs, gitdir, ref, value });
5783}
5784
5785// @ts-check
5786
5787/**
5788 * Create an annotated tag.
5789 *
5790 * @param {object} args
5791 * @param {FsClient} args.fs - a file system implementation
5792 * @param {SignCallback} [args.onSign] - a PGP signing implementation
5793 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
5794 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
5795 * @param {string} args.ref - What to name the tag
5796 * @param {string} [args.message = ref] - The tag message to use.
5797 * @param {string} [args.object = 'HEAD'] - The SHA-1 object id the tag points to. (Will resolve to a SHA-1 object id if value is a ref.) By default, the commit object which is referred by the current `HEAD` is used.
5798 * @param {object} [args.tagger] - The details about the tagger.
5799 * @param {string} [args.tagger.name] - Default is `user.name` config.
5800 * @param {string} [args.tagger.email] - Default is `user.email` config.
5801 * @param {number} [args.tagger.timestamp=Math.floor(Date.now()/1000)] - Set the tagger timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
5802 * @param {number} [args.tagger.timezoneOffset] - Set the tagger timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
5803 * @param {string} [args.gpgsig] - The gpgsig attatched to the tag object. (Mutually exclusive with the `signingKey` option.)
5804 * @param {string} [args.signingKey] - Sign the tag object using this private PGP key. (Mutually exclusive with the `gpgsig` option.)
5805 * @param {boolean} [args.force = false] - Instead of throwing an error if a tag named `ref` already exists, overwrite the existing tag. Note that this option does not modify the original tag object itself.
5806 * @param {object} [args.cache] - a [cache](cache.md) object
5807 *
5808 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
5809 *
5810 * @example
5811 * await git.annotatedTag({
5812 * fs,
5813 * dir: '/tutorial',
5814 * ref: 'test-tag',
5815 * message: 'This commit is awesome',
5816 * tagger: {
5817 * name: 'Mr. Test',
5818 * email: 'mrtest@example.com'
5819 * }
5820 * })
5821 * console.log('done')
5822 *
5823 */
5824async function annotatedTag({
5825 fs: _fs,
5826 onSign,
5827 dir,
5828 gitdir = join(dir, '.git'),
5829 ref,
5830 tagger: _tagger,
5831 message = ref,
5832 gpgsig,
5833 object,
5834 signingKey,
5835 force = false,
5836 cache = {},
5837}) {
5838 try {
5839 assertParameter('fs', _fs);
5840 assertParameter('gitdir', gitdir);
5841 assertParameter('ref', ref);
5842 if (signingKey) {
5843 assertParameter('onSign', onSign);
5844 }
5845 const fs = new FileSystem(_fs);
5846
5847 // Fill in missing arguments with default values
5848 const tagger = await normalizeAuthorObject({ fs, gitdir, author: _tagger });
5849 if (!tagger) throw new MissingNameError('tagger')
5850
5851 return await _annotatedTag({
5852 fs,
5853 cache,
5854 onSign,
5855 gitdir,
5856 ref,
5857 tagger,
5858 message,
5859 gpgsig,
5860 object,
5861 signingKey,
5862 force,
5863 })
5864 } catch (err) {
5865 err.caller = 'git.annotatedTag';
5866 throw err
5867 }
5868}
5869
5870// @ts-check
5871
5872/**
5873 * Create a branch
5874 *
5875 * @param {object} args
5876 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5877 * @param {string} args.gitdir
5878 * @param {string} args.ref
5879 * @param {string} [args.object = 'HEAD']
5880 * @param {boolean} [args.checkout = false]
5881 * @param {boolean} [args.force = false]
5882 *
5883 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
5884 *
5885 * @example
5886 * await git.branch({ dir: '$input((/))', ref: '$input((develop))' })
5887 * console.log('done')
5888 *
5889 */
5890async function _branch({
5891 fs,
5892 gitdir,
5893 ref,
5894 object,
5895 checkout = false,
5896 force = false,
5897}) {
5898 if (ref !== cleanGitRef.clean(ref)) {
5899 throw new InvalidRefNameError(ref, cleanGitRef.clean(ref))
5900 }
5901
5902 const fullref = `refs/heads/${ref}`;
5903
5904 if (!force) {
5905 const exist = await GitRefManager.exists({ fs, gitdir, ref: fullref });
5906 if (exist) {
5907 throw new AlreadyExistsError('branch', ref, false)
5908 }
5909 }
5910
5911 // Get current HEAD tree oid
5912 let oid;
5913 try {
5914 oid = await GitRefManager.resolve({ fs, gitdir, ref: object || 'HEAD' });
5915 } catch (e) {
5916 // Probably an empty repo
5917 }
5918
5919 // Create a new ref that points at the current commit
5920 if (oid) {
5921 await GitRefManager.writeRef({ fs, gitdir, ref: fullref, value: oid });
5922 }
5923
5924 if (checkout) {
5925 // Update HEAD
5926 await GitRefManager.writeSymbolicRef({
5927 fs,
5928 gitdir,
5929 ref: 'HEAD',
5930 value: fullref,
5931 });
5932 }
5933}
5934
5935// @ts-check
5936
5937/**
5938 * Create a branch
5939 *
5940 * @param {object} args
5941 * @param {FsClient} args.fs - a file system implementation
5942 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
5943 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
5944 * @param {string} args.ref - What to name the branch
5945 * @param {string} [args.object = 'HEAD'] - What oid to use as the start point. Accepts a symbolic ref.
5946 * @param {boolean} [args.checkout = false] - Update `HEAD` to point at the newly created branch
5947 * @param {boolean} [args.force = false] - Instead of throwing an error if a branched named `ref` already exists, overwrite the existing branch.
5948 *
5949 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
5950 *
5951 * @example
5952 * await git.branch({ fs, dir: '/tutorial', ref: 'develop' })
5953 * console.log('done')
5954 *
5955 */
5956async function branch({
5957 fs,
5958 dir,
5959 gitdir = join(dir, '.git'),
5960 ref,
5961 object,
5962 checkout = false,
5963 force = false,
5964}) {
5965 try {
5966 assertParameter('fs', fs);
5967 assertParameter('gitdir', gitdir);
5968 assertParameter('ref', ref);
5969 return await _branch({
5970 fs: new FileSystem(fs),
5971 gitdir,
5972 ref,
5973 object,
5974 checkout,
5975 force,
5976 })
5977 } catch (err) {
5978 err.caller = 'git.branch';
5979 throw err
5980 }
5981}
5982
5983const worthWalking = (filepath, root) => {
5984 if (filepath === '.' || root == null || root.length === 0 || root === '.') {
5985 return true
5986 }
5987 if (root.length >= filepath.length) {
5988 return root.startsWith(filepath)
5989 } else {
5990 return filepath.startsWith(root)
5991 }
5992};
5993
5994// @ts-check
5995
5996/**
5997 * @param {object} args
5998 * @param {import('../models/FileSystem.js').FileSystem} args.fs
5999 * @param {any} args.cache
6000 * @param {ProgressCallback} [args.onProgress]
6001 * @param {string} args.dir
6002 * @param {string} args.gitdir
6003 * @param {string} args.ref
6004 * @param {string[]} [args.filepaths]
6005 * @param {string} args.remote
6006 * @param {boolean} args.noCheckout
6007 * @param {boolean} [args.noUpdateHead]
6008 * @param {boolean} [args.dryRun]
6009 * @param {boolean} [args.force]
6010 * @param {boolean} [args.track]
6011 *
6012 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
6013 *
6014 */
6015async function _checkout({
6016 fs,
6017 cache,
6018 onProgress,
6019 dir,
6020 gitdir,
6021 remote,
6022 ref,
6023 filepaths,
6024 noCheckout,
6025 noUpdateHead,
6026 dryRun,
6027 force,
6028 track = true,
6029}) {
6030 // Get tree oid
6031 let oid;
6032 try {
6033 oid = await GitRefManager.resolve({ fs, gitdir, ref });
6034 // TODO: Figure out what to do if both 'ref' and 'remote' are specified, ref already exists,
6035 // and is configured to track a different remote.
6036 } catch (err) {
6037 if (ref === 'HEAD') throw err
6038 // If `ref` doesn't exist, create a new remote tracking branch
6039 // Figure out the commit to checkout
6040 const remoteRef = `${remote}/${ref}`;
6041 oid = await GitRefManager.resolve({
6042 fs,
6043 gitdir,
6044 ref: remoteRef,
6045 });
6046 if (track) {
6047 // Set up remote tracking branch
6048 const config = await GitConfigManager.get({ fs, gitdir });
6049 await config.set(`branch.${ref}.remote`, remote);
6050 await config.set(`branch.${ref}.merge`, `refs/heads/${ref}`);
6051 await GitConfigManager.save({ fs, gitdir, config });
6052 }
6053 // Create a new branch that points at that same commit
6054 await GitRefManager.writeRef({
6055 fs,
6056 gitdir,
6057 ref: `refs/heads/${ref}`,
6058 value: oid,
6059 });
6060 }
6061
6062 // Update working dir
6063 if (!noCheckout) {
6064 let ops;
6065 // First pass - just analyze files (not directories) and figure out what needs to be done
6066 try {
6067 ops = await analyze({
6068 fs,
6069 cache,
6070 onProgress,
6071 dir,
6072 gitdir,
6073 ref,
6074 force,
6075 filepaths,
6076 });
6077 } catch (err) {
6078 // Throw a more helpful error message for this common mistake.
6079 if (err instanceof NotFoundError && err.data.what === oid) {
6080 throw new CommitNotFetchedError(ref, oid)
6081 } else {
6082 throw err
6083 }
6084 }
6085
6086 // Report conflicts
6087 const conflicts = ops
6088 .filter(([method]) => method === 'conflict')
6089 .map(([method, fullpath]) => fullpath);
6090 if (conflicts.length > 0) {
6091 throw new CheckoutConflictError(conflicts)
6092 }
6093
6094 // Collect errors
6095 const errors = ops
6096 .filter(([method]) => method === 'error')
6097 .map(([method, fullpath]) => fullpath);
6098 if (errors.length > 0) {
6099 throw new InternalError(errors.join(', '))
6100 }
6101
6102 if (dryRun) {
6103 // Since the format of 'ops' is in flux, I really would rather folk besides myself not start relying on it
6104 // return ops
6105 return
6106 }
6107
6108 // Second pass - execute planned changes
6109 // The cheapest semi-parallel solution without computing a full dependency graph will be
6110 // to just do ops in 4 dumb phases: delete files, delete dirs, create dirs, write files
6111
6112 let count = 0;
6113 const total = ops.length;
6114 await GitIndexManager.acquire({ fs, gitdir, cache }, async function(index) {
6115 await Promise.all(
6116 ops
6117 .filter(
6118 ([method]) => method === 'delete' || method === 'delete-index'
6119 )
6120 .map(async function([method, fullpath]) {
6121 const filepath = `${dir}/${fullpath}`;
6122 if (method === 'delete') {
6123 await fs.rm(filepath);
6124 }
6125 index.delete({ filepath: fullpath });
6126 if (onProgress) {
6127 await onProgress({
6128 phase: 'Updating workdir',
6129 loaded: ++count,
6130 total,
6131 });
6132 }
6133 })
6134 );
6135 });
6136
6137 // Note: this is cannot be done naively in parallel
6138 await GitIndexManager.acquire({ fs, gitdir, cache }, async function(index) {
6139 for (const [method, fullpath] of ops) {
6140 if (method === 'rmdir' || method === 'rmdir-index') {
6141 const filepath = `${dir}/${fullpath}`;
6142 try {
6143 if (method === 'rmdir-index') {
6144 index.delete({ filepath: fullpath });
6145 }
6146 await fs.rmdir(filepath);
6147 if (onProgress) {
6148 await onProgress({
6149 phase: 'Updating workdir',
6150 loaded: ++count,
6151 total,
6152 });
6153 }
6154 } catch (e) {
6155 if (e.code === 'ENOTEMPTY') {
6156 console.log(
6157 `Did not delete ${fullpath} because directory is not empty`
6158 );
6159 } else {
6160 throw e
6161 }
6162 }
6163 }
6164 }
6165 });
6166
6167 await Promise.all(
6168 ops
6169 .filter(([method]) => method === 'mkdir' || method === 'mkdir-index')
6170 .map(async function([_, fullpath]) {
6171 const filepath = `${dir}/${fullpath}`;
6172 await fs.mkdir(filepath);
6173 if (onProgress) {
6174 await onProgress({
6175 phase: 'Updating workdir',
6176 loaded: ++count,
6177 total,
6178 });
6179 }
6180 })
6181 );
6182
6183 await GitIndexManager.acquire({ fs, gitdir, cache }, async function(index) {
6184 await Promise.all(
6185 ops
6186 .filter(
6187 ([method]) =>
6188 method === 'create' ||
6189 method === 'create-index' ||
6190 method === 'update' ||
6191 method === 'mkdir-index'
6192 )
6193 .map(async function([method, fullpath, oid, mode, chmod]) {
6194 const filepath = `${dir}/${fullpath}`;
6195 try {
6196 if (method !== 'create-index' && method !== 'mkdir-index') {
6197 const { object } = await _readObject({ fs, cache, gitdir, oid });
6198 if (chmod) {
6199 // Note: the mode option of fs.write only works when creating files,
6200 // not updating them. Since the `fs` plugin doesn't expose `chmod` this
6201 // is our only option.
6202 await fs.rm(filepath);
6203 }
6204 if (mode === 0o100644) {
6205 // regular file
6206 await fs.write(filepath, object);
6207 } else if (mode === 0o100755) {
6208 // executable file
6209 await fs.write(filepath, object, { mode: 0o777 });
6210 } else if (mode === 0o120000) {
6211 // symlink
6212 await fs.writelink(filepath, object);
6213 } else {
6214 throw new InternalError(
6215 `Invalid mode 0o${mode.toString(8)} detected in blob ${oid}`
6216 )
6217 }
6218 }
6219
6220 const stats = await fs.lstat(filepath);
6221 // We can't trust the executable bit returned by lstat on Windows,
6222 // so we need to preserve this value from the TREE.
6223 // TODO: Figure out how git handles this internally.
6224 if (mode === 0o100755) {
6225 stats.mode = 0o755;
6226 }
6227 // Submodules are present in the git index but use a unique mode different from trees
6228 if (method === 'mkdir-index') {
6229 stats.mode = 0o160000;
6230 }
6231 index.insert({
6232 filepath: fullpath,
6233 stats,
6234 oid,
6235 });
6236 if (onProgress) {
6237 await onProgress({
6238 phase: 'Updating workdir',
6239 loaded: ++count,
6240 total,
6241 });
6242 }
6243 } catch (e) {
6244 console.log(e);
6245 }
6246 })
6247 );
6248 });
6249 }
6250
6251 // Update HEAD
6252 if (!noUpdateHead) {
6253 const fullRef = await GitRefManager.expand({ fs, gitdir, ref });
6254 if (fullRef.startsWith('refs/heads')) {
6255 await GitRefManager.writeSymbolicRef({
6256 fs,
6257 gitdir,
6258 ref: 'HEAD',
6259 value: fullRef,
6260 });
6261 } else {
6262 // detached head
6263 await GitRefManager.writeRef({ fs, gitdir, ref: 'HEAD', value: oid });
6264 }
6265 }
6266}
6267
6268async function analyze({
6269 fs,
6270 cache,
6271 onProgress,
6272 dir,
6273 gitdir,
6274 ref,
6275 force,
6276 filepaths,
6277}) {
6278 let count = 0;
6279 return _walk({
6280 fs,
6281 cache,
6282 dir,
6283 gitdir,
6284 trees: [TREE({ ref }), WORKDIR(), STAGE()],
6285 map: async function(fullpath, [commit, workdir, stage]) {
6286 if (fullpath === '.') return
6287 // match against base paths
6288 if (filepaths && !filepaths.some(base => worthWalking(fullpath, base))) {
6289 return null
6290 }
6291 // Emit progress event
6292 if (onProgress) {
6293 await onProgress({ phase: 'Analyzing workdir', loaded: ++count });
6294 }
6295
6296 // This is a kind of silly pattern but it worked so well for me in the past
6297 // and it makes intuitively demonstrating exhaustiveness so *easy*.
6298 // This checks for the presense and/or absence of each of the 3 entries,
6299 // converts that to a 3-bit binary representation, and then handles
6300 // every possible combination (2^3 or 8 cases) with a lookup table.
6301 const key = [!!stage, !!commit, !!workdir].map(Number).join('');
6302 switch (key) {
6303 // Impossible case.
6304 case '000':
6305 return
6306 // Ignore workdir files that are not tracked and not part of the new commit.
6307 case '001':
6308 // OK, make an exception for explicitly named files.
6309 if (force && filepaths && filepaths.includes(fullpath)) {
6310 return ['delete', fullpath]
6311 }
6312 return
6313 // New entries
6314 case '010': {
6315 switch (await commit.type()) {
6316 case 'tree': {
6317 return ['mkdir', fullpath]
6318 }
6319 case 'blob': {
6320 return [
6321 'create',
6322 fullpath,
6323 await commit.oid(),
6324 await commit.mode(),
6325 ]
6326 }
6327 case 'commit': {
6328 return [
6329 'mkdir-index',
6330 fullpath,
6331 await commit.oid(),
6332 await commit.mode(),
6333 ]
6334 }
6335 default: {
6336 return [
6337 'error',
6338 `new entry Unhandled type ${await commit.type()}`,
6339 ]
6340 }
6341 }
6342 }
6343 // New entries but there is already something in the workdir there.
6344 case '011': {
6345 switch (`${await commit.type()}-${await workdir.type()}`) {
6346 case 'tree-tree': {
6347 return // noop
6348 }
6349 case 'tree-blob':
6350 case 'blob-tree': {
6351 return ['conflict', fullpath]
6352 }
6353 case 'blob-blob': {
6354 // Is the incoming file different?
6355 if ((await commit.oid()) !== (await workdir.oid())) {
6356 if (force) {
6357 return [
6358 'update',
6359 fullpath,
6360 await commit.oid(),
6361 await commit.mode(),
6362 (await commit.mode()) !== (await workdir.mode()),
6363 ]
6364 } else {
6365 return ['conflict', fullpath]
6366 }
6367 } else {
6368 // Is the incoming file a different mode?
6369 if ((await commit.mode()) !== (await workdir.mode())) {
6370 if (force) {
6371 return [
6372 'update',
6373 fullpath,
6374 await commit.oid(),
6375 await commit.mode(),
6376 true,
6377 ]
6378 } else {
6379 return ['conflict', fullpath]
6380 }
6381 } else {
6382 return [
6383 'create-index',
6384 fullpath,
6385 await commit.oid(),
6386 await commit.mode(),
6387 ]
6388 }
6389 }
6390 }
6391 case 'commit-tree': {
6392 // TODO: submodule
6393 // We'll ignore submodule directories for now.
6394 // Users prefer we not throw an error for lack of submodule support.
6395 // gitlinks
6396 return
6397 }
6398 case 'commit-blob': {
6399 // TODO: submodule
6400 // But... we'll complain if there is a *file* where we would
6401 // put a submodule if we had submodule support.
6402 return ['conflict', fullpath]
6403 }
6404 default: {
6405 return ['error', `new entry Unhandled type ${commit.type}`]
6406 }
6407 }
6408 }
6409 // Something in stage but not in the commit OR the workdir.
6410 // Note: I verified this behavior against canonical git.
6411 case '100': {
6412 return ['delete-index', fullpath]
6413 }
6414 // Deleted entries
6415 // TODO: How to handle if stage type and workdir type mismatch?
6416 case '101': {
6417 switch (await stage.type()) {
6418 case 'tree': {
6419 return ['rmdir', fullpath]
6420 }
6421 case 'blob': {
6422 // Git checks that the workdir.oid === stage.oid before deleting file
6423 if ((await stage.oid()) !== (await workdir.oid())) {
6424 if (force) {
6425 return ['delete', fullpath]
6426 } else {
6427 return ['conflict', fullpath]
6428 }
6429 } else {
6430 return ['delete', fullpath]
6431 }
6432 }
6433 case 'commit': {
6434 return ['rmdir-index', fullpath]
6435 }
6436 default: {
6437 return [
6438 'error',
6439 `delete entry Unhandled type ${await stage.type()}`,
6440 ]
6441 }
6442 }
6443 }
6444 /* eslint-disable no-fallthrough */
6445 // File missing from workdir
6446 case '110':
6447 // Possibly modified entries
6448 case '111': {
6449 /* eslint-enable no-fallthrough */
6450 switch (`${await stage.type()}-${await commit.type()}`) {
6451 case 'tree-tree': {
6452 return
6453 }
6454 case 'blob-blob': {
6455 // If the file hasn't changed, there is no need to do anything.
6456 // Existing file modifications in the workdir can be be left as is.
6457 if (
6458 (await stage.oid()) === (await commit.oid()) &&
6459 (await stage.mode()) === (await commit.mode()) &&
6460 !force
6461 ) {
6462 return
6463 }
6464
6465 // Check for local changes that would be lost
6466 if (workdir) {
6467 // Note: canonical git only compares with the stage. But we're smart enough
6468 // to compare to the stage AND the incoming commit.
6469 if (
6470 (await workdir.oid()) !== (await stage.oid()) &&
6471 (await workdir.oid()) !== (await commit.oid())
6472 ) {
6473 if (force) {
6474 return [
6475 'update',
6476 fullpath,
6477 await commit.oid(),
6478 await commit.mode(),
6479 (await commit.mode()) !== (await workdir.mode()),
6480 ]
6481 } else {
6482 return ['conflict', fullpath]
6483 }
6484 }
6485 } else if (force) {
6486 return [
6487 'update',
6488 fullpath,
6489 await commit.oid(),
6490 await commit.mode(),
6491 (await commit.mode()) !== (await stage.mode()),
6492 ]
6493 }
6494 // Has file mode changed?
6495 if ((await commit.mode()) !== (await stage.mode())) {
6496 return [
6497 'update',
6498 fullpath,
6499 await commit.oid(),
6500 await commit.mode(),
6501 true,
6502 ]
6503 }
6504 // TODO: HANDLE SYMLINKS
6505 // Has the file content changed?
6506 if ((await commit.oid()) !== (await stage.oid())) {
6507 return [
6508 'update',
6509 fullpath,
6510 await commit.oid(),
6511 await commit.mode(),
6512 false,
6513 ]
6514 } else {
6515 return
6516 }
6517 }
6518 case 'tree-blob': {
6519 return ['update-dir-to-blob', fullpath, await commit.oid()]
6520 }
6521 case 'blob-tree': {
6522 return ['update-blob-to-tree', fullpath]
6523 }
6524 case 'commit-commit': {
6525 return [
6526 'mkdir-index',
6527 fullpath,
6528 await commit.oid(),
6529 await commit.mode(),
6530 ]
6531 }
6532 default: {
6533 return [
6534 'error',
6535 `update entry Unhandled type ${await stage.type()}-${await commit.type()}`,
6536 ]
6537 }
6538 }
6539 }
6540 }
6541 },
6542 // Modify the default flat mapping
6543 reduce: async function(parent, children) {
6544 children = flat(children);
6545 if (!parent) {
6546 return children
6547 } else if (parent && parent[0] === 'rmdir') {
6548 children.push(parent);
6549 return children
6550 } else {
6551 children.unshift(parent);
6552 return children
6553 }
6554 },
6555 })
6556}
6557
6558// @ts-check
6559
6560/**
6561 * Checkout a branch
6562 *
6563 * If the branch already exists it will check out that branch. Otherwise, it will create a new remote tracking branch set to track the remote branch of that name.
6564 *
6565 * @param {object} args
6566 * @param {FsClient} args.fs - a file system implementation
6567 * @param {ProgressCallback} [args.onProgress] - optional progress event callback
6568 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
6569 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
6570 * @param {string} [args.ref = 'HEAD'] - Source to checkout files from
6571 * @param {string[]} [args.filepaths] - Limit the checkout to the given files and directories
6572 * @param {string} [args.remote = 'origin'] - Which remote repository to use
6573 * @param {boolean} [args.noCheckout = false] - If true, will update HEAD but won't update the working directory
6574 * @param {boolean} [args.noUpdateHead] - If true, will update the working directory but won't update HEAD. Defaults to `false` when `ref` is provided, and `true` if `ref` is not provided.
6575 * @param {boolean} [args.dryRun = false] - If true, simulates a checkout so you can test whether it would succeed.
6576 * @param {boolean} [args.force = false] - If true, conflicts will be ignored and files will be overwritten regardless of local changes.
6577 * @param {boolean} [args.track = true] - If false, will not set the remote branch tracking information. Defaults to true.
6578 * @param {object} [args.cache] - a [cache](cache.md) object
6579 *
6580 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
6581 *
6582 * @example
6583 * // switch to the main branch
6584 * await git.checkout({
6585 * fs,
6586 * dir: '/tutorial',
6587 * ref: 'main'
6588 * })
6589 * console.log('done')
6590 *
6591 * @example
6592 * // restore the 'docs' and 'src/docs' folders to the way they were, overwriting any changes
6593 * await git.checkout({
6594 * fs,
6595 * dir: '/tutorial',
6596 * force: true,
6597 * filepaths: ['docs', 'src/docs']
6598 * })
6599 * console.log('done')
6600 *
6601 * @example
6602 * // restore the 'docs' and 'src/docs' folders to the way they are in the 'develop' branch, overwriting any changes
6603 * await git.checkout({
6604 * fs,
6605 * dir: '/tutorial',
6606 * ref: 'develop',
6607 * noUpdateHead: true,
6608 * force: true,
6609 * filepaths: ['docs', 'src/docs']
6610 * })
6611 * console.log('done')
6612 */
6613async function checkout({
6614 fs,
6615 onProgress,
6616 dir,
6617 gitdir = join(dir, '.git'),
6618 remote = 'origin',
6619 ref: _ref,
6620 filepaths,
6621 noCheckout = false,
6622 noUpdateHead = _ref === undefined,
6623 dryRun = false,
6624 force = false,
6625 track = true,
6626 cache = {},
6627}) {
6628 try {
6629 assertParameter('fs', fs);
6630 assertParameter('dir', dir);
6631 assertParameter('gitdir', gitdir);
6632
6633 const ref = _ref || 'HEAD';
6634 return await _checkout({
6635 fs: new FileSystem(fs),
6636 cache,
6637 onProgress,
6638 dir,
6639 gitdir,
6640 remote,
6641 ref,
6642 filepaths,
6643 noCheckout,
6644 noUpdateHead,
6645 dryRun,
6646 force,
6647 track,
6648 })
6649 } catch (err) {
6650 err.caller = 'git.checkout';
6651 throw err
6652 }
6653}
6654
6655// @see https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions
6656const abbreviateRx = new RegExp('^refs/(heads/|tags/|remotes/)?(.*)');
6657
6658function abbreviateRef(ref) {
6659 const match = abbreviateRx.exec(ref);
6660 if (match) {
6661 if (match[1] === 'remotes/' && ref.endsWith('/HEAD')) {
6662 return match[2].slice(0, -5)
6663 } else {
6664 return match[2]
6665 }
6666 }
6667 return ref
6668}
6669
6670// @ts-check
6671
6672/**
6673 * @param {Object} args
6674 * @param {import('../models/FileSystem.js').FileSystem} args.fs
6675 * @param {string} args.gitdir
6676 * @param {boolean} [args.fullname = false] - Return the full path (e.g. "refs/heads/main") instead of the abbreviated form.
6677 * @param {boolean} [args.test = false] - If the current branch doesn't actually exist (such as right after git init) then return `undefined`.
6678 *
6679 * @returns {Promise<string|void>} The name of the current branch or undefined if the HEAD is detached.
6680 *
6681 */
6682async function _currentBranch({
6683 fs,
6684 gitdir,
6685 fullname = false,
6686 test = false,
6687}) {
6688 const ref = await GitRefManager.resolve({
6689 fs,
6690 gitdir,
6691 ref: 'HEAD',
6692 depth: 2,
6693 });
6694 if (test) {
6695 try {
6696 await GitRefManager.resolve({ fs, gitdir, ref });
6697 } catch (_) {
6698 return
6699 }
6700 }
6701 // Return `undefined` for detached HEAD
6702 if (!ref.startsWith('refs/')) return
6703 return fullname ? ref : abbreviateRef(ref)
6704}
6705
6706function translateSSHtoHTTP(url) {
6707 // handle "shorter scp-like syntax"
6708 url = url.replace(/^git@([^:]+):/, 'https://$1/');
6709 // handle proper SSH URLs
6710 url = url.replace(/^ssh:\/\//, 'https://');
6711 return url
6712}
6713
6714function calculateBasicAuthHeader({ username = '', password = '' }) {
6715 return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
6716}
6717
6718// Currently 'for await' upsets my linters.
6719async function forAwait(iterable, cb) {
6720 const iter = getIterator(iterable);
6721 while (true) {
6722 const { value, done } = await iter.next();
6723 if (value) await cb(value);
6724 if (done) break
6725 }
6726 if (iter.return) iter.return();
6727}
6728
6729async function collect(iterable) {
6730 let size = 0;
6731 const buffers = [];
6732 // This will be easier once `for await ... of` loops are available.
6733 await forAwait(iterable, value => {
6734 buffers.push(value);
6735 size += value.byteLength;
6736 });
6737 const result = new Uint8Array(size);
6738 let nextIndex = 0;
6739 for (const buffer of buffers) {
6740 result.set(buffer, nextIndex);
6741 nextIndex += buffer.byteLength;
6742 }
6743 return result
6744}
6745
6746function extractAuthFromUrl(url) {
6747 // For whatever reason, the `fetch` API does not convert credentials embedded in the URL
6748 // into Basic Authentication headers automatically. Instead it throws an error!
6749 // So we must manually parse the URL, rip out the user:password portion if it is present
6750 // and compute the Authorization header.
6751 // Note: I tried using new URL(url) but that throws a security exception in Edge. :rolleyes:
6752 let userpass = url.match(/^https?:\/\/([^/]+)@/);
6753 // No credentials, return the url unmodified and an empty auth object
6754 if (userpass == null) return { url, auth: {} }
6755 userpass = userpass[1];
6756 const [username, password] = userpass.split(':');
6757 // Remove credentials from URL
6758 url = url.replace(`${userpass}@`, '');
6759 // Has credentials, return the fetch-safe URL and the parsed credentials
6760 return { url, auth: { username, password } }
6761}
6762
6763function padHex(b, n) {
6764 const s = n.toString(16);
6765 return '0'.repeat(b - s.length) + s
6766}
6767
6768/**
6769pkt-line Format
6770---------------
6771
6772Much (but not all) of the payload is described around pkt-lines.
6773
6774A pkt-line is a variable length binary string. The first four bytes
6775of the line, the pkt-len, indicates the total length of the line,
6776in hexadecimal. The pkt-len includes the 4 bytes used to contain
6777the length's hexadecimal representation.
6778
6779A pkt-line MAY contain binary data, so implementors MUST ensure
6780pkt-line parsing/formatting routines are 8-bit clean.
6781
6782A non-binary line SHOULD BE terminated by an LF, which if present
6783MUST be included in the total length. Receivers MUST treat pkt-lines
6784with non-binary data the same whether or not they contain the trailing
6785LF (stripping the LF if present, and not complaining when it is
6786missing).
6787
6788The maximum length of a pkt-line's data component is 65516 bytes.
6789Implementations MUST NOT send pkt-line whose length exceeds 65520
6790(65516 bytes of payload + 4 bytes of length data).
6791
6792Implementations SHOULD NOT send an empty pkt-line ("0004").
6793
6794A pkt-line with a length field of 0 ("0000"), called a flush-pkt,
6795is a special case and MUST be handled differently than an empty
6796pkt-line ("0004").
6797
6798----
6799 pkt-line = data-pkt / flush-pkt
6800
6801 data-pkt = pkt-len pkt-payload
6802 pkt-len = 4*(HEXDIG)
6803 pkt-payload = (pkt-len - 4)*(OCTET)
6804
6805 flush-pkt = "0000"
6806----
6807
6808Examples (as C-style strings):
6809
6810----
6811 pkt-line actual value
6812 ---------------------------------
6813 "0006a\n" "a\n"
6814 "0005a" "a"
6815 "000bfoobar\n" "foobar\n"
6816 "0004" ""
6817----
6818*/
6819
6820// I'm really using this more as a namespace.
6821// There's not a lot of "state" in a pkt-line
6822
6823class GitPktLine {
6824 static flush() {
6825 return Buffer.from('0000', 'utf8')
6826 }
6827
6828 static delim() {
6829 return Buffer.from('0001', 'utf8')
6830 }
6831
6832 static encode(line) {
6833 if (typeof line === 'string') {
6834 line = Buffer.from(line);
6835 }
6836 const length = line.length + 4;
6837 const hexlength = padHex(4, length);
6838 return Buffer.concat([Buffer.from(hexlength, 'utf8'), line])
6839 }
6840
6841 static streamReader(stream) {
6842 const reader = new StreamReader(stream);
6843 return async function read() {
6844 try {
6845 let length = await reader.read(4);
6846 if (length == null) return true
6847 length = parseInt(length.toString('utf8'), 16);
6848 if (length === 0) return null
6849 if (length === 1) return null // delim packets
6850 const buffer = await reader.read(length - 4);
6851 if (buffer == null) return true
6852 return buffer
6853 } catch (err) {
6854 stream.error = err;
6855 return true
6856 }
6857 }
6858 }
6859}
6860
6861// @ts-check
6862
6863/**
6864 * @param {function} read
6865 */
6866async function parseCapabilitiesV2(read) {
6867 /** @type {Object<string, string | true>} */
6868 const capabilities2 = {};
6869
6870 let line;
6871 while (true) {
6872 line = await read();
6873 if (line === true) break
6874 if (line === null) continue
6875 line = line.toString('utf8').replace(/\n$/, '');
6876 const i = line.indexOf('=');
6877 if (i > -1) {
6878 const key = line.slice(0, i);
6879 const value = line.slice(i + 1);
6880 capabilities2[key] = value;
6881 } else {
6882 capabilities2[line] = true;
6883 }
6884 }
6885 return { protocolVersion: 2, capabilities2 }
6886}
6887
6888async function parseRefsAdResponse(stream, { service }) {
6889 const capabilities = new Set();
6890 const refs = new Map();
6891 const symrefs = new Map();
6892
6893 // There is probably a better way to do this, but for now
6894 // let's just throw the result parser inline here.
6895 const read = GitPktLine.streamReader(stream);
6896 let lineOne = await read();
6897 // skip past any flushes
6898 while (lineOne === null) lineOne = await read();
6899
6900 if (lineOne === true) throw new EmptyServerResponseError()
6901
6902 // Handle protocol v2 responses (Bitbucket Server doesn't include a `# service=` line)
6903 if (lineOne.includes('version 2')) {
6904 return parseCapabilitiesV2(read)
6905 }
6906
6907 // Clients MUST ignore an LF at the end of the line.
6908 if (lineOne.toString('utf8').replace(/\n$/, '') !== `# service=${service}`) {
6909 throw new ParseError(`# service=${service}\\n`, lineOne.toString('utf8'))
6910 }
6911 let lineTwo = await read();
6912 // skip past any flushes
6913 while (lineTwo === null) lineTwo = await read();
6914 // In the edge case of a brand new repo, zero refs (and zero capabilities)
6915 // are returned.
6916 if (lineTwo === true) return { capabilities, refs, symrefs }
6917 lineTwo = lineTwo.toString('utf8');
6918
6919 // Handle protocol v2 responses
6920 if (lineTwo.includes('version 2')) {
6921 return parseCapabilitiesV2(read)
6922 }
6923
6924 const [firstRef, capabilitiesLine] = splitAndAssert(lineTwo, '\x00', '\\x00');
6925 capabilitiesLine.split(' ').map(x => capabilities.add(x));
6926 const [ref, name] = splitAndAssert(firstRef, ' ', ' ');
6927 refs.set(name, ref);
6928 while (true) {
6929 const line = await read();
6930 if (line === true) break
6931 if (line !== null) {
6932 const [ref, name] = splitAndAssert(line.toString('utf8'), ' ', ' ');
6933 refs.set(name, ref);
6934 }
6935 }
6936 // Symrefs are thrown into the "capabilities" unfortunately.
6937 for (const cap of capabilities) {
6938 if (cap.startsWith('symref=')) {
6939 const m = cap.match(/symref=([^:]+):(.*)/);
6940 if (m.length === 3) {
6941 symrefs.set(m[1], m[2]);
6942 }
6943 }
6944 }
6945 return { protocolVersion: 1, capabilities, refs, symrefs }
6946}
6947
6948function splitAndAssert(line, sep, expected) {
6949 const split = line.trim().split(sep);
6950 if (split.length !== 2) {
6951 throw new ParseError(
6952 `Two strings separated by '${expected}'`,
6953 line.toString('utf8')
6954 )
6955 }
6956 return split
6957}
6958
6959// Try to accomodate known CORS proxy implementations:
6960// - https://jcubic.pl/proxy.php? <-- uses query string
6961// - https://cors.isomorphic-git.org <-- uses path
6962const corsProxify = (corsProxy, url) =>
6963 corsProxy.endsWith('?')
6964 ? `${corsProxy}${url}`
6965 : `${corsProxy}/${url.replace(/^https?:\/\//, '')}`;
6966
6967const updateHeaders = (headers, auth) => {
6968 // Update the basic auth header
6969 if (auth.username || auth.password) {
6970 headers.Authorization = calculateBasicAuthHeader(auth);
6971 }
6972 // but any manually provided headers take precedence
6973 if (auth.headers) {
6974 Object.assign(headers, auth.headers);
6975 }
6976};
6977
6978/**
6979 * @param {GitHttpResponse} res
6980 *
6981 * @returns {{ preview: string, response: string, data: Buffer }}
6982 */
6983const stringifyBody = async res => {
6984 try {
6985 // Some services provide a meaningful error message in the body of 403s like "token lacks the scopes necessary to perform this action"
6986 const data = Buffer.from(await collect(res.body));
6987 const response = data.toString('utf8');
6988 const preview =
6989 response.length < 256 ? response : response.slice(0, 256) + '...';
6990 return { preview, response, data }
6991 } catch (e) {
6992 return {}
6993 }
6994};
6995
6996class GitRemoteHTTP {
6997 static async capabilities() {
6998 return ['discover', 'connect']
6999 }
7000
7001 /**
7002 * @param {Object} args
7003 * @param {HttpClient} args.http
7004 * @param {ProgressCallback} [args.onProgress]
7005 * @param {AuthCallback} [args.onAuth]
7006 * @param {AuthFailureCallback} [args.onAuthFailure]
7007 * @param {AuthSuccessCallback} [args.onAuthSuccess]
7008 * @param {string} [args.corsProxy]
7009 * @param {string} args.service
7010 * @param {string} args.url
7011 * @param {Object<string, string>} args.headers
7012 * @param {1 | 2} args.protocolVersion - Git Protocol Version
7013 */
7014 static async discover({
7015 http,
7016 onProgress,
7017 onAuth,
7018 onAuthSuccess,
7019 onAuthFailure,
7020 corsProxy,
7021 service,
7022 url: _origUrl,
7023 headers,
7024 protocolVersion,
7025 }) {
7026 let { url, auth } = extractAuthFromUrl(_origUrl);
7027 const proxifiedURL = corsProxy ? corsProxify(corsProxy, url) : url;
7028 if (auth.username || auth.password) {
7029 headers.Authorization = calculateBasicAuthHeader(auth);
7030 }
7031 if (protocolVersion === 2) {
7032 headers['Git-Protocol'] = 'version=2';
7033 }
7034
7035 let res;
7036 let tryAgain;
7037 let providedAuthBefore = false;
7038 do {
7039 res = await http.request({
7040 onProgress,
7041 method: 'GET',
7042 url: `${proxifiedURL}/info/refs?service=${service}`,
7043 headers,
7044 });
7045
7046 // the default loop behavior
7047 tryAgain = false;
7048
7049 // 401 is the "correct" response for access denied. 203 is Non-Authoritative Information and comes from Azure DevOps, which
7050 // apparently doesn't realize this is a git request and is returning the HTML for the "Azure DevOps Services | Sign In" page.
7051 if (res.statusCode === 401 || res.statusCode === 203) {
7052 // On subsequent 401s, call `onAuthFailure` instead of `onAuth`.
7053 // This is so that naive `onAuth` callbacks that return a fixed value don't create an infinite loop of retrying.
7054 const getAuth = providedAuthBefore ? onAuthFailure : onAuth;
7055 if (getAuth) {
7056 // Acquire credentials and try again
7057 // TODO: read `useHttpPath` value from git config and pass along?
7058 auth = await getAuth(url, {
7059 ...auth,
7060 headers: { ...headers },
7061 });
7062 if (auth && auth.cancel) {
7063 throw new UserCanceledError()
7064 } else if (auth) {
7065 updateHeaders(headers, auth);
7066 providedAuthBefore = true;
7067 tryAgain = true;
7068 }
7069 }
7070 } else if (
7071 res.statusCode === 200 &&
7072 providedAuthBefore &&
7073 onAuthSuccess
7074 ) {
7075 await onAuthSuccess(url, auth);
7076 }
7077 } while (tryAgain)
7078
7079 if (res.statusCode !== 200) {
7080 const { response } = await stringifyBody(res);
7081 throw new HttpError(res.statusCode, res.statusMessage, response)
7082 }
7083 // Git "smart" HTTP servers should respond with the correct Content-Type header.
7084 if (
7085 res.headers['content-type'] === `application/x-${service}-advertisement`
7086 ) {
7087 const remoteHTTP = await parseRefsAdResponse(res.body, { service });
7088 remoteHTTP.auth = auth;
7089 return remoteHTTP
7090 } else {
7091 // If they don't send the correct content-type header, that's a good indicator it is either a "dumb" HTTP
7092 // server, or the user specified an incorrect remote URL and the response is actually an HTML page.
7093 // In this case, we save the response as plain text so we can generate a better error message if needed.
7094 const { preview, response, data } = await stringifyBody(res);
7095 // For backwards compatibility, try to parse it anyway.
7096 // TODO: maybe just throw instead of trying?
7097 try {
7098 const remoteHTTP = await parseRefsAdResponse([data], { service });
7099 remoteHTTP.auth = auth;
7100 return remoteHTTP
7101 } catch (e) {
7102 throw new SmartHttpError(preview, response)
7103 }
7104 }
7105 }
7106
7107 /**
7108 * @param {Object} args
7109 * @param {HttpClient} args.http
7110 * @param {ProgressCallback} [args.onProgress]
7111 * @param {string} [args.corsProxy]
7112 * @param {string} args.service
7113 * @param {string} args.url
7114 * @param {Object<string, string>} [args.headers]
7115 * @param {any} args.body
7116 * @param {any} args.auth
7117 */
7118 static async connect({
7119 http,
7120 onProgress,
7121 corsProxy,
7122 service,
7123 url,
7124 auth,
7125 body,
7126 headers,
7127 }) {
7128 // We already have the "correct" auth value at this point, but
7129 // we need to strip out the username/password from the URL yet again.
7130 const urlAuth = extractAuthFromUrl(url);
7131 if (urlAuth) url = urlAuth.url;
7132
7133 if (corsProxy) url = corsProxify(corsProxy, url);
7134
7135 headers['content-type'] = `application/x-${service}-request`;
7136 headers.accept = `application/x-${service}-result`;
7137 updateHeaders(headers, auth);
7138
7139 const res = await http.request({
7140 onProgress,
7141 method: 'POST',
7142 url: `${url}/${service}`,
7143 body,
7144 headers,
7145 });
7146 if (res.statusCode !== 200) {
7147 const { response } = stringifyBody(res);
7148 throw new HttpError(res.statusCode, res.statusMessage, response)
7149 }
7150 return res
7151 }
7152}
7153
7154function parseRemoteUrl({ url }) {
7155 // the stupid "shorter scp-like syntax"
7156 if (url.startsWith('git@')) {
7157 return {
7158 transport: 'ssh',
7159 address: url,
7160 }
7161 }
7162 const matches = url.match(/(\w+)(:\/\/|::)(.*)/);
7163 if (matches === null) return
7164 /*
7165 * When git encounters a URL of the form <transport>://<address>, where <transport> is
7166 * a protocol that it cannot handle natively, it automatically invokes git remote-<transport>
7167 * with the full URL as the second argument.
7168 *
7169 * @see https://git-scm.com/docs/git-remote-helpers
7170 */
7171 if (matches[2] === '://') {
7172 return {
7173 transport: matches[1],
7174 address: matches[0],
7175 }
7176 }
7177 /*
7178 * A URL of the form <transport>::<address> explicitly instructs git to invoke
7179 * git remote-<transport> with <address> as the second argument.
7180 *
7181 * @see https://git-scm.com/docs/git-remote-helpers
7182 */
7183 if (matches[2] === '::') {
7184 return {
7185 transport: matches[1],
7186 address: matches[3],
7187 }
7188 }
7189}
7190
7191class GitRemoteManager {
7192 static getRemoteHelperFor({ url }) {
7193 // TODO: clean up the remoteHelper API and move into PluginCore
7194 const remoteHelpers = new Map();
7195 remoteHelpers.set('http', GitRemoteHTTP);
7196 remoteHelpers.set('https', GitRemoteHTTP);
7197
7198 const parts = parseRemoteUrl({ url });
7199 if (!parts) {
7200 throw new UrlParseError(url)
7201 }
7202 if (remoteHelpers.has(parts.transport)) {
7203 return remoteHelpers.get(parts.transport)
7204 }
7205 throw new UnknownTransportError(
7206 url,
7207 parts.transport,
7208 parts.transport === 'ssh' ? translateSSHtoHTTP(url) : undefined
7209 )
7210 }
7211}
7212
7213let lock$1 = null;
7214
7215class GitShallowManager {
7216 static async read({ fs, gitdir }) {
7217 if (lock$1 === null) lock$1 = new AsyncLock();
7218 const filepath = join(gitdir, 'shallow');
7219 const oids = new Set();
7220 await lock$1.acquire(filepath, async function() {
7221 const text = await fs.read(filepath, { encoding: 'utf8' });
7222 if (text === null) return oids // no file
7223 if (text.trim() === '') return oids // empty file
7224 text
7225 .trim()
7226 .split('\n')
7227 .map(oid => oids.add(oid));
7228 });
7229 return oids
7230 }
7231
7232 static async write({ fs, gitdir, oids }) {
7233 if (lock$1 === null) lock$1 = new AsyncLock();
7234 const filepath = join(gitdir, 'shallow');
7235 if (oids.size > 0) {
7236 const text = [...oids].join('\n') + '\n';
7237 await lock$1.acquire(filepath, async function() {
7238 await fs.write(filepath, text, {
7239 encoding: 'utf8',
7240 });
7241 });
7242 } else {
7243 // No shallows
7244 await lock$1.acquire(filepath, async function() {
7245 await fs.rm(filepath);
7246 });
7247 }
7248 }
7249}
7250
7251async function hasObjectLoose({ fs, gitdir, oid }) {
7252 const source = `objects/${oid.slice(0, 2)}/${oid.slice(2)}`;
7253 return fs.exists(`${gitdir}/${source}`)
7254}
7255
7256async function hasObjectPacked({
7257 fs,
7258 cache,
7259 gitdir,
7260 oid,
7261 getExternalRefDelta,
7262}) {
7263 // Check to see if it's in a packfile.
7264 // Iterate through all the .idx files
7265 let list = await fs.readdir(join(gitdir, 'objects/pack'));
7266 list = list.filter(x => x.endsWith('.idx'));
7267 for (const filename of list) {
7268 const indexFile = `${gitdir}/objects/pack/${filename}`;
7269 const p = await readPackIndex({
7270 fs,
7271 cache,
7272 filename: indexFile,
7273 getExternalRefDelta,
7274 });
7275 if (p.error) throw new InternalError(p.error)
7276 // If the packfile DOES have the oid we're looking for...
7277 if (p.offsets.has(oid)) {
7278 return true
7279 }
7280 }
7281 // Failed to find it
7282 return false
7283}
7284
7285async function hasObject({
7286 fs,
7287 cache,
7288 gitdir,
7289 oid,
7290 format = 'content',
7291}) {
7292 // Curry the current read method so that the packfile un-deltification
7293 // process can acquire external ref-deltas.
7294 const getExternalRefDelta = oid => _readObject({ fs, cache, gitdir, oid });
7295
7296 // Look for it in the loose object directory.
7297 let result = await hasObjectLoose({ fs, gitdir, oid });
7298 // Check to see if it's in a packfile.
7299 if (!result) {
7300 result = await hasObjectPacked({
7301 fs,
7302 cache,
7303 gitdir,
7304 oid,
7305 getExternalRefDelta,
7306 });
7307 }
7308 // Finally
7309 return result
7310}
7311
7312// TODO: make a function that just returns obCount. then emptyPackfile = () => sizePack(pack) === 0
7313function emptyPackfile(pack) {
7314 const pheader = '5041434b';
7315 const version = '00000002';
7316 const obCount = '00000000';
7317 const header = pheader + version + obCount;
7318 return pack.slice(0, 12).toString('hex') === header
7319}
7320
7321function filterCapabilities(server, client) {
7322 const serverNames = server.map(cap => cap.split('=', 1)[0]);
7323 return client.filter(cap => {
7324 const name = cap.split('=', 1)[0];
7325 return serverNames.includes(name)
7326 })
7327}
7328
7329const pkg = {
7330 name: 'isomorphic-git',
7331 version: '1.25.4',
7332 agent: 'git/isomorphic-git@1.25.4',
7333};
7334
7335class FIFO {
7336 constructor() {
7337 this._queue = [];
7338 }
7339
7340 write(chunk) {
7341 if (this._ended) {
7342 throw Error('You cannot write to a FIFO that has already been ended!')
7343 }
7344 if (this._waiting) {
7345 const resolve = this._waiting;
7346 this._waiting = null;
7347 resolve({ value: chunk });
7348 } else {
7349 this._queue.push(chunk);
7350 }
7351 }
7352
7353 end() {
7354 this._ended = true;
7355 if (this._waiting) {
7356 const resolve = this._waiting;
7357 this._waiting = null;
7358 resolve({ done: true });
7359 }
7360 }
7361
7362 destroy(err) {
7363 this.error = err;
7364 this.end();
7365 }
7366
7367 async next() {
7368 if (this._queue.length > 0) {
7369 return { value: this._queue.shift() }
7370 }
7371 if (this._ended) {
7372 return { done: true }
7373 }
7374 if (this._waiting) {
7375 throw Error(
7376 'You cannot call read until the previous call to read has returned!'
7377 )
7378 }
7379 return new Promise(resolve => {
7380 this._waiting = resolve;
7381 })
7382 }
7383}
7384
7385// Note: progress messages are designed to be written directly to the terminal,
7386// so they are often sent with just a carriage return to overwrite the last line of output.
7387// But there are also messages delimited with newlines.
7388// I also include CRLF just in case.
7389function findSplit(str) {
7390 const r = str.indexOf('\r');
7391 const n = str.indexOf('\n');
7392 if (r === -1 && n === -1) return -1
7393 if (r === -1) return n + 1 // \n
7394 if (n === -1) return r + 1 // \r
7395 if (n === r + 1) return n + 1 // \r\n
7396 return Math.min(r, n) + 1 // \r or \n
7397}
7398
7399function splitLines(input) {
7400 const output = new FIFO();
7401 let tmp = ''
7402 ;(async () => {
7403 await forAwait(input, chunk => {
7404 chunk = chunk.toString('utf8');
7405 tmp += chunk;
7406 while (true) {
7407 const i = findSplit(tmp);
7408 if (i === -1) break
7409 output.write(tmp.slice(0, i));
7410 tmp = tmp.slice(i);
7411 }
7412 });
7413 if (tmp.length > 0) {
7414 output.write(tmp);
7415 }
7416 output.end();
7417 })();
7418 return output
7419}
7420
7421/*
7422If 'side-band' or 'side-band-64k' capabilities have been specified by
7423the client, the server will send the packfile data multiplexed.
7424
7425Each packet starting with the packet-line length of the amount of data
7426that follows, followed by a single byte specifying the sideband the
7427following data is coming in on.
7428
7429In 'side-band' mode, it will send up to 999 data bytes plus 1 control
7430code, for a total of up to 1000 bytes in a pkt-line. In 'side-band-64k'
7431mode it will send up to 65519 data bytes plus 1 control code, for a
7432total of up to 65520 bytes in a pkt-line.
7433
7434The sideband byte will be a '1', '2' or a '3'. Sideband '1' will contain
7435packfile data, sideband '2' will be used for progress information that the
7436client will generally print to stderr and sideband '3' is used for error
7437information.
7438
7439If no 'side-band' capability was specified, the server will stream the
7440entire packfile without multiplexing.
7441*/
7442
7443class GitSideBand {
7444 static demux(input) {
7445 const read = GitPktLine.streamReader(input);
7446 // And now for the ridiculous side-band or side-band-64k protocol
7447 const packetlines = new FIFO();
7448 const packfile = new FIFO();
7449 const progress = new FIFO();
7450 // TODO: Use a proper through stream?
7451 const nextBit = async function() {
7452 const line = await read();
7453 // Skip over flush packets
7454 if (line === null) return nextBit()
7455 // A made up convention to signal there's no more to read.
7456 if (line === true) {
7457 packetlines.end();
7458 progress.end();
7459 input.error ? packfile.destroy(input.error) : packfile.end();
7460 return
7461 }
7462 // Examine first byte to determine which output "stream" to use
7463 switch (line[0]) {
7464 case 1: {
7465 // pack data
7466 packfile.write(line.slice(1));
7467 break
7468 }
7469 case 2: {
7470 // progress message
7471 progress.write(line.slice(1));
7472 break
7473 }
7474 case 3: {
7475 // fatal error message just before stream aborts
7476 const error = line.slice(1);
7477 progress.write(error);
7478 packetlines.end();
7479 progress.end();
7480 packfile.destroy(new Error(error.toString('utf8')));
7481 return
7482 }
7483 default: {
7484 // Not part of the side-band-64k protocol
7485 packetlines.write(line);
7486 }
7487 }
7488 // Careful not to blow up the stack.
7489 // I think Promises in a tail-call position should be OK.
7490 nextBit();
7491 };
7492 nextBit();
7493 return {
7494 packetlines,
7495 packfile,
7496 progress,
7497 }
7498 }
7499 // static mux ({
7500 // protocol, // 'side-band' or 'side-band-64k'
7501 // packetlines,
7502 // packfile,
7503 // progress,
7504 // error
7505 // }) {
7506 // const MAX_PACKET_LENGTH = protocol === 'side-band-64k' ? 999 : 65519
7507 // let output = new PassThrough()
7508 // packetlines.on('data', data => {
7509 // if (data === null) {
7510 // output.write(GitPktLine.flush())
7511 // } else {
7512 // output.write(GitPktLine.encode(data))
7513 // }
7514 // })
7515 // let packfileWasEmpty = true
7516 // let packfileEnded = false
7517 // let progressEnded = false
7518 // let errorEnded = false
7519 // let goodbye = Buffer.concat([
7520 // GitPktLine.encode(Buffer.from('010A', 'hex')),
7521 // GitPktLine.flush()
7522 // ])
7523 // packfile
7524 // .on('data', data => {
7525 // packfileWasEmpty = false
7526 // const buffers = splitBuffer(data, MAX_PACKET_LENGTH)
7527 // for (const buffer of buffers) {
7528 // output.write(
7529 // GitPktLine.encode(Buffer.concat([Buffer.from('01', 'hex'), buffer]))
7530 // )
7531 // }
7532 // })
7533 // .on('end', () => {
7534 // packfileEnded = true
7535 // if (!packfileWasEmpty) output.write(goodbye)
7536 // if (progressEnded && errorEnded) output.end()
7537 // })
7538 // progress
7539 // .on('data', data => {
7540 // const buffers = splitBuffer(data, MAX_PACKET_LENGTH)
7541 // for (const buffer of buffers) {
7542 // output.write(
7543 // GitPktLine.encode(Buffer.concat([Buffer.from('02', 'hex'), buffer]))
7544 // )
7545 // }
7546 // })
7547 // .on('end', () => {
7548 // progressEnded = true
7549 // if (packfileEnded && errorEnded) output.end()
7550 // })
7551 // error
7552 // .on('data', data => {
7553 // const buffers = splitBuffer(data, MAX_PACKET_LENGTH)
7554 // for (const buffer of buffers) {
7555 // output.write(
7556 // GitPktLine.encode(Buffer.concat([Buffer.from('03', 'hex'), buffer]))
7557 // )
7558 // }
7559 // })
7560 // .on('end', () => {
7561 // errorEnded = true
7562 // if (progressEnded && packfileEnded) output.end()
7563 // })
7564 // return output
7565 // }
7566}
7567
7568async function parseUploadPackResponse(stream) {
7569 const { packetlines, packfile, progress } = GitSideBand.demux(stream);
7570 const shallows = [];
7571 const unshallows = [];
7572 const acks = [];
7573 let nak = false;
7574 let done = false;
7575 return new Promise((resolve, reject) => {
7576 // Parse the response
7577 forAwait(packetlines, data => {
7578 const line = data.toString('utf8').trim();
7579 if (line.startsWith('shallow')) {
7580 const oid = line.slice(-41).trim();
7581 if (oid.length !== 40) {
7582 reject(new InvalidOidError(oid));
7583 }
7584 shallows.push(oid);
7585 } else if (line.startsWith('unshallow')) {
7586 const oid = line.slice(-41).trim();
7587 if (oid.length !== 40) {
7588 reject(new InvalidOidError(oid));
7589 }
7590 unshallows.push(oid);
7591 } else if (line.startsWith('ACK')) {
7592 const [, oid, status] = line.split(' ');
7593 acks.push({ oid, status });
7594 if (!status) done = true;
7595 } else if (line.startsWith('NAK')) {
7596 nak = true;
7597 done = true;
7598 } else {
7599 done = true;
7600 nak = true;
7601 }
7602 if (done) {
7603 stream.error
7604 ? reject(stream.error)
7605 : resolve({ shallows, unshallows, acks, nak, packfile, progress });
7606 }
7607 }).finally(() => {
7608 if (!done) {
7609 stream.error
7610 ? reject(stream.error)
7611 : resolve({ shallows, unshallows, acks, nak, packfile, progress });
7612 }
7613 });
7614 })
7615}
7616
7617function writeUploadPackRequest({
7618 capabilities = [],
7619 wants = [],
7620 haves = [],
7621 shallows = [],
7622 depth = null,
7623 since = null,
7624 exclude = [],
7625}) {
7626 const packstream = [];
7627 wants = [...new Set(wants)]; // remove duplicates
7628 let firstLineCapabilities = ` ${capabilities.join(' ')}`;
7629 for (const oid of wants) {
7630 packstream.push(GitPktLine.encode(`want ${oid}${firstLineCapabilities}\n`));
7631 firstLineCapabilities = '';
7632 }
7633 for (const oid of shallows) {
7634 packstream.push(GitPktLine.encode(`shallow ${oid}\n`));
7635 }
7636 if (depth !== null) {
7637 packstream.push(GitPktLine.encode(`deepen ${depth}\n`));
7638 }
7639 if (since !== null) {
7640 packstream.push(
7641 GitPktLine.encode(`deepen-since ${Math.floor(since.valueOf() / 1000)}\n`)
7642 );
7643 }
7644 for (const oid of exclude) {
7645 packstream.push(GitPktLine.encode(`deepen-not ${oid}\n`));
7646 }
7647 packstream.push(GitPktLine.flush());
7648 for (const oid of haves) {
7649 packstream.push(GitPktLine.encode(`have ${oid}\n`));
7650 }
7651 packstream.push(GitPktLine.encode(`done\n`));
7652 return packstream
7653}
7654
7655// @ts-check
7656
7657/**
7658 *
7659 * @typedef {object} FetchResult - The object returned has the following schema:
7660 * @property {string | null} defaultBranch - The branch that is cloned if no branch is specified
7661 * @property {string | null} fetchHead - The SHA-1 object id of the fetched head commit
7662 * @property {string | null} fetchHeadDescription - a textual description of the branch that was fetched
7663 * @property {Object<string, string>} [headers] - The HTTP response headers returned by the git server
7664 * @property {string[]} [pruned] - A list of branches that were pruned, if you provided the `prune` parameter
7665 *
7666 */
7667
7668/**
7669 * @param {object} args
7670 * @param {import('../models/FileSystem.js').FileSystem} args.fs
7671 * @param {any} args.cache
7672 * @param {HttpClient} args.http
7673 * @param {ProgressCallback} [args.onProgress]
7674 * @param {MessageCallback} [args.onMessage]
7675 * @param {AuthCallback} [args.onAuth]
7676 * @param {AuthFailureCallback} [args.onAuthFailure]
7677 * @param {AuthSuccessCallback} [args.onAuthSuccess]
7678 * @param {string} args.gitdir
7679 * @param {string|void} [args.url]
7680 * @param {string} [args.corsProxy]
7681 * @param {string} [args.ref]
7682 * @param {string} [args.remoteRef]
7683 * @param {string} [args.remote]
7684 * @param {boolean} [args.singleBranch = false]
7685 * @param {boolean} [args.tags = false]
7686 * @param {number} [args.depth]
7687 * @param {Date} [args.since]
7688 * @param {string[]} [args.exclude = []]
7689 * @param {boolean} [args.relative = false]
7690 * @param {Object<string, string>} [args.headers]
7691 * @param {boolean} [args.prune]
7692 * @param {boolean} [args.pruneTags]
7693 *
7694 * @returns {Promise<FetchResult>}
7695 * @see FetchResult
7696 */
7697async function _fetch({
7698 fs,
7699 cache,
7700 http,
7701 onProgress,
7702 onMessage,
7703 onAuth,
7704 onAuthSuccess,
7705 onAuthFailure,
7706 gitdir,
7707 ref: _ref,
7708 remoteRef: _remoteRef,
7709 remote: _remote,
7710 url: _url,
7711 corsProxy,
7712 depth = null,
7713 since = null,
7714 exclude = [],
7715 relative = false,
7716 tags = false,
7717 singleBranch = false,
7718 headers = {},
7719 prune = false,
7720 pruneTags = false,
7721}) {
7722 const ref = _ref || (await _currentBranch({ fs, gitdir, test: true }));
7723 const config = await GitConfigManager.get({ fs, gitdir });
7724 // Figure out what remote to use.
7725 const remote =
7726 _remote || (ref && (await config.get(`branch.${ref}.remote`))) || 'origin';
7727 // Lookup the URL for the given remote.
7728 const url = _url || (await config.get(`remote.${remote}.url`));
7729 if (typeof url === 'undefined') {
7730 throw new MissingParameterError('remote OR url')
7731 }
7732 // Figure out what remote ref to use.
7733 const remoteRef =
7734 _remoteRef ||
7735 (ref && (await config.get(`branch.${ref}.merge`))) ||
7736 _ref ||
7737 'HEAD';
7738
7739 if (corsProxy === undefined) {
7740 corsProxy = await config.get('http.corsProxy');
7741 }
7742
7743 const GitRemoteHTTP = GitRemoteManager.getRemoteHelperFor({ url });
7744 const remoteHTTP = await GitRemoteHTTP.discover({
7745 http,
7746 onAuth,
7747 onAuthSuccess,
7748 onAuthFailure,
7749 corsProxy,
7750 service: 'git-upload-pack',
7751 url,
7752 headers,
7753 protocolVersion: 1,
7754 });
7755 const auth = remoteHTTP.auth; // hack to get new credentials from CredentialManager API
7756 const remoteRefs = remoteHTTP.refs;
7757 // For the special case of an empty repository with no refs, return null.
7758 if (remoteRefs.size === 0) {
7759 return {
7760 defaultBranch: null,
7761 fetchHead: null,
7762 fetchHeadDescription: null,
7763 }
7764 }
7765 // Check that the remote supports the requested features
7766 if (depth !== null && !remoteHTTP.capabilities.has('shallow')) {
7767 throw new RemoteCapabilityError('shallow', 'depth')
7768 }
7769 if (since !== null && !remoteHTTP.capabilities.has('deepen-since')) {
7770 throw new RemoteCapabilityError('deepen-since', 'since')
7771 }
7772 if (exclude.length > 0 && !remoteHTTP.capabilities.has('deepen-not')) {
7773 throw new RemoteCapabilityError('deepen-not', 'exclude')
7774 }
7775 if (relative === true && !remoteHTTP.capabilities.has('deepen-relative')) {
7776 throw new RemoteCapabilityError('deepen-relative', 'relative')
7777 }
7778 // Figure out the SHA for the requested ref
7779 const { oid, fullref } = GitRefManager.resolveAgainstMap({
7780 ref: remoteRef,
7781 map: remoteRefs,
7782 });
7783 // Filter out refs we want to ignore: only keep ref we're cloning, HEAD, branches, and tags (if we're keeping them)
7784 for (const remoteRef of remoteRefs.keys()) {
7785 if (
7786 remoteRef === fullref ||
7787 remoteRef === 'HEAD' ||
7788 remoteRef.startsWith('refs/heads/') ||
7789 (tags && remoteRef.startsWith('refs/tags/'))
7790 ) {
7791 continue
7792 }
7793 remoteRefs.delete(remoteRef);
7794 }
7795 // Assemble the application/x-git-upload-pack-request
7796 const capabilities = filterCapabilities(
7797 [...remoteHTTP.capabilities],
7798 [
7799 'multi_ack_detailed',
7800 'no-done',
7801 'side-band-64k',
7802 // Note: I removed 'thin-pack' option since our code doesn't "fatten" packfiles,
7803 // which is necessary for compatibility with git. It was the cause of mysterious
7804 // 'fatal: pack has [x] unresolved deltas' errors that plagued us for some time.
7805 // isomorphic-git is perfectly happy with thin packfiles in .git/objects/pack but
7806 // canonical git it turns out is NOT.
7807 'ofs-delta',
7808 `agent=${pkg.agent}`,
7809 ]
7810 );
7811 if (relative) capabilities.push('deepen-relative');
7812 // Start figuring out which oids from the remote we want to request
7813 const wants = singleBranch ? [oid] : remoteRefs.values();
7814 // Come up with a reasonable list of oids to tell the remote we already have
7815 // (preferably oids that are close ancestors of the branch heads we're fetching)
7816 const haveRefs = singleBranch
7817 ? [ref]
7818 : await GitRefManager.listRefs({
7819 fs,
7820 gitdir,
7821 filepath: `refs`,
7822 });
7823 let haves = [];
7824 for (let ref of haveRefs) {
7825 try {
7826 ref = await GitRefManager.expand({ fs, gitdir, ref });
7827 const oid = await GitRefManager.resolve({ fs, gitdir, ref });
7828 if (await hasObject({ fs, cache, gitdir, oid })) {
7829 haves.push(oid);
7830 }
7831 } catch (err) {}
7832 }
7833 haves = [...new Set(haves)];
7834 const oids = await GitShallowManager.read({ fs, gitdir });
7835 const shallows = remoteHTTP.capabilities.has('shallow') ? [...oids] : [];
7836 const packstream = writeUploadPackRequest({
7837 capabilities,
7838 wants,
7839 haves,
7840 shallows,
7841 depth,
7842 since,
7843 exclude,
7844 });
7845 // CodeCommit will hang up if we don't send a Content-Length header
7846 // so we can't stream the body.
7847 const packbuffer = Buffer.from(await collect(packstream));
7848 const raw = await GitRemoteHTTP.connect({
7849 http,
7850 onProgress,
7851 corsProxy,
7852 service: 'git-upload-pack',
7853 url,
7854 auth,
7855 body: [packbuffer],
7856 headers,
7857 });
7858 const response = await parseUploadPackResponse(raw.body);
7859 if (raw.headers) {
7860 response.headers = raw.headers;
7861 }
7862 // Apply all the 'shallow' and 'unshallow' commands
7863 for (const oid of response.shallows) {
7864 if (!oids.has(oid)) {
7865 // this is in a try/catch mostly because my old test fixtures are missing objects
7866 try {
7867 // server says it's shallow, but do we have the parents?
7868 const { object } = await _readObject({ fs, cache, gitdir, oid });
7869 const commit = new GitCommit(object);
7870 const hasParents = await Promise.all(
7871 commit
7872 .headers()
7873 .parent.map(oid => hasObject({ fs, cache, gitdir, oid }))
7874 );
7875 const haveAllParents =
7876 hasParents.length === 0 || hasParents.every(has => has);
7877 if (!haveAllParents) {
7878 oids.add(oid);
7879 }
7880 } catch (err) {
7881 oids.add(oid);
7882 }
7883 }
7884 }
7885 for (const oid of response.unshallows) {
7886 oids.delete(oid);
7887 }
7888 await GitShallowManager.write({ fs, gitdir, oids });
7889 // Update local remote refs
7890 if (singleBranch) {
7891 const refs = new Map([[fullref, oid]]);
7892 // But wait, maybe it was a symref, like 'HEAD'!
7893 // We need to save all the refs in the symref chain (sigh).
7894 const symrefs = new Map();
7895 let bail = 10;
7896 let key = fullref;
7897 while (bail--) {
7898 const value = remoteHTTP.symrefs.get(key);
7899 if (value === undefined) break
7900 symrefs.set(key, value);
7901 key = value;
7902 }
7903 // final value must not be a symref but a real ref
7904 const realRef = remoteRefs.get(key);
7905 // There may be no ref at all if we've fetched a specific commit hash
7906 if (realRef) {
7907 refs.set(key, realRef);
7908 }
7909 const { pruned } = await GitRefManager.updateRemoteRefs({
7910 fs,
7911 gitdir,
7912 remote,
7913 refs,
7914 symrefs,
7915 tags,
7916 prune,
7917 });
7918 if (prune) {
7919 response.pruned = pruned;
7920 }
7921 } else {
7922 const { pruned } = await GitRefManager.updateRemoteRefs({
7923 fs,
7924 gitdir,
7925 remote,
7926 refs: remoteRefs,
7927 symrefs: remoteHTTP.symrefs,
7928 tags,
7929 prune,
7930 pruneTags,
7931 });
7932 if (prune) {
7933 response.pruned = pruned;
7934 }
7935 }
7936 // We need this value later for the `clone` command.
7937 response.HEAD = remoteHTTP.symrefs.get('HEAD');
7938 // AWS CodeCommit doesn't list HEAD as a symref, but we can reverse engineer it
7939 // Find the SHA of the branch called HEAD
7940 if (response.HEAD === undefined) {
7941 const { oid } = GitRefManager.resolveAgainstMap({
7942 ref: 'HEAD',
7943 map: remoteRefs,
7944 });
7945 // Use the name of the first branch that's not called HEAD that has
7946 // the same SHA as the branch called HEAD.
7947 for (const [key, value] of remoteRefs.entries()) {
7948 if (key !== 'HEAD' && value === oid) {
7949 response.HEAD = key;
7950 break
7951 }
7952 }
7953 }
7954 const noun = fullref.startsWith('refs/tags') ? 'tag' : 'branch';
7955 response.FETCH_HEAD = {
7956 oid,
7957 description: `${noun} '${abbreviateRef(fullref)}' of ${url}`,
7958 };
7959
7960 if (onProgress || onMessage) {
7961 const lines = splitLines(response.progress);
7962 forAwait(lines, async line => {
7963 if (onMessage) await onMessage(line);
7964 if (onProgress) {
7965 const matches = line.match(/([^:]*).*\((\d+?)\/(\d+?)\)/);
7966 if (matches) {
7967 await onProgress({
7968 phase: matches[1].trim(),
7969 loaded: parseInt(matches[2], 10),
7970 total: parseInt(matches[3], 10),
7971 });
7972 }
7973 }
7974 });
7975 }
7976 const packfile = Buffer.from(await collect(response.packfile));
7977 if (raw.body.error) throw raw.body.error
7978 const packfileSha = packfile.slice(-20).toString('hex');
7979 const res = {
7980 defaultBranch: response.HEAD,
7981 fetchHead: response.FETCH_HEAD.oid,
7982 fetchHeadDescription: response.FETCH_HEAD.description,
7983 };
7984 if (response.headers) {
7985 res.headers = response.headers;
7986 }
7987 if (prune) {
7988 res.pruned = response.pruned;
7989 }
7990 // This is a quick fix for the empty .git/objects/pack/pack-.pack file error,
7991 // which due to the way `git-list-pack` works causes the program to hang when it tries to read it.
7992 // TODO: Longer term, we should actually:
7993 // a) NOT concatenate the entire packfile into memory (line 78),
7994 // b) compute the SHA of the stream except for the last 20 bytes, using the same library used in push.js, and
7995 // c) compare the computed SHA with the last 20 bytes of the stream before saving to disk, and throwing a "packfile got corrupted during download" error if the SHA doesn't match.
7996 if (packfileSha !== '' && !emptyPackfile(packfile)) {
7997 res.packfile = `objects/pack/pack-${packfileSha}.pack`;
7998 const fullpath = join(gitdir, res.packfile);
7999 await fs.write(fullpath, packfile);
8000 const getExternalRefDelta = oid => _readObject({ fs, cache, gitdir, oid });
8001 const idx = await GitPackIndex.fromPack({
8002 pack: packfile,
8003 getExternalRefDelta,
8004 onProgress,
8005 });
8006 await fs.write(fullpath.replace(/\.pack$/, '.idx'), await idx.toBuffer());
8007 }
8008 return res
8009}
8010
8011// @ts-check
8012
8013/**
8014 * Initialize a new repository
8015 *
8016 * @param {object} args
8017 * @param {import('../models/FileSystem.js').FileSystem} args.fs
8018 * @param {string} [args.dir]
8019 * @param {string} [args.gitdir]
8020 * @param {boolean} [args.bare = false]
8021 * @param {string} [args.defaultBranch = 'master']
8022 * @returns {Promise<void>}
8023 */
8024async function _init({
8025 fs,
8026 bare = false,
8027 dir,
8028 gitdir = bare ? dir : join(dir, '.git'),
8029 defaultBranch = 'master',
8030}) {
8031 // Don't overwrite an existing config
8032 if (await fs.exists(gitdir + '/config')) return
8033
8034 let folders = [
8035 'hooks',
8036 'info',
8037 'objects/info',
8038 'objects/pack',
8039 'refs/heads',
8040 'refs/tags',
8041 ];
8042 folders = folders.map(dir => gitdir + '/' + dir);
8043 for (const folder of folders) {
8044 await fs.mkdir(folder);
8045 }
8046
8047 await fs.write(
8048 gitdir + '/config',
8049 '[core]\n' +
8050 '\trepositoryformatversion = 0\n' +
8051 '\tfilemode = false\n' +
8052 `\tbare = ${bare}\n` +
8053 (bare ? '' : '\tlogallrefupdates = true\n') +
8054 '\tsymlinks = false\n' +
8055 '\tignorecase = true\n'
8056 );
8057 await fs.write(gitdir + '/HEAD', `ref: refs/heads/${defaultBranch}\n`);
8058}
8059
8060// @ts-check
8061
8062/**
8063 * @param {object} args
8064 * @param {import('../models/FileSystem.js').FileSystem} args.fs
8065 * @param {object} args.cache
8066 * @param {HttpClient} args.http
8067 * @param {ProgressCallback} [args.onProgress]
8068 * @param {MessageCallback} [args.onMessage]
8069 * @param {AuthCallback} [args.onAuth]
8070 * @param {AuthFailureCallback} [args.onAuthFailure]
8071 * @param {AuthSuccessCallback} [args.onAuthSuccess]
8072 * @param {string} [args.dir]
8073 * @param {string} args.gitdir
8074 * @param {string} args.url
8075 * @param {string} args.corsProxy
8076 * @param {string} args.ref
8077 * @param {boolean} args.singleBranch
8078 * @param {boolean} args.noCheckout
8079 * @param {boolean} args.noTags
8080 * @param {string} args.remote
8081 * @param {number} args.depth
8082 * @param {Date} args.since
8083 * @param {string[]} args.exclude
8084 * @param {boolean} args.relative
8085 * @param {Object<string, string>} args.headers
8086 *
8087 * @returns {Promise<void>} Resolves successfully when clone completes
8088 *
8089 */
8090async function _clone({
8091 fs,
8092 cache,
8093 http,
8094 onProgress,
8095 onMessage,
8096 onAuth,
8097 onAuthSuccess,
8098 onAuthFailure,
8099 dir,
8100 gitdir,
8101 url,
8102 corsProxy,
8103 ref,
8104 remote,
8105 depth,
8106 since,
8107 exclude,
8108 relative,
8109 singleBranch,
8110 noCheckout,
8111 noTags,
8112 headers,
8113}) {
8114 try {
8115 await _init({ fs, gitdir });
8116 await _addRemote({ fs, gitdir, remote, url, force: false });
8117 if (corsProxy) {
8118 const config = await GitConfigManager.get({ fs, gitdir });
8119 await config.set(`http.corsProxy`, corsProxy);
8120 await GitConfigManager.save({ fs, gitdir, config });
8121 }
8122 const { defaultBranch, fetchHead } = await _fetch({
8123 fs,
8124 cache,
8125 http,
8126 onProgress,
8127 onMessage,
8128 onAuth,
8129 onAuthSuccess,
8130 onAuthFailure,
8131 gitdir,
8132 ref,
8133 remote,
8134 corsProxy,
8135 depth,
8136 since,
8137 exclude,
8138 relative,
8139 singleBranch,
8140 headers,
8141 tags: !noTags,
8142 });
8143 if (fetchHead === null) return
8144 ref = ref || defaultBranch;
8145 ref = ref.replace('refs/heads/', '');
8146 // Checkout that branch
8147 await _checkout({
8148 fs,
8149 cache,
8150 onProgress,
8151 dir,
8152 gitdir,
8153 ref,
8154 remote,
8155 noCheckout,
8156 });
8157 } catch (err) {
8158 // Remove partial local repository, see #1283
8159 // Ignore any error as we are already failing.
8160 // The catch is necessary so the original error is not masked.
8161 await fs
8162 .rmdir(gitdir, { recursive: true, maxRetries: 10 })
8163 .catch(() => undefined);
8164 throw err
8165 }
8166}
8167
8168// @ts-check
8169
8170/**
8171 * Clone a repository
8172 *
8173 * @param {object} args
8174 * @param {FsClient} args.fs - a file system implementation
8175 * @param {HttpClient} args.http - an HTTP client
8176 * @param {ProgressCallback} [args.onProgress] - optional progress event callback
8177 * @param {MessageCallback} [args.onMessage] - optional message event callback
8178 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
8179 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
8180 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
8181 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
8182 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8183 * @param {string} args.url - The URL of the remote repository
8184 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Value is stored in the git config file for that repo.
8185 * @param {string} [args.ref] - Which branch to checkout. By default this is the designated "main branch" of the repository.
8186 * @param {boolean} [args.singleBranch = false] - Instead of the default behavior of fetching all the branches, only fetch a single branch.
8187 * @param {boolean} [args.noCheckout = false] - If true, clone will only fetch the repo, not check out a branch. Skipping checkout can save a lot of time normally spent writing files to disk.
8188 * @param {boolean} [args.noTags = false] - By default clone will fetch all tags. `noTags` disables that behavior.
8189 * @param {string} [args.remote = 'origin'] - What to name the remote that is created.
8190 * @param {number} [args.depth] - Integer. Determines how much of the git repository's history to retrieve
8191 * @param {Date} [args.since] - Only fetch commits created after the given date. Mutually exclusive with `depth`.
8192 * @param {string[]} [args.exclude = []] - A list of branches or tags. Instructs the remote server not to send us any commits reachable from these refs.
8193 * @param {boolean} [args.relative = false] - Changes the meaning of `depth` to be measured from the current shallow depth rather than from the branch tip.
8194 * @param {Object<string, string>} [args.headers = {}] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
8195 * @param {object} [args.cache] - a [cache](cache.md) object
8196 *
8197 * @returns {Promise<void>} Resolves successfully when clone completes
8198 *
8199 * @example
8200 * await git.clone({
8201 * fs,
8202 * http,
8203 * dir: '/tutorial',
8204 * corsProxy: 'https://cors.isomorphic-git.org',
8205 * url: 'https://github.com/isomorphic-git/isomorphic-git',
8206 * singleBranch: true,
8207 * depth: 1
8208 * })
8209 * console.log('done')
8210 *
8211 */
8212async function clone({
8213 fs,
8214 http,
8215 onProgress,
8216 onMessage,
8217 onAuth,
8218 onAuthSuccess,
8219 onAuthFailure,
8220 dir,
8221 gitdir = join(dir, '.git'),
8222 url,
8223 corsProxy = undefined,
8224 ref = undefined,
8225 remote = 'origin',
8226 depth = undefined,
8227 since = undefined,
8228 exclude = [],
8229 relative = false,
8230 singleBranch = false,
8231 noCheckout = false,
8232 noTags = false,
8233 headers = {},
8234 cache = {},
8235}) {
8236 try {
8237 assertParameter('fs', fs);
8238 assertParameter('http', http);
8239 assertParameter('gitdir', gitdir);
8240 if (!noCheckout) {
8241 assertParameter('dir', dir);
8242 }
8243 assertParameter('url', url);
8244
8245 return await _clone({
8246 fs: new FileSystem(fs),
8247 cache,
8248 http,
8249 onProgress,
8250 onMessage,
8251 onAuth,
8252 onAuthSuccess,
8253 onAuthFailure,
8254 dir,
8255 gitdir,
8256 url,
8257 corsProxy,
8258 ref,
8259 remote,
8260 depth,
8261 since,
8262 exclude,
8263 relative,
8264 singleBranch,
8265 noCheckout,
8266 noTags,
8267 headers,
8268 })
8269 } catch (err) {
8270 err.caller = 'git.clone';
8271 throw err
8272 }
8273}
8274
8275// @ts-check
8276
8277/**
8278 * Create a new commit
8279 *
8280 * @param {Object} args
8281 * @param {FsClient} args.fs - a file system implementation
8282 * @param {SignCallback} [args.onSign] - a PGP signing implementation
8283 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8284 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8285 * @param {string} args.message - The commit message to use.
8286 * @param {Object} [args.author] - The details about the author.
8287 * @param {string} [args.author.name] - Default is `user.name` config.
8288 * @param {string} [args.author.email] - Default is `user.email` config.
8289 * @param {number} [args.author.timestamp=Math.floor(Date.now()/1000)] - Set the author timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
8290 * @param {number} [args.author.timezoneOffset] - Set the author timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
8291 * @param {Object} [args.committer = author] - The details about the commit committer, in the same format as the author parameter. If not specified, the author details are used.
8292 * @param {string} [args.committer.name] - Default is `user.name` config.
8293 * @param {string} [args.committer.email] - Default is `user.email` config.
8294 * @param {number} [args.committer.timestamp=Math.floor(Date.now()/1000)] - Set the committer timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
8295 * @param {number} [args.committer.timezoneOffset] - Set the committer timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
8296 * @param {string} [args.signingKey] - Sign the tag object using this private PGP key.
8297 * @param {boolean} [args.dryRun = false] - If true, simulates making a commit so you can test whether it would succeed. Implies `noUpdateBranch`.
8298 * @param {boolean} [args.noUpdateBranch = false] - If true, does not update the branch pointer after creating the commit.
8299 * @param {string} [args.ref] - The fully expanded name of the branch to commit to. Default is the current branch pointed to by HEAD. (TODO: fix it so it can expand branch names without throwing if the branch doesn't exist yet.)
8300 * @param {string[]} [args.parent] - The SHA-1 object ids of the commits to use as parents. If not specified, the commit pointed to by `ref` is used.
8301 * @param {string} [args.tree] - The SHA-1 object id of the tree to use. If not specified, a new tree object is created from the current git index.
8302 * @param {object} [args.cache] - a [cache](cache.md) object
8303 *
8304 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly created commit.
8305 *
8306 * @example
8307 * let sha = await git.commit({
8308 * fs,
8309 * dir: '/tutorial',
8310 * author: {
8311 * name: 'Mr. Test',
8312 * email: 'mrtest@example.com',
8313 * },
8314 * message: 'Added the a.txt file'
8315 * })
8316 * console.log(sha)
8317 *
8318 */
8319async function commit({
8320 fs: _fs,
8321 onSign,
8322 dir,
8323 gitdir = join(dir, '.git'),
8324 message,
8325 author: _author,
8326 committer: _committer,
8327 signingKey,
8328 dryRun = false,
8329 noUpdateBranch = false,
8330 ref,
8331 parent,
8332 tree,
8333 cache = {},
8334}) {
8335 try {
8336 assertParameter('fs', _fs);
8337 assertParameter('message', message);
8338 if (signingKey) {
8339 assertParameter('onSign', onSign);
8340 }
8341 const fs = new FileSystem(_fs);
8342
8343 const author = await normalizeAuthorObject({ fs, gitdir, author: _author });
8344 if (!author) throw new MissingNameError('author')
8345
8346 const committer = await normalizeCommitterObject({
8347 fs,
8348 gitdir,
8349 author,
8350 committer: _committer,
8351 });
8352 if (!committer) throw new MissingNameError('committer')
8353
8354 return await _commit({
8355 fs,
8356 cache,
8357 onSign,
8358 gitdir,
8359 message,
8360 author,
8361 committer,
8362 signingKey,
8363 dryRun,
8364 noUpdateBranch,
8365 ref,
8366 parent,
8367 tree,
8368 })
8369 } catch (err) {
8370 err.caller = 'git.commit';
8371 throw err
8372 }
8373}
8374
8375// @ts-check
8376
8377/**
8378 * Get the name of the branch currently pointed to by .git/HEAD
8379 *
8380 * @param {Object} args
8381 * @param {FsClient} args.fs - a file system implementation
8382 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8383 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8384 * @param {boolean} [args.fullname = false] - Return the full path (e.g. "refs/heads/main") instead of the abbreviated form.
8385 * @param {boolean} [args.test = false] - If the current branch doesn't actually exist (such as right after git init) then return `undefined`.
8386 *
8387 * @returns {Promise<string|void>} The name of the current branch or undefined if the HEAD is detached.
8388 *
8389 * @example
8390 * // Get the current branch name
8391 * let branch = await git.currentBranch({
8392 * fs,
8393 * dir: '/tutorial',
8394 * fullname: false
8395 * })
8396 * console.log(branch)
8397 *
8398 */
8399async function currentBranch({
8400 fs,
8401 dir,
8402 gitdir = join(dir, '.git'),
8403 fullname = false,
8404 test = false,
8405}) {
8406 try {
8407 assertParameter('fs', fs);
8408 assertParameter('gitdir', gitdir);
8409 return await _currentBranch({
8410 fs: new FileSystem(fs),
8411 gitdir,
8412 fullname,
8413 test,
8414 })
8415 } catch (err) {
8416 err.caller = 'git.currentBranch';
8417 throw err
8418 }
8419}
8420
8421// @ts-check
8422
8423/**
8424 * @param {Object} args
8425 * @param {import('../models/FileSystem.js').FileSystem} args.fs
8426 * @param {string} args.gitdir
8427 * @param {string} args.ref
8428 *
8429 * @returns {Promise<void>}
8430 */
8431async function _deleteBranch({ fs, gitdir, ref }) {
8432 ref = ref.startsWith('refs/heads/') ? ref : `refs/heads/${ref}`;
8433 const exist = await GitRefManager.exists({ fs, gitdir, ref });
8434 if (!exist) {
8435 throw new NotFoundError(ref)
8436 }
8437
8438 const fullRef = await GitRefManager.expand({ fs, gitdir, ref });
8439 const currentRef = await _currentBranch({ fs, gitdir, fullname: true });
8440 if (fullRef === currentRef) {
8441 // detach HEAD
8442 const value = await GitRefManager.resolve({ fs, gitdir, ref: fullRef });
8443 await GitRefManager.writeRef({ fs, gitdir, ref: 'HEAD', value });
8444 }
8445
8446 // Delete a specified branch
8447 await GitRefManager.deleteRef({ fs, gitdir, ref: fullRef });
8448}
8449
8450// @ts-check
8451
8452/**
8453 * Delete a local branch
8454 *
8455 * > Note: This only deletes loose branches - it should be fixed in the future to delete packed branches as well.
8456 *
8457 * @param {Object} args
8458 * @param {FsClient} args.fs - a file system implementation
8459 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8460 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8461 * @param {string} args.ref - The branch to delete
8462 *
8463 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
8464 *
8465 * @example
8466 * await git.deleteBranch({ fs, dir: '/tutorial', ref: 'local-branch' })
8467 * console.log('done')
8468 *
8469 */
8470async function deleteBranch({
8471 fs,
8472 dir,
8473 gitdir = join(dir, '.git'),
8474 ref,
8475}) {
8476 try {
8477 assertParameter('fs', fs);
8478 assertParameter('ref', ref);
8479 return await _deleteBranch({
8480 fs: new FileSystem(fs),
8481 gitdir,
8482 ref,
8483 })
8484 } catch (err) {
8485 err.caller = 'git.deleteBranch';
8486 throw err
8487 }
8488}
8489
8490// @ts-check
8491
8492/**
8493 * Delete a local ref
8494 *
8495 * @param {Object} args
8496 * @param {FsClient} args.fs - a file system implementation
8497 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8498 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8499 * @param {string} args.ref - The ref to delete
8500 *
8501 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
8502 *
8503 * @example
8504 * await git.deleteRef({ fs, dir: '/tutorial', ref: 'refs/tags/test-tag' })
8505 * console.log('done')
8506 *
8507 */
8508async function deleteRef({ fs, dir, gitdir = join(dir, '.git'), ref }) {
8509 try {
8510 assertParameter('fs', fs);
8511 assertParameter('ref', ref);
8512 await GitRefManager.deleteRef({ fs: new FileSystem(fs), gitdir, ref });
8513 } catch (err) {
8514 err.caller = 'git.deleteRef';
8515 throw err
8516 }
8517}
8518
8519// @ts-check
8520
8521/**
8522 * @param {Object} args
8523 * @param {import('../models/FileSystem.js').FileSystem} args.fs
8524 * @param {string} args.gitdir
8525 * @param {string} args.remote
8526 *
8527 * @returns {Promise<void>}
8528 */
8529async function _deleteRemote({ fs, gitdir, remote }) {
8530 const config = await GitConfigManager.get({ fs, gitdir });
8531 await config.deleteSection('remote', remote);
8532 await GitConfigManager.save({ fs, gitdir, config });
8533}
8534
8535// @ts-check
8536
8537/**
8538 * Removes the local config entry for a given remote
8539 *
8540 * @param {Object} args
8541 * @param {FsClient} args.fs - a file system implementation
8542 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8543 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8544 * @param {string} args.remote - The name of the remote to delete
8545 *
8546 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
8547 *
8548 * @example
8549 * await git.deleteRemote({ fs, dir: '/tutorial', remote: 'upstream' })
8550 * console.log('done')
8551 *
8552 */
8553async function deleteRemote({
8554 fs,
8555 dir,
8556 gitdir = join(dir, '.git'),
8557 remote,
8558}) {
8559 try {
8560 assertParameter('fs', fs);
8561 assertParameter('remote', remote);
8562 return await _deleteRemote({
8563 fs: new FileSystem(fs),
8564 gitdir,
8565 remote,
8566 })
8567 } catch (err) {
8568 err.caller = 'git.deleteRemote';
8569 throw err
8570 }
8571}
8572
8573// @ts-check
8574
8575/**
8576 * Delete a local tag ref
8577 *
8578 * @param {Object} args
8579 * @param {import('../models/FileSystem.js').FileSystem} args.fs
8580 * @param {string} args.gitdir
8581 * @param {string} args.ref - The tag to delete
8582 *
8583 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
8584 *
8585 * @example
8586 * await git.deleteTag({ dir: '$input((/))', ref: '$input((test-tag))' })
8587 * console.log('done')
8588 *
8589 */
8590async function _deleteTag({ fs, gitdir, ref }) {
8591 ref = ref.startsWith('refs/tags/') ? ref : `refs/tags/${ref}`;
8592 await GitRefManager.deleteRef({ fs, gitdir, ref });
8593}
8594
8595// @ts-check
8596
8597/**
8598 * Delete a local tag ref
8599 *
8600 * @param {Object} args
8601 * @param {FsClient} args.fs - a file system implementation
8602 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8603 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8604 * @param {string} args.ref - The tag to delete
8605 *
8606 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
8607 *
8608 * @example
8609 * await git.deleteTag({ fs, dir: '/tutorial', ref: 'test-tag' })
8610 * console.log('done')
8611 *
8612 */
8613async function deleteTag({ fs, dir, gitdir = join(dir, '.git'), ref }) {
8614 try {
8615 assertParameter('fs', fs);
8616 assertParameter('ref', ref);
8617 return await _deleteTag({
8618 fs: new FileSystem(fs),
8619 gitdir,
8620 ref,
8621 })
8622 } catch (err) {
8623 err.caller = 'git.deleteTag';
8624 throw err
8625 }
8626}
8627
8628async function expandOidLoose({ fs, gitdir, oid: short }) {
8629 const prefix = short.slice(0, 2);
8630 const objectsSuffixes = await fs.readdir(`${gitdir}/objects/${prefix}`);
8631 return objectsSuffixes
8632 .map(suffix => `${prefix}${suffix}`)
8633 .filter(_oid => _oid.startsWith(short))
8634}
8635
8636async function expandOidPacked({
8637 fs,
8638 cache,
8639 gitdir,
8640 oid: short,
8641 getExternalRefDelta,
8642}) {
8643 // Iterate through all the .pack files
8644 const results = [];
8645 let list = await fs.readdir(join(gitdir, 'objects/pack'));
8646 list = list.filter(x => x.endsWith('.idx'));
8647 for (const filename of list) {
8648 const indexFile = `${gitdir}/objects/pack/${filename}`;
8649 const p = await readPackIndex({
8650 fs,
8651 cache,
8652 filename: indexFile,
8653 getExternalRefDelta,
8654 });
8655 if (p.error) throw new InternalError(p.error)
8656 // Search through the list of oids in the packfile
8657 for (const oid of p.offsets.keys()) {
8658 if (oid.startsWith(short)) results.push(oid);
8659 }
8660 }
8661 return results
8662}
8663
8664async function _expandOid({ fs, cache, gitdir, oid: short }) {
8665 // Curry the current read method so that the packfile un-deltification
8666 // process can acquire external ref-deltas.
8667 const getExternalRefDelta = oid => _readObject({ fs, cache, gitdir, oid });
8668
8669 const results = await expandOidLoose({ fs, gitdir, oid: short });
8670 const packedOids = await expandOidPacked({
8671 fs,
8672 cache,
8673 gitdir,
8674 oid: short,
8675 getExternalRefDelta,
8676 });
8677 // Objects can exist in a pack file as well as loose, make sure we only get a list of unique oids.
8678 for (const packedOid of packedOids) {
8679 if (results.indexOf(packedOid) === -1) {
8680 results.push(packedOid);
8681 }
8682 }
8683
8684 if (results.length === 1) {
8685 return results[0]
8686 }
8687 if (results.length > 1) {
8688 throw new AmbiguousError('oids', short, results)
8689 }
8690 throw new NotFoundError(`an object matching "${short}"`)
8691}
8692
8693// @ts-check
8694
8695/**
8696 * Expand and resolve a short oid into a full oid
8697 *
8698 * @param {Object} args
8699 * @param {FsClient} args.fs - a file system implementation
8700 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8701 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8702 * @param {string} args.oid - The shortened oid prefix to expand (like "0414d2a")
8703 * @param {object} [args.cache] - a [cache](cache.md) object
8704 *
8705 * @returns {Promise<string>} Resolves successfully with the full oid (like "0414d2a286d7bbc7a4a326a61c1f9f888a8ab87f")
8706 *
8707 * @example
8708 * let oid = await git.expandOid({ fs, dir: '/tutorial', oid: '0414d2a'})
8709 * console.log(oid)
8710 *
8711 */
8712async function expandOid({
8713 fs,
8714 dir,
8715 gitdir = join(dir, '.git'),
8716 oid,
8717 cache = {},
8718}) {
8719 try {
8720 assertParameter('fs', fs);
8721 assertParameter('gitdir', gitdir);
8722 assertParameter('oid', oid);
8723 return await _expandOid({
8724 fs: new FileSystem(fs),
8725 cache,
8726 gitdir,
8727 oid,
8728 })
8729 } catch (err) {
8730 err.caller = 'git.expandOid';
8731 throw err
8732 }
8733}
8734
8735// @ts-check
8736
8737/**
8738 * Expand an abbreviated ref to its full name
8739 *
8740 * @param {Object} args
8741 * @param {FsClient} args.fs - a file system implementation
8742 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8743 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8744 * @param {string} args.ref - The ref to expand (like "v1.0.0")
8745 *
8746 * @returns {Promise<string>} Resolves successfully with a full ref name ("refs/tags/v1.0.0")
8747 *
8748 * @example
8749 * let fullRef = await git.expandRef({ fs, dir: '/tutorial', ref: 'main'})
8750 * console.log(fullRef)
8751 *
8752 */
8753async function expandRef({ fs, dir, gitdir = join(dir, '.git'), ref }) {
8754 try {
8755 assertParameter('fs', fs);
8756 assertParameter('gitdir', gitdir);
8757 assertParameter('ref', ref);
8758 return await GitRefManager.expand({
8759 fs: new FileSystem(fs),
8760 gitdir,
8761 ref,
8762 })
8763 } catch (err) {
8764 err.caller = 'git.expandRef';
8765 throw err
8766 }
8767}
8768
8769// @ts-check
8770
8771/**
8772 * @param {object} args
8773 * @param {import('../models/FileSystem.js').FileSystem} args.fs
8774 * @param {any} args.cache
8775 * @param {string} args.gitdir
8776 * @param {string[]} args.oids
8777 *
8778 */
8779async function _findMergeBase({ fs, cache, gitdir, oids }) {
8780 // Note: right now, the tests are geared so that the output should match that of
8781 // `git merge-base --all --octopus`
8782 // because without the --octopus flag, git's output seems to depend on the ORDER of the oids,
8783 // and computing virtual merge bases is just too much for me to fathom right now.
8784
8785 // If we start N independent walkers, one at each of the given `oids`, and walk backwards
8786 // through ancestors, eventually we'll discover a commit where each one of these N walkers
8787 // has passed through. So we just need to keep track of which walkers have visited each commit
8788 // until we find a commit that N distinct walkers has visited.
8789 const visits = {};
8790 const passes = oids.length;
8791 let heads = oids.map((oid, index) => ({ index, oid }));
8792 while (heads.length) {
8793 // Count how many times we've passed each commit
8794 const result = new Set();
8795 for (const { oid, index } of heads) {
8796 if (!visits[oid]) visits[oid] = new Set();
8797 visits[oid].add(index);
8798 if (visits[oid].size === passes) {
8799 result.add(oid);
8800 }
8801 }
8802 if (result.size > 0) {
8803 return [...result]
8804 }
8805 // We haven't found a common ancestor yet
8806 const newheads = new Map();
8807 for (const { oid, index } of heads) {
8808 try {
8809 const { object } = await _readObject({ fs, cache, gitdir, oid });
8810 const commit = GitCommit.from(object);
8811 const { parent } = commit.parseHeaders();
8812 for (const oid of parent) {
8813 if (!visits[oid] || !visits[oid].has(index)) {
8814 newheads.set(oid + ':' + index, { oid, index });
8815 }
8816 }
8817 } catch (err) {
8818 // do nothing
8819 }
8820 }
8821 heads = Array.from(newheads.values());
8822 }
8823 return []
8824}
8825
8826const LINEBREAKS = /^.*(\r?\n|$)/gm;
8827
8828function mergeFile({ branches, contents }) {
8829 const ourName = branches[1];
8830 const theirName = branches[2];
8831
8832 const baseContent = contents[0];
8833 const ourContent = contents[1];
8834 const theirContent = contents[2];
8835
8836 const ours = ourContent.match(LINEBREAKS);
8837 const base = baseContent.match(LINEBREAKS);
8838 const theirs = theirContent.match(LINEBREAKS);
8839
8840 // Here we let the diff3 library do the heavy lifting.
8841 const result = diff3Merge(ours, base, theirs);
8842
8843 const markerSize = 7;
8844
8845 // Here we note whether there are conflicts and format the results
8846 let mergedText = '';
8847 let cleanMerge = true;
8848
8849 for (const item of result) {
8850 if (item.ok) {
8851 mergedText += item.ok.join('');
8852 }
8853 if (item.conflict) {
8854 cleanMerge = false;
8855 mergedText += `${'<'.repeat(markerSize)} ${ourName}\n`;
8856 mergedText += item.conflict.a.join('');
8857
8858 mergedText += `${'='.repeat(markerSize)}\n`;
8859 mergedText += item.conflict.b.join('');
8860 mergedText += `${'>'.repeat(markerSize)} ${theirName}\n`;
8861 }
8862 }
8863 return { cleanMerge, mergedText }
8864}
8865
8866// @ts-check
8867
8868/**
8869 * Create a merged tree
8870 *
8871 * @param {Object} args
8872 * @param {import('../models/FileSystem.js').FileSystem} args.fs
8873 * @param {object} args.cache
8874 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
8875 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
8876 * @param {string} args.ourOid - The SHA-1 object id of our tree
8877 * @param {string} args.baseOid - The SHA-1 object id of the base tree
8878 * @param {string} args.theirOid - The SHA-1 object id of their tree
8879 * @param {string} [args.ourName='ours'] - The name to use in conflicted files for our hunks
8880 * @param {string} [args.baseName='base'] - The name to use in conflicted files (in diff3 format) for the base hunks
8881 * @param {string} [args.theirName='theirs'] - The name to use in conflicted files for their hunks
8882 * @param {boolean} [args.dryRun=false]
8883 * @param {boolean} [args.abortOnConflict=false]
8884 * @param {MergeDriverCallback} [args.mergeDriver]
8885 *
8886 * @returns {Promise<string>} - The SHA-1 object id of the merged tree
8887 *
8888 */
8889async function mergeTree({
8890 fs,
8891 cache,
8892 dir,
8893 gitdir = join(dir, '.git'),
8894 index,
8895 ourOid,
8896 baseOid,
8897 theirOid,
8898 ourName = 'ours',
8899 baseName = 'base',
8900 theirName = 'theirs',
8901 dryRun = false,
8902 abortOnConflict = true,
8903 mergeDriver,
8904}) {
8905 const ourTree = TREE({ ref: ourOid });
8906 const baseTree = TREE({ ref: baseOid });
8907 const theirTree = TREE({ ref: theirOid });
8908
8909 const unmergedFiles = [];
8910 const bothModified = [];
8911 const deleteByUs = [];
8912 const deleteByTheirs = [];
8913
8914 const results = await _walk({
8915 fs,
8916 cache,
8917 dir,
8918 gitdir,
8919 trees: [ourTree, baseTree, theirTree],
8920 map: async function(filepath, [ours, base, theirs]) {
8921 const path = basename(filepath);
8922 // What we did, what they did
8923 const ourChange = await modified(ours, base);
8924 const theirChange = await modified(theirs, base);
8925 switch (`${ourChange}-${theirChange}`) {
8926 case 'false-false': {
8927 return {
8928 mode: await base.mode(),
8929 path,
8930 oid: await base.oid(),
8931 type: await base.type(),
8932 }
8933 }
8934 case 'false-true': {
8935 return theirs
8936 ? {
8937 mode: await theirs.mode(),
8938 path,
8939 oid: await theirs.oid(),
8940 type: await theirs.type(),
8941 }
8942 : undefined
8943 }
8944 case 'true-false': {
8945 return ours
8946 ? {
8947 mode: await ours.mode(),
8948 path,
8949 oid: await ours.oid(),
8950 type: await ours.type(),
8951 }
8952 : undefined
8953 }
8954 case 'true-true': {
8955 // Modifications
8956 if (
8957 ours &&
8958 base &&
8959 theirs &&
8960 (await ours.type()) === 'blob' &&
8961 (await base.type()) === 'blob' &&
8962 (await theirs.type()) === 'blob'
8963 ) {
8964 return mergeBlobs({
8965 fs,
8966 gitdir,
8967 path,
8968 ours,
8969 base,
8970 theirs,
8971 ourName,
8972 baseName,
8973 theirName,
8974 mergeDriver,
8975 }).then(async r => {
8976 if (!r.cleanMerge) {
8977 unmergedFiles.push(filepath);
8978 bothModified.push(filepath);
8979 if (!abortOnConflict) {
8980 const baseOid = await base.oid();
8981 const ourOid = await ours.oid();
8982 const theirOid = await theirs.oid();
8983
8984 index.delete({ filepath });
8985
8986 index.insert({ filepath, oid: baseOid, stage: 1 });
8987 index.insert({ filepath, oid: ourOid, stage: 2 });
8988 index.insert({ filepath, oid: theirOid, stage: 3 });
8989 }
8990 } else if (!abortOnConflict) {
8991 index.insert({ filepath, oid: r.mergeResult.oid, stage: 0 });
8992 }
8993 return r.mergeResult
8994 })
8995 }
8996
8997 // deleted by us
8998 if (
8999 base &&
9000 !ours &&
9001 theirs &&
9002 (await base.type()) === 'blob' &&
9003 (await theirs.type()) === 'blob'
9004 ) {
9005 unmergedFiles.push(filepath);
9006 deleteByUs.push(filepath);
9007 if (!abortOnConflict) {
9008 const baseOid = await base.oid();
9009 const theirOid = await theirs.oid();
9010
9011 index.delete({ filepath });
9012
9013 index.insert({ filepath, oid: baseOid, stage: 1 });
9014 index.insert({ filepath, oid: theirOid, stage: 3 });
9015 }
9016
9017 return {
9018 mode: await theirs.mode(),
9019 oid: await theirs.oid(),
9020 type: 'blob',
9021 path,
9022 }
9023 }
9024
9025 // deleted by theirs
9026 if (
9027 base &&
9028 ours &&
9029 !theirs &&
9030 (await base.type()) === 'blob' &&
9031 (await ours.type()) === 'blob'
9032 ) {
9033 unmergedFiles.push(filepath);
9034 deleteByTheirs.push(filepath);
9035 if (!abortOnConflict) {
9036 const baseOid = await base.oid();
9037 const ourOid = await ours.oid();
9038
9039 index.delete({ filepath });
9040
9041 index.insert({ filepath, oid: baseOid, stage: 1 });
9042 index.insert({ filepath, oid: ourOid, stage: 2 });
9043 }
9044
9045 return {
9046 mode: await ours.mode(),
9047 oid: await ours.oid(),
9048 type: 'blob',
9049 path,
9050 }
9051 }
9052
9053 // deleted by both
9054 if (base && !ours && !theirs && (await base.type()) === 'blob') {
9055 return undefined
9056 }
9057
9058 // all other types of conflicts fail
9059 // TODO: Merge conflicts involving additions
9060 throw new MergeNotSupportedError()
9061 }
9062 }
9063 },
9064 /**
9065 * @param {TreeEntry} [parent]
9066 * @param {Array<TreeEntry>} children
9067 */
9068 reduce:
9069 unmergedFiles.length !== 0 && (!dir || abortOnConflict)
9070 ? undefined
9071 : async (parent, children) => {
9072 const entries = children.filter(Boolean); // remove undefineds
9073
9074 // if the parent was deleted, the children have to go
9075 if (!parent) return
9076
9077 // automatically delete directories if they have been emptied
9078 if (parent && parent.type === 'tree' && entries.length === 0) return
9079
9080 if (entries.length > 0) {
9081 const tree = new GitTree(entries);
9082 const object = tree.toObject();
9083 const oid = await _writeObject({
9084 fs,
9085 gitdir,
9086 type: 'tree',
9087 object,
9088 dryRun,
9089 });
9090 parent.oid = oid;
9091 }
9092 return parent
9093 },
9094 });
9095
9096 if (unmergedFiles.length !== 0) {
9097 if (dir && !abortOnConflict) {
9098 await _walk({
9099 fs,
9100 cache,
9101 dir,
9102 gitdir,
9103 trees: [TREE({ ref: results.oid })],
9104 map: async function(filepath, [entry]) {
9105 const path = `${dir}/${filepath}`;
9106 if ((await entry.type()) === 'blob') {
9107 const mode = await entry.mode();
9108 const content = new TextDecoder().decode(await entry.content());
9109 await fs.write(path, content, { mode });
9110 }
9111 return true
9112 },
9113 });
9114 }
9115 return new MergeConflictError(
9116 unmergedFiles,
9117 bothModified,
9118 deleteByUs,
9119 deleteByTheirs
9120 )
9121 }
9122
9123 return results.oid
9124}
9125
9126/**
9127 *
9128 * @param {Object} args
9129 * @param {import('../models/FileSystem').FileSystem} args.fs
9130 * @param {string} args.gitdir
9131 * @param {string} args.path
9132 * @param {WalkerEntry} args.ours
9133 * @param {WalkerEntry} args.base
9134 * @param {WalkerEntry} args.theirs
9135 * @param {string} [args.ourName]
9136 * @param {string} [args.baseName]
9137 * @param {string} [args.theirName]
9138 * @param {boolean} [args.dryRun = false]
9139 * @param {MergeDriverCallback} [args.mergeDriver]
9140 *
9141 */
9142async function mergeBlobs({
9143 fs,
9144 gitdir,
9145 path,
9146 ours,
9147 base,
9148 theirs,
9149 ourName,
9150 theirName,
9151 baseName,
9152 dryRun,
9153 mergeDriver = mergeFile,
9154}) {
9155 const type = 'blob';
9156 // Compute the new mode.
9157 // Since there are ONLY two valid blob modes ('100755' and '100644') it boils down to this
9158 const mode =
9159 (await base.mode()) === (await ours.mode())
9160 ? await theirs.mode()
9161 : await ours.mode();
9162 // The trivial case: nothing to merge except maybe mode
9163 if ((await ours.oid()) === (await theirs.oid())) {
9164 return {
9165 cleanMerge: true,
9166 mergeResult: { mode, path, oid: await ours.oid(), type },
9167 }
9168 }
9169 // if only one side made oid changes, return that side's oid
9170 if ((await ours.oid()) === (await base.oid())) {
9171 return {
9172 cleanMerge: true,
9173 mergeResult: { mode, path, oid: await theirs.oid(), type },
9174 }
9175 }
9176 if ((await theirs.oid()) === (await base.oid())) {
9177 return {
9178 cleanMerge: true,
9179 mergeResult: { mode, path, oid: await ours.oid(), type },
9180 }
9181 }
9182 // if both sides made changes do a merge
9183 const ourContent = Buffer.from(await ours.content()).toString('utf8');
9184 const baseContent = Buffer.from(await base.content()).toString('utf8');
9185 const theirContent = Buffer.from(await theirs.content()).toString('utf8');
9186 const { mergedText, cleanMerge } = await mergeDriver({
9187 branches: [baseName, ourName, theirName],
9188 contents: [baseContent, ourContent, theirContent],
9189 path,
9190 });
9191 const oid = await _writeObject({
9192 fs,
9193 gitdir,
9194 type: 'blob',
9195 object: Buffer.from(mergedText, 'utf8'),
9196 dryRun,
9197 });
9198
9199 return { cleanMerge, mergeResult: { mode, path, oid, type } }
9200}
9201
9202// @ts-check
9203
9204// import diff3 from 'node-diff3'
9205/**
9206 *
9207 * @typedef {Object} MergeResult - Returns an object with a schema like this:
9208 * @property {string} [oid] - The SHA-1 object id that is now at the head of the branch. Absent only if `dryRun` was specified and `mergeCommit` is true.
9209 * @property {boolean} [alreadyMerged] - True if the branch was already merged so no changes were made
9210 * @property {boolean} [fastForward] - True if it was a fast-forward merge
9211 * @property {boolean} [mergeCommit] - True if merge resulted in a merge commit
9212 * @property {string} [tree] - The SHA-1 object id of the tree resulting from a merge commit
9213 *
9214 */
9215
9216/**
9217 * @param {object} args
9218 * @param {import('../models/FileSystem.js').FileSystem} args.fs
9219 * @param {object} args.cache
9220 * @param {string} args.gitdir
9221 * @param {string} [args.ours]
9222 * @param {string} args.theirs
9223 * @param {boolean} args.fastForward
9224 * @param {boolean} args.fastForwardOnly
9225 * @param {boolean} args.dryRun
9226 * @param {boolean} args.noUpdateBranch
9227 * @param {boolean} args.abortOnConflict
9228 * @param {string} [args.message]
9229 * @param {Object} args.author
9230 * @param {string} args.author.name
9231 * @param {string} args.author.email
9232 * @param {number} args.author.timestamp
9233 * @param {number} args.author.timezoneOffset
9234 * @param {Object} args.committer
9235 * @param {string} args.committer.name
9236 * @param {string} args.committer.email
9237 * @param {number} args.committer.timestamp
9238 * @param {number} args.committer.timezoneOffset
9239 * @param {string} [args.signingKey]
9240 * @param {SignCallback} [args.onSign] - a PGP signing implementation
9241 * @param {MergeDriverCallback} [args.mergeDriver]
9242 *
9243 * @returns {Promise<MergeResult>} Resolves to a description of the merge operation
9244 *
9245 */
9246async function _merge({
9247 fs,
9248 cache,
9249 dir,
9250 gitdir,
9251 ours,
9252 theirs,
9253 fastForward = true,
9254 fastForwardOnly = false,
9255 dryRun = false,
9256 noUpdateBranch = false,
9257 abortOnConflict = true,
9258 message,
9259 author,
9260 committer,
9261 signingKey,
9262 onSign,
9263 mergeDriver,
9264}) {
9265 if (ours === undefined) {
9266 ours = await _currentBranch({ fs, gitdir, fullname: true });
9267 }
9268 ours = await GitRefManager.expand({
9269 fs,
9270 gitdir,
9271 ref: ours,
9272 });
9273 theirs = await GitRefManager.expand({
9274 fs,
9275 gitdir,
9276 ref: theirs,
9277 });
9278 const ourOid = await GitRefManager.resolve({
9279 fs,
9280 gitdir,
9281 ref: ours,
9282 });
9283 const theirOid = await GitRefManager.resolve({
9284 fs,
9285 gitdir,
9286 ref: theirs,
9287 });
9288 // find most recent common ancestor of ref a and ref b
9289 const baseOids = await _findMergeBase({
9290 fs,
9291 cache,
9292 gitdir,
9293 oids: [ourOid, theirOid],
9294 });
9295 if (baseOids.length !== 1) {
9296 // TODO: Recursive Merge strategy
9297 throw new MergeNotSupportedError()
9298 }
9299 const baseOid = baseOids[0];
9300 // handle fast-forward case
9301 if (baseOid === theirOid) {
9302 return {
9303 oid: ourOid,
9304 alreadyMerged: true,
9305 }
9306 }
9307 if (fastForward && baseOid === ourOid) {
9308 if (!dryRun && !noUpdateBranch) {
9309 await GitRefManager.writeRef({ fs, gitdir, ref: ours, value: theirOid });
9310 }
9311 return {
9312 oid: theirOid,
9313 fastForward: true,
9314 }
9315 } else {
9316 // not a simple fast-forward
9317 if (fastForwardOnly) {
9318 throw new FastForwardError()
9319 }
9320 // try a fancier merge
9321 const tree = await GitIndexManager.acquire(
9322 { fs, gitdir, cache, allowUnmerged: false },
9323 async index => {
9324 return mergeTree({
9325 fs,
9326 cache,
9327 dir,
9328 gitdir,
9329 index,
9330 ourOid,
9331 theirOid,
9332 baseOid,
9333 ourName: abbreviateRef(ours),
9334 baseName: 'base',
9335 theirName: abbreviateRef(theirs),
9336 dryRun,
9337 abortOnConflict,
9338 mergeDriver,
9339 })
9340 }
9341 );
9342
9343 // Defer throwing error until the index lock is relinquished and index is
9344 // written to filsesystem
9345 if (tree instanceof MergeConflictError) throw tree
9346
9347 if (!message) {
9348 message = `Merge branch '${abbreviateRef(theirs)}' into ${abbreviateRef(
9349 ours
9350 )}`;
9351 }
9352 const oid = await _commit({
9353 fs,
9354 cache,
9355 gitdir,
9356 message,
9357 ref: ours,
9358 tree,
9359 parent: [ourOid, theirOid],
9360 author,
9361 committer,
9362 signingKey,
9363 onSign,
9364 dryRun,
9365 noUpdateBranch,
9366 });
9367 return {
9368 oid,
9369 tree,
9370 mergeCommit: true,
9371 }
9372 }
9373}
9374
9375// @ts-check
9376
9377/**
9378 * @param {object} args
9379 * @param {import('../models/FileSystem.js').FileSystem} args.fs
9380 * @param {object} args.cache
9381 * @param {HttpClient} args.http
9382 * @param {ProgressCallback} [args.onProgress]
9383 * @param {MessageCallback} [args.onMessage]
9384 * @param {AuthCallback} [args.onAuth]
9385 * @param {AuthFailureCallback} [args.onAuthFailure]
9386 * @param {AuthSuccessCallback} [args.onAuthSuccess]
9387 * @param {string} args.dir
9388 * @param {string} args.gitdir
9389 * @param {string} args.ref
9390 * @param {string} [args.url]
9391 * @param {string} [args.remote]
9392 * @param {string} [args.remoteRef]
9393 * @param {boolean} [args.prune]
9394 * @param {boolean} [args.pruneTags]
9395 * @param {string} [args.corsProxy]
9396 * @param {boolean} args.singleBranch
9397 * @param {boolean} args.fastForward
9398 * @param {boolean} args.fastForwardOnly
9399 * @param {Object<string, string>} [args.headers]
9400 * @param {Object} args.author
9401 * @param {string} args.author.name
9402 * @param {string} args.author.email
9403 * @param {number} args.author.timestamp
9404 * @param {number} args.author.timezoneOffset
9405 * @param {Object} args.committer
9406 * @param {string} args.committer.name
9407 * @param {string} args.committer.email
9408 * @param {number} args.committer.timestamp
9409 * @param {number} args.committer.timezoneOffset
9410 * @param {string} [args.signingKey]
9411 *
9412 * @returns {Promise<void>} Resolves successfully when pull operation completes
9413 *
9414 */
9415async function _pull({
9416 fs,
9417 cache,
9418 http,
9419 onProgress,
9420 onMessage,
9421 onAuth,
9422 onAuthSuccess,
9423 onAuthFailure,
9424 dir,
9425 gitdir,
9426 ref,
9427 url,
9428 remote,
9429 remoteRef,
9430 prune,
9431 pruneTags,
9432 fastForward,
9433 fastForwardOnly,
9434 corsProxy,
9435 singleBranch,
9436 headers,
9437 author,
9438 committer,
9439 signingKey,
9440}) {
9441 try {
9442 // If ref is undefined, use 'HEAD'
9443 if (!ref) {
9444 const head = await _currentBranch({ fs, gitdir });
9445 // TODO: use a better error.
9446 if (!head) {
9447 throw new MissingParameterError('ref')
9448 }
9449 ref = head;
9450 }
9451
9452 const { fetchHead, fetchHeadDescription } = await _fetch({
9453 fs,
9454 cache,
9455 http,
9456 onProgress,
9457 onMessage,
9458 onAuth,
9459 onAuthSuccess,
9460 onAuthFailure,
9461 gitdir,
9462 corsProxy,
9463 ref,
9464 url,
9465 remote,
9466 remoteRef,
9467 singleBranch,
9468 headers,
9469 prune,
9470 pruneTags,
9471 });
9472 // Merge the remote tracking branch into the local one.
9473 await _merge({
9474 fs,
9475 cache,
9476 gitdir,
9477 ours: ref,
9478 theirs: fetchHead,
9479 fastForward,
9480 fastForwardOnly,
9481 message: `Merge ${fetchHeadDescription}`,
9482 author,
9483 committer,
9484 signingKey,
9485 dryRun: false,
9486 noUpdateBranch: false,
9487 });
9488 await _checkout({
9489 fs,
9490 cache,
9491 onProgress,
9492 dir,
9493 gitdir,
9494 ref,
9495 remote,
9496 noCheckout: false,
9497 });
9498 } catch (err) {
9499 err.caller = 'git.pull';
9500 throw err
9501 }
9502}
9503
9504// @ts-check
9505
9506/**
9507 * Like `pull`, but hard-coded with `fastForward: true` so there is no need for an `author` parameter.
9508 *
9509 * @param {object} args
9510 * @param {FsClient} args.fs - a file system client
9511 * @param {HttpClient} args.http - an HTTP client
9512 * @param {ProgressCallback} [args.onProgress] - optional progress event callback
9513 * @param {MessageCallback} [args.onMessage] - optional message event callback
9514 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
9515 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
9516 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
9517 * @param {string} args.dir] - The [working tree](dir-vs-gitdir.md) directory path
9518 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
9519 * @param {string} [args.ref] - Which branch to merge into. By default this is the currently checked out branch.
9520 * @param {string} [args.url] - (Added in 1.1.0) The URL of the remote repository. The default is the value set in the git config for that remote.
9521 * @param {string} [args.remote] - (Added in 1.1.0) If URL is not specified, determines which remote to use.
9522 * @param {string} [args.remoteRef] - (Added in 1.1.0) The name of the branch on the remote to fetch. By default this is the configured remote tracking branch.
9523 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Overrides value in repo config.
9524 * @param {boolean} [args.singleBranch = false] - Instead of the default behavior of fetching all the branches, only fetch a single branch.
9525 * @param {Object<string, string>} [args.headers] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
9526 * @param {object} [args.cache] - a [cache](cache.md) object
9527 *
9528 * @returns {Promise<void>} Resolves successfully when pull operation completes
9529 *
9530 * @example
9531 * await git.fastForward({
9532 * fs,
9533 * http,
9534 * dir: '/tutorial',
9535 * ref: 'main',
9536 * singleBranch: true
9537 * })
9538 * console.log('done')
9539 *
9540 */
9541async function fastForward({
9542 fs,
9543 http,
9544 onProgress,
9545 onMessage,
9546 onAuth,
9547 onAuthSuccess,
9548 onAuthFailure,
9549 dir,
9550 gitdir = join(dir, '.git'),
9551 ref,
9552 url,
9553 remote,
9554 remoteRef,
9555 corsProxy,
9556 singleBranch,
9557 headers = {},
9558 cache = {},
9559}) {
9560 try {
9561 assertParameter('fs', fs);
9562 assertParameter('http', http);
9563 assertParameter('gitdir', gitdir);
9564
9565 const thisWillNotBeUsed = {
9566 name: '',
9567 email: '',
9568 timestamp: Date.now(),
9569 timezoneOffset: 0,
9570 };
9571
9572 return await _pull({
9573 fs: new FileSystem(fs),
9574 cache,
9575 http,
9576 onProgress,
9577 onMessage,
9578 onAuth,
9579 onAuthSuccess,
9580 onAuthFailure,
9581 dir,
9582 gitdir,
9583 ref,
9584 url,
9585 remote,
9586 remoteRef,
9587 fastForwardOnly: true,
9588 corsProxy,
9589 singleBranch,
9590 headers,
9591 author: thisWillNotBeUsed,
9592 committer: thisWillNotBeUsed,
9593 })
9594 } catch (err) {
9595 err.caller = 'git.fastForward';
9596 throw err
9597 }
9598}
9599
9600// @ts-check
9601
9602/**
9603 *
9604 * @typedef {object} FetchResult - The object returned has the following schema:
9605 * @property {string | null} defaultBranch - The branch that is cloned if no branch is specified
9606 * @property {string | null} fetchHead - The SHA-1 object id of the fetched head commit
9607 * @property {string | null} fetchHeadDescription - a textual description of the branch that was fetched
9608 * @property {Object<string, string>} [headers] - The HTTP response headers returned by the git server
9609 * @property {string[]} [pruned] - A list of branches that were pruned, if you provided the `prune` parameter
9610 *
9611 */
9612
9613/**
9614 * Fetch commits from a remote repository
9615 *
9616 * @param {object} args
9617 * @param {FsClient} args.fs - a file system client
9618 * @param {HttpClient} args.http - an HTTP client
9619 * @param {ProgressCallback} [args.onProgress] - optional progress event callback
9620 * @param {MessageCallback} [args.onMessage] - optional message event callback
9621 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
9622 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
9623 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
9624 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
9625 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
9626 * @param {string} [args.url] - The URL of the remote repository. The default is the value set in the git config for that remote.
9627 * @param {string} [args.remote] - If URL is not specified, determines which remote to use.
9628 * @param {boolean} [args.singleBranch = false] - Instead of the default behavior of fetching all the branches, only fetch a single branch.
9629 * @param {string} [args.ref] - Which branch to fetch if `singleBranch` is true. By default this is the current branch or the remote's default branch.
9630 * @param {string} [args.remoteRef] - The name of the branch on the remote to fetch if `singleBranch` is true. By default this is the configured remote tracking branch.
9631 * @param {boolean} [args.tags = false] - Also fetch tags
9632 * @param {number} [args.depth] - Integer. Determines how much of the git repository's history to retrieve
9633 * @param {boolean} [args.relative = false] - Changes the meaning of `depth` to be measured from the current shallow depth rather than from the branch tip.
9634 * @param {Date} [args.since] - Only fetch commits created after the given date. Mutually exclusive with `depth`.
9635 * @param {string[]} [args.exclude = []] - A list of branches or tags. Instructs the remote server not to send us any commits reachable from these refs.
9636 * @param {boolean} [args.prune = false] - Delete local remote-tracking branches that are not present on the remote
9637 * @param {boolean} [args.pruneTags = false] - Prune local tags that don’t exist on the remote, and force-update those tags that differ
9638 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Overrides value in repo config.
9639 * @param {Object<string, string>} [args.headers] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
9640 * @param {object} [args.cache] - a [cache](cache.md) object
9641 *
9642 * @returns {Promise<FetchResult>} Resolves successfully when fetch completes
9643 * @see FetchResult
9644 *
9645 * @example
9646 * let result = await git.fetch({
9647 * fs,
9648 * http,
9649 * dir: '/tutorial',
9650 * corsProxy: 'https://cors.isomorphic-git.org',
9651 * url: 'https://github.com/isomorphic-git/isomorphic-git',
9652 * ref: 'main',
9653 * depth: 1,
9654 * singleBranch: true,
9655 * tags: false
9656 * })
9657 * console.log(result)
9658 *
9659 */
9660async function fetch({
9661 fs,
9662 http,
9663 onProgress,
9664 onMessage,
9665 onAuth,
9666 onAuthSuccess,
9667 onAuthFailure,
9668 dir,
9669 gitdir = join(dir, '.git'),
9670 ref,
9671 remote,
9672 remoteRef,
9673 url,
9674 corsProxy,
9675 depth = null,
9676 since = null,
9677 exclude = [],
9678 relative = false,
9679 tags = false,
9680 singleBranch = false,
9681 headers = {},
9682 prune = false,
9683 pruneTags = false,
9684 cache = {},
9685}) {
9686 try {
9687 assertParameter('fs', fs);
9688 assertParameter('http', http);
9689 assertParameter('gitdir', gitdir);
9690
9691 return await _fetch({
9692 fs: new FileSystem(fs),
9693 cache,
9694 http,
9695 onProgress,
9696 onMessage,
9697 onAuth,
9698 onAuthSuccess,
9699 onAuthFailure,
9700 gitdir,
9701 ref,
9702 remote,
9703 remoteRef,
9704 url,
9705 corsProxy,
9706 depth,
9707 since,
9708 exclude,
9709 relative,
9710 tags,
9711 singleBranch,
9712 headers,
9713 prune,
9714 pruneTags,
9715 })
9716 } catch (err) {
9717 err.caller = 'git.fetch';
9718 throw err
9719 }
9720}
9721
9722// @ts-check
9723
9724/**
9725 * Find the merge base for a set of commits
9726 *
9727 * @param {object} args
9728 * @param {FsClient} args.fs - a file system client
9729 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
9730 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
9731 * @param {string[]} args.oids - Which commits
9732 * @param {object} [args.cache] - a [cache](cache.md) object
9733 *
9734 */
9735async function findMergeBase({
9736 fs,
9737 dir,
9738 gitdir = join(dir, '.git'),
9739 oids,
9740 cache = {},
9741}) {
9742 try {
9743 assertParameter('fs', fs);
9744 assertParameter('gitdir', gitdir);
9745 assertParameter('oids', oids);
9746
9747 return await _findMergeBase({
9748 fs: new FileSystem(fs),
9749 cache,
9750 gitdir,
9751 oids,
9752 })
9753 } catch (err) {
9754 err.caller = 'git.findMergeBase';
9755 throw err
9756 }
9757}
9758
9759// @ts-check
9760
9761/**
9762 * Find the root git directory
9763 *
9764 * Starting at `filepath`, walks upward until it finds a directory that contains a subdirectory called '.git'.
9765 *
9766 * @param {Object} args
9767 * @param {import('../models/FileSystem.js').FileSystem} args.fs
9768 * @param {string} args.filepath
9769 *
9770 * @returns {Promise<string>} Resolves successfully with a root git directory path
9771 */
9772async function _findRoot({ fs, filepath }) {
9773 if (await fs.exists(join(filepath, '.git'))) {
9774 return filepath
9775 } else {
9776 const parent = dirname(filepath);
9777 if (parent === filepath) {
9778 throw new NotFoundError(`git root for ${filepath}`)
9779 }
9780 return _findRoot({ fs, filepath: parent })
9781 }
9782}
9783
9784// @ts-check
9785
9786/**
9787 * Find the root git directory
9788 *
9789 * Starting at `filepath`, walks upward until it finds a directory that contains a subdirectory called '.git'.
9790 *
9791 * @param {Object} args
9792 * @param {FsClient} args.fs - a file system client
9793 * @param {string} args.filepath - The file directory to start searching in.
9794 *
9795 * @returns {Promise<string>} Resolves successfully with a root git directory path
9796 * @throws {NotFoundError}
9797 *
9798 * @example
9799 * let gitroot = await git.findRoot({
9800 * fs,
9801 * filepath: '/tutorial/src/utils'
9802 * })
9803 * console.log(gitroot)
9804 *
9805 */
9806async function findRoot({ fs, filepath }) {
9807 try {
9808 assertParameter('fs', fs);
9809 assertParameter('filepath', filepath);
9810
9811 return await _findRoot({ fs: new FileSystem(fs), filepath })
9812 } catch (err) {
9813 err.caller = 'git.findRoot';
9814 throw err
9815 }
9816}
9817
9818// @ts-check
9819
9820/**
9821 * Read an entry from the git config files.
9822 *
9823 * *Caveats:*
9824 * - Currently only the local `$GIT_DIR/config` file can be read or written. However support for the global `~/.gitconfig` and system `$(prefix)/etc/gitconfig` will be added in the future.
9825 * - The current parser does not support the more exotic features of the git-config file format such as `[include]` and `[includeIf]`.
9826 *
9827 * @param {Object} args
9828 * @param {FsClient} args.fs - a file system implementation
9829 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
9830 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
9831 * @param {string} args.path - The key of the git config entry
9832 *
9833 * @returns {Promise<any>} Resolves with the config value
9834 *
9835 * @example
9836 * // Read config value
9837 * let value = await git.getConfig({
9838 * fs,
9839 * dir: '/tutorial',
9840 * path: 'remote.origin.url'
9841 * })
9842 * console.log(value)
9843 *
9844 */
9845async function getConfig({ fs, dir, gitdir = join(dir, '.git'), path }) {
9846 try {
9847 assertParameter('fs', fs);
9848 assertParameter('gitdir', gitdir);
9849 assertParameter('path', path);
9850
9851 return await _getConfig({
9852 fs: new FileSystem(fs),
9853 gitdir,
9854 path,
9855 })
9856 } catch (err) {
9857 err.caller = 'git.getConfig';
9858 throw err
9859 }
9860}
9861
9862// @ts-check
9863
9864/**
9865 * @param {Object} args
9866 * @param {import('../models/FileSystem.js').FileSystem} args.fs
9867 * @param {string} args.gitdir
9868 * @param {string} args.path
9869 *
9870 * @returns {Promise<Array<any>>} Resolves with an array of the config value
9871 *
9872 */
9873async function _getConfigAll({ fs, gitdir, path }) {
9874 const config = await GitConfigManager.get({ fs, gitdir });
9875 return config.getall(path)
9876}
9877
9878// @ts-check
9879
9880/**
9881 * Read a multi-valued entry from the git config files.
9882 *
9883 * *Caveats:*
9884 * - Currently only the local `$GIT_DIR/config` file can be read or written. However support for the global `~/.gitconfig` and system `$(prefix)/etc/gitconfig` will be added in the future.
9885 * - The current parser does not support the more exotic features of the git-config file format such as `[include]` and `[includeIf]`.
9886 *
9887 * @param {Object} args
9888 * @param {FsClient} args.fs - a file system implementation
9889 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
9890 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
9891 * @param {string} args.path - The key of the git config entry
9892 *
9893 * @returns {Promise<Array<any>>} Resolves with the config value
9894 *
9895 */
9896async function getConfigAll({
9897 fs,
9898 dir,
9899 gitdir = join(dir, '.git'),
9900 path,
9901}) {
9902 try {
9903 assertParameter('fs', fs);
9904 assertParameter('gitdir', gitdir);
9905 assertParameter('path', path);
9906
9907 return await _getConfigAll({
9908 fs: new FileSystem(fs),
9909 gitdir,
9910 path,
9911 })
9912 } catch (err) {
9913 err.caller = 'git.getConfigAll';
9914 throw err
9915 }
9916}
9917
9918// @ts-check
9919
9920/**
9921 *
9922 * @typedef {Object} GetRemoteInfoResult - The object returned has the following schema:
9923 * @property {string[]} capabilities - The list of capabilities returned by the server (part of the Git protocol)
9924 * @property {Object} [refs]
9925 * @property {string} [HEAD] - The default branch of the remote
9926 * @property {Object<string, string>} [refs.heads] - The branches on the remote
9927 * @property {Object<string, string>} [refs.pull] - The special branches representing pull requests (non-standard)
9928 * @property {Object<string, string>} [refs.tags] - The tags on the remote
9929 *
9930 */
9931
9932/**
9933 * List a remote servers branches, tags, and capabilities.
9934 *
9935 * This is a rare command that doesn't require an `fs`, `dir`, or even `gitdir` argument.
9936 * It just communicates to a remote git server, using the first step of the `git-upload-pack` handshake, but stopping short of fetching the packfile.
9937 *
9938 * @param {object} args
9939 * @param {HttpClient} args.http - an HTTP client
9940 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
9941 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
9942 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
9943 * @param {string} args.url - The URL of the remote repository. Will be gotten from gitconfig if absent.
9944 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Overrides value in repo config.
9945 * @param {boolean} [args.forPush = false] - By default, the command queries the 'fetch' capabilities. If true, it will ask for the 'push' capabilities.
9946 * @param {Object<string, string>} [args.headers] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
9947 *
9948 * @returns {Promise<GetRemoteInfoResult>} Resolves successfully with an object listing the branches, tags, and capabilities of the remote.
9949 * @see GetRemoteInfoResult
9950 *
9951 * @example
9952 * let info = await git.getRemoteInfo({
9953 * http,
9954 * url:
9955 * "https://cors.isomorphic-git.org/github.com/isomorphic-git/isomorphic-git.git"
9956 * });
9957 * console.log(info);
9958 *
9959 */
9960async function getRemoteInfo({
9961 http,
9962 onAuth,
9963 onAuthSuccess,
9964 onAuthFailure,
9965 corsProxy,
9966 url,
9967 headers = {},
9968 forPush = false,
9969}) {
9970 try {
9971 assertParameter('http', http);
9972 assertParameter('url', url);
9973
9974 const GitRemoteHTTP = GitRemoteManager.getRemoteHelperFor({ url });
9975 const remote = await GitRemoteHTTP.discover({
9976 http,
9977 onAuth,
9978 onAuthSuccess,
9979 onAuthFailure,
9980 corsProxy,
9981 service: forPush ? 'git-receive-pack' : 'git-upload-pack',
9982 url,
9983 headers,
9984 protocolVersion: 1,
9985 });
9986
9987 // Note: remote.capabilities, remote.refs, and remote.symrefs are Set and Map objects,
9988 // but one of the objectives of the public API is to always return JSON-compatible objects
9989 // so we must JSONify them.
9990 const result = {
9991 capabilities: [...remote.capabilities],
9992 };
9993 // Convert the flat list into an object tree, because I figure 99% of the time
9994 // that will be easier to use.
9995 for (const [ref, oid] of remote.refs) {
9996 const parts = ref.split('/');
9997 const last = parts.pop();
9998 let o = result;
9999 for (const part of parts) {
10000 o[part] = o[part] || {};
10001 o = o[part];
10002 }
10003 o[last] = oid;
10004 }
10005 // Merge symrefs on top of refs to more closely match actual git repo layouts
10006 for (const [symref, ref] of remote.symrefs) {
10007 const parts = symref.split('/');
10008 const last = parts.pop();
10009 let o = result;
10010 for (const part of parts) {
10011 o[part] = o[part] || {};
10012 o = o[part];
10013 }
10014 o[last] = ref;
10015 }
10016 return result
10017 } catch (err) {
10018 err.caller = 'git.getRemoteInfo';
10019 throw err
10020 }
10021}
10022
10023// @ts-check
10024
10025/**
10026 * @param {any} remote
10027 * @param {string} prefix
10028 * @param {boolean} symrefs
10029 * @param {boolean} peelTags
10030 * @returns {ServerRef[]}
10031 */
10032function formatInfoRefs(remote, prefix, symrefs, peelTags) {
10033 const refs = [];
10034 for (const [key, value] of remote.refs) {
10035 if (prefix && !key.startsWith(prefix)) continue
10036
10037 if (key.endsWith('^{}')) {
10038 if (peelTags) {
10039 const _key = key.replace('^{}', '');
10040 // Peeled tags are almost always listed immediately after the original tag
10041 const last = refs[refs.length - 1];
10042 const r = last.ref === _key ? last : refs.find(x => x.ref === _key);
10043 if (r === undefined) {
10044 throw new Error('I did not expect this to happen')
10045 }
10046 r.peeled = value;
10047 }
10048 continue
10049 }
10050 /** @type ServerRef */
10051 const ref = { ref: key, oid: value };
10052 if (symrefs) {
10053 if (remote.symrefs.has(key)) {
10054 ref.target = remote.symrefs.get(key);
10055 }
10056 }
10057 refs.push(ref);
10058 }
10059 return refs
10060}
10061
10062// @ts-check
10063
10064/**
10065 * @typedef {Object} GetRemoteInfo2Result - This object has the following schema:
10066 * @property {1 | 2} protocolVersion - Git protocol version the server supports
10067 * @property {Object<string, string | true>} capabilities - An object of capabilities represented as keys and values
10068 * @property {ServerRef[]} [refs] - Server refs (they get returned by protocol version 1 whether you want them or not)
10069 */
10070
10071/**
10072 * List a remote server's capabilities.
10073 *
10074 * This is a rare command that doesn't require an `fs`, `dir`, or even `gitdir` argument.
10075 * It just communicates to a remote git server, determining what protocol version, commands, and features it supports.
10076 *
10077 * > The successor to [`getRemoteInfo`](./getRemoteInfo.md), this command supports Git Wire Protocol Version 2.
10078 * > Therefore its return type is more complicated as either:
10079 * >
10080 * > - v1 capabilities (and refs) or
10081 * > - v2 capabilities (and no refs)
10082 * >
10083 * > are returned.
10084 * > If you just care about refs, use [`listServerRefs`](./listServerRefs.md)
10085 *
10086 * @param {object} args
10087 * @param {HttpClient} args.http - an HTTP client
10088 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
10089 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
10090 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
10091 * @param {string} args.url - The URL of the remote repository. Will be gotten from gitconfig if absent.
10092 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Overrides value in repo config.
10093 * @param {boolean} [args.forPush = false] - By default, the command queries the 'fetch' capabilities. If true, it will ask for the 'push' capabilities.
10094 * @param {Object<string, string>} [args.headers] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
10095 * @param {1 | 2} [args.protocolVersion = 2] - Which version of the Git Protocol to use.
10096 *
10097 * @returns {Promise<GetRemoteInfo2Result>} Resolves successfully with an object listing the capabilities of the remote.
10098 * @see GetRemoteInfo2Result
10099 * @see ServerRef
10100 *
10101 * @example
10102 * let info = await git.getRemoteInfo2({
10103 * http,
10104 * corsProxy: "https://cors.isomorphic-git.org",
10105 * url: "https://github.com/isomorphic-git/isomorphic-git.git"
10106 * });
10107 * console.log(info);
10108 *
10109 */
10110async function getRemoteInfo2({
10111 http,
10112 onAuth,
10113 onAuthSuccess,
10114 onAuthFailure,
10115 corsProxy,
10116 url,
10117 headers = {},
10118 forPush = false,
10119 protocolVersion = 2,
10120}) {
10121 try {
10122 assertParameter('http', http);
10123 assertParameter('url', url);
10124
10125 const GitRemoteHTTP = GitRemoteManager.getRemoteHelperFor({ url });
10126 const remote = await GitRemoteHTTP.discover({
10127 http,
10128 onAuth,
10129 onAuthSuccess,
10130 onAuthFailure,
10131 corsProxy,
10132 service: forPush ? 'git-receive-pack' : 'git-upload-pack',
10133 url,
10134 headers,
10135 protocolVersion,
10136 });
10137
10138 if (remote.protocolVersion === 2) {
10139 /** @type GetRemoteInfo2Result */
10140 return {
10141 protocolVersion: remote.protocolVersion,
10142 capabilities: remote.capabilities2,
10143 }
10144 }
10145
10146 // Note: remote.capabilities, remote.refs, and remote.symrefs are Set and Map objects,
10147 // but one of the objectives of the public API is to always return JSON-compatible objects
10148 // so we must JSONify them.
10149 /** @type Object<string, true> */
10150 const capabilities = {};
10151 for (const cap of remote.capabilities) {
10152 const [key, value] = cap.split('=');
10153 if (value) {
10154 capabilities[key] = value;
10155 } else {
10156 capabilities[key] = true;
10157 }
10158 }
10159 /** @type GetRemoteInfo2Result */
10160 return {
10161 protocolVersion: 1,
10162 capabilities,
10163 refs: formatInfoRefs(remote, undefined, true, true),
10164 }
10165 } catch (err) {
10166 err.caller = 'git.getRemoteInfo2';
10167 throw err
10168 }
10169}
10170
10171async function hashObject({
10172 type,
10173 object,
10174 format = 'content',
10175 oid = undefined,
10176}) {
10177 if (format !== 'deflated') {
10178 if (format !== 'wrapped') {
10179 object = GitObject.wrap({ type, object });
10180 }
10181 oid = await shasum(object);
10182 }
10183 return { oid, object }
10184}
10185
10186// @ts-check
10187
10188/**
10189 *
10190 * @typedef {object} HashBlobResult - The object returned has the following schema:
10191 * @property {string} oid - The SHA-1 object id
10192 * @property {'blob'} type - The type of the object
10193 * @property {Uint8Array} object - The wrapped git object (the thing that is hashed)
10194 * @property {'wrapped'} format - The format of the object
10195 *
10196 */
10197
10198/**
10199 * Compute what the SHA-1 object id of a file would be
10200 *
10201 * @param {object} args
10202 * @param {Uint8Array|string} args.object - The object to write. If `object` is a String then it will be converted to a Uint8Array using UTF-8 encoding.
10203 *
10204 * @returns {Promise<HashBlobResult>} Resolves successfully with the SHA-1 object id and the wrapped object Uint8Array.
10205 * @see HashBlobResult
10206 *
10207 * @example
10208 * let { oid, type, object, format } = await git.hashBlob({
10209 * object: 'Hello world!',
10210 * })
10211 *
10212 * console.log('oid', oid)
10213 * console.log('type', type)
10214 * console.log('object', object)
10215 * console.log('format', format)
10216 *
10217 */
10218async function hashBlob({ object }) {
10219 try {
10220 assertParameter('object', object);
10221
10222 // Convert object to buffer
10223 if (typeof object === 'string') {
10224 object = Buffer.from(object, 'utf8');
10225 } else {
10226 object = Buffer.from(object);
10227 }
10228
10229 const type = 'blob';
10230 const { oid, object: _object } = await hashObject({
10231 type: 'blob',
10232 format: 'content',
10233 object,
10234 });
10235 return { oid, type, object: new Uint8Array(_object), format: 'wrapped' }
10236 } catch (err) {
10237 err.caller = 'git.hashBlob';
10238 throw err
10239 }
10240}
10241
10242// @ts-check
10243
10244/**
10245 * @param {object} args
10246 * @param {import('../models/FileSystem.js').FileSystem} args.fs
10247 * @param {any} args.cache
10248 * @param {ProgressCallback} [args.onProgress]
10249 * @param {string} args.dir
10250 * @param {string} args.gitdir
10251 * @param {string} args.filepath
10252 *
10253 * @returns {Promise<{oids: string[]}>}
10254 */
10255async function _indexPack({
10256 fs,
10257 cache,
10258 onProgress,
10259 dir,
10260 gitdir,
10261 filepath,
10262}) {
10263 try {
10264 filepath = join(dir, filepath);
10265 const pack = await fs.read(filepath);
10266 const getExternalRefDelta = oid => _readObject({ fs, cache, gitdir, oid });
10267 const idx = await GitPackIndex.fromPack({
10268 pack,
10269 getExternalRefDelta,
10270 onProgress,
10271 });
10272 await fs.write(filepath.replace(/\.pack$/, '.idx'), await idx.toBuffer());
10273 return {
10274 oids: [...idx.hashes],
10275 }
10276 } catch (err) {
10277 err.caller = 'git.indexPack';
10278 throw err
10279 }
10280}
10281
10282// @ts-check
10283
10284/**
10285 * Create the .idx file for a given .pack file
10286 *
10287 * @param {object} args
10288 * @param {FsClient} args.fs - a file system client
10289 * @param {ProgressCallback} [args.onProgress] - optional progress event callback
10290 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
10291 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10292 * @param {string} args.filepath - The path to the .pack file to index
10293 * @param {object} [args.cache] - a [cache](cache.md) object
10294 *
10295 * @returns {Promise<{oids: string[]}>} Resolves with a list of the SHA-1 object ids contained in the packfile
10296 *
10297 * @example
10298 * let packfiles = await fs.promises.readdir('/tutorial/.git/objects/pack')
10299 * packfiles = packfiles.filter(name => name.endsWith('.pack'))
10300 * console.log('packfiles', packfiles)
10301 *
10302 * const { oids } = await git.indexPack({
10303 * fs,
10304 * dir: '/tutorial',
10305 * filepath: `.git/objects/pack/${packfiles[0]}`,
10306 * async onProgress (evt) {
10307 * console.log(`${evt.phase}: ${evt.loaded} / ${evt.total}`)
10308 * }
10309 * })
10310 * console.log(oids)
10311 *
10312 */
10313async function indexPack({
10314 fs,
10315 onProgress,
10316 dir,
10317 gitdir = join(dir, '.git'),
10318 filepath,
10319 cache = {},
10320}) {
10321 try {
10322 assertParameter('fs', fs);
10323 assertParameter('dir', dir);
10324 assertParameter('gitdir', dir);
10325 assertParameter('filepath', filepath);
10326
10327 return await _indexPack({
10328 fs: new FileSystem(fs),
10329 cache,
10330 onProgress,
10331 dir,
10332 gitdir,
10333 filepath,
10334 })
10335 } catch (err) {
10336 err.caller = 'git.indexPack';
10337 throw err
10338 }
10339}
10340
10341// @ts-check
10342
10343/**
10344 * Initialize a new repository
10345 *
10346 * @param {object} args
10347 * @param {FsClient} args.fs - a file system client
10348 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
10349 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10350 * @param {boolean} [args.bare = false] - Initialize a bare repository
10351 * @param {string} [args.defaultBranch = 'master'] - The name of the default branch (might be changed to a required argument in 2.0.0)
10352 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
10353 *
10354 * @example
10355 * await git.init({ fs, dir: '/tutorial' })
10356 * console.log('done')
10357 *
10358 */
10359async function init({
10360 fs,
10361 bare = false,
10362 dir,
10363 gitdir = bare ? dir : join(dir, '.git'),
10364 defaultBranch = 'master',
10365}) {
10366 try {
10367 assertParameter('fs', fs);
10368 assertParameter('gitdir', gitdir);
10369 if (!bare) {
10370 assertParameter('dir', dir);
10371 }
10372
10373 return await _init({
10374 fs: new FileSystem(fs),
10375 bare,
10376 dir,
10377 gitdir,
10378 defaultBranch,
10379 })
10380 } catch (err) {
10381 err.caller = 'git.init';
10382 throw err
10383 }
10384}
10385
10386// @ts-check
10387
10388/**
10389 * @param {object} args
10390 * @param {import('../models/FileSystem.js').FileSystem} args.fs
10391 * @param {any} args.cache
10392 * @param {string} args.gitdir
10393 * @param {string} args.oid
10394 * @param {string} args.ancestor
10395 * @param {number} args.depth - Maximum depth to search before giving up. -1 means no maximum depth.
10396 *
10397 * @returns {Promise<boolean>}
10398 */
10399async function _isDescendent({
10400 fs,
10401 cache,
10402 gitdir,
10403 oid,
10404 ancestor,
10405 depth,
10406}) {
10407 const shallows = await GitShallowManager.read({ fs, gitdir });
10408 if (!oid) {
10409 throw new MissingParameterError('oid')
10410 }
10411 if (!ancestor) {
10412 throw new MissingParameterError('ancestor')
10413 }
10414 // If you don't like this behavior, add your own check.
10415 // Edge cases are hard to define a perfect solution.
10416 if (oid === ancestor) return false
10417 // We do not use recursion here, because that would lead to depth-first traversal,
10418 // and we want to maintain a breadth-first traversal to avoid hitting shallow clone depth cutoffs.
10419 const queue = [oid];
10420 const visited = new Set();
10421 let searchdepth = 0;
10422 while (queue.length) {
10423 if (searchdepth++ === depth) {
10424 throw new MaxDepthError(depth)
10425 }
10426 const oid = queue.shift();
10427 const { type, object } = await _readObject({
10428 fs,
10429 cache,
10430 gitdir,
10431 oid,
10432 });
10433 if (type !== 'commit') {
10434 throw new ObjectTypeError(oid, type, 'commit')
10435 }
10436 const commit = GitCommit.from(object).parse();
10437 // Are any of the parents the sought-after ancestor?
10438 for (const parent of commit.parent) {
10439 if (parent === ancestor) return true
10440 }
10441 // If not, add them to heads (unless we know this is a shallow commit)
10442 if (!shallows.has(oid)) {
10443 for (const parent of commit.parent) {
10444 if (!visited.has(parent)) {
10445 queue.push(parent);
10446 visited.add(parent);
10447 }
10448 }
10449 }
10450 // Eventually, we'll travel entire tree to the roots where all the parents are empty arrays,
10451 // or hit the shallow depth and throw an error. Excluding the possibility of grafts, or
10452 // different branches cloned to different depths, you would hit this error at the same time
10453 // for all parents, so trying to continue is futile.
10454 }
10455 return false
10456}
10457
10458// @ts-check
10459
10460/**
10461 * Check whether a git commit is descended from another
10462 *
10463 * @param {object} args
10464 * @param {FsClient} args.fs - a file system client
10465 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
10466 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10467 * @param {string} args.oid - The descendent commit
10468 * @param {string} args.ancestor - The (proposed) ancestor commit
10469 * @param {number} [args.depth = -1] - Maximum depth to search before giving up. -1 means no maximum depth.
10470 * @param {object} [args.cache] - a [cache](cache.md) object
10471 *
10472 * @returns {Promise<boolean>} Resolves to true if `oid` is a descendent of `ancestor`
10473 *
10474 * @example
10475 * let oid = await git.resolveRef({ fs, dir: '/tutorial', ref: 'main' })
10476 * let ancestor = await git.resolveRef({ fs, dir: '/tutorial', ref: 'v0.20.0' })
10477 * console.log(oid, ancestor)
10478 * await git.isDescendent({ fs, dir: '/tutorial', oid, ancestor, depth: -1 })
10479 *
10480 */
10481async function isDescendent({
10482 fs,
10483 dir,
10484 gitdir = join(dir, '.git'),
10485 oid,
10486 ancestor,
10487 depth = -1,
10488 cache = {},
10489}) {
10490 try {
10491 assertParameter('fs', fs);
10492 assertParameter('gitdir', gitdir);
10493 assertParameter('oid', oid);
10494 assertParameter('ancestor', ancestor);
10495
10496 return await _isDescendent({
10497 fs: new FileSystem(fs),
10498 cache,
10499 gitdir,
10500 oid,
10501 ancestor,
10502 depth,
10503 })
10504 } catch (err) {
10505 err.caller = 'git.isDescendent';
10506 throw err
10507 }
10508}
10509
10510// @ts-check
10511
10512/**
10513 * Test whether a filepath should be ignored (because of .gitignore or .git/exclude)
10514 *
10515 * @param {object} args
10516 * @param {FsClient} args.fs - a file system client
10517 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
10518 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10519 * @param {string} args.filepath - The filepath to test
10520 *
10521 * @returns {Promise<boolean>} Resolves to true if the file should be ignored
10522 *
10523 * @example
10524 * await git.isIgnored({ fs, dir: '/tutorial', filepath: 'docs/add.md' })
10525 *
10526 */
10527async function isIgnored({
10528 fs,
10529 dir,
10530 gitdir = join(dir, '.git'),
10531 filepath,
10532}) {
10533 try {
10534 assertParameter('fs', fs);
10535 assertParameter('dir', dir);
10536 assertParameter('gitdir', gitdir);
10537 assertParameter('filepath', filepath);
10538
10539 return GitIgnoreManager.isIgnored({
10540 fs: new FileSystem(fs),
10541 dir,
10542 gitdir,
10543 filepath,
10544 })
10545 } catch (err) {
10546 err.caller = 'git.isIgnored';
10547 throw err
10548 }
10549}
10550
10551// @ts-check
10552
10553/**
10554 * List branches
10555 *
10556 * By default it lists local branches. If a 'remote' is specified, it lists the remote's branches. When listing remote branches, the HEAD branch is not filtered out, so it may be included in the list of results.
10557 *
10558 * Note that specifying a remote does not actually contact the server and update the list of branches.
10559 * If you want an up-to-date list, first do a `fetch` to that remote.
10560 * (Which branch you fetch doesn't matter - the list of branches available on the remote is updated during the fetch handshake.)
10561 *
10562 * Also note, that a branch is a reference to a commit. If you initialize a new repository it has no commits, so the
10563 * `listBranches` function will return an empty list, until you create the first commit.
10564 *
10565 * @param {object} args
10566 * @param {FsClient} args.fs - a file system client
10567 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
10568 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10569 * @param {string} [args.remote] - Instead of the branches in `refs/heads`, list the branches in `refs/remotes/${remote}`.
10570 *
10571 * @returns {Promise<Array<string>>} Resolves successfully with an array of branch names
10572 *
10573 * @example
10574 * let branches = await git.listBranches({ fs, dir: '/tutorial' })
10575 * console.log(branches)
10576 * let remoteBranches = await git.listBranches({ fs, dir: '/tutorial', remote: 'origin' })
10577 * console.log(remoteBranches)
10578 *
10579 */
10580async function listBranches({
10581 fs,
10582 dir,
10583 gitdir = join(dir, '.git'),
10584 remote,
10585}) {
10586 try {
10587 assertParameter('fs', fs);
10588 assertParameter('gitdir', gitdir);
10589
10590 return GitRefManager.listBranches({
10591 fs: new FileSystem(fs),
10592 gitdir,
10593 remote,
10594 })
10595 } catch (err) {
10596 err.caller = 'git.listBranches';
10597 throw err
10598 }
10599}
10600
10601// @ts-check
10602
10603/**
10604 * @param {object} args
10605 * @param {import('../models/FileSystem.js').FileSystem} args.fs
10606 * @param {object} args.cache
10607 * @param {string} args.gitdir
10608 * @param {string} [args.ref]
10609 *
10610 * @returns {Promise<Array<string>>}
10611 */
10612async function _listFiles({ fs, gitdir, ref, cache }) {
10613 if (ref) {
10614 const oid = await GitRefManager.resolve({ gitdir, fs, ref });
10615 const filenames = [];
10616 await accumulateFilesFromOid({
10617 fs,
10618 cache,
10619 gitdir,
10620 oid,
10621 filenames,
10622 prefix: '',
10623 });
10624 return filenames
10625 } else {
10626 return GitIndexManager.acquire({ fs, gitdir, cache }, async function(
10627 index
10628 ) {
10629 return index.entries.map(x => x.path)
10630 })
10631 }
10632}
10633
10634async function accumulateFilesFromOid({
10635 fs,
10636 cache,
10637 gitdir,
10638 oid,
10639 filenames,
10640 prefix,
10641}) {
10642 const { tree } = await _readTree({ fs, cache, gitdir, oid });
10643 // TODO: Use `walk` to do this. Should be faster.
10644 for (const entry of tree) {
10645 if (entry.type === 'tree') {
10646 await accumulateFilesFromOid({
10647 fs,
10648 cache,
10649 gitdir,
10650 oid: entry.oid,
10651 filenames,
10652 prefix: join(prefix, entry.path),
10653 });
10654 } else {
10655 filenames.push(join(prefix, entry.path));
10656 }
10657 }
10658}
10659
10660// @ts-check
10661
10662/**
10663 * List all the files in the git index or a commit
10664 *
10665 * > Note: This function is efficient for listing the files in the staging area, but listing all the files in a commit requires recursively walking through the git object store.
10666 * > If you do not require a complete list of every file, better performance can be achieved by using [walk](./walk) and ignoring subdirectories you don't care about.
10667 *
10668 * @param {object} args
10669 * @param {FsClient} args.fs - a file system client
10670 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
10671 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10672 * @param {string} [args.ref] - Return a list of all the files in the commit at `ref` instead of the files currently in the git index (aka staging area)
10673 * @param {object} [args.cache] - a [cache](cache.md) object
10674 *
10675 * @returns {Promise<Array<string>>} Resolves successfully with an array of filepaths
10676 *
10677 * @example
10678 * // All the files in the previous commit
10679 * let files = await git.listFiles({ fs, dir: '/tutorial', ref: 'HEAD' })
10680 * console.log(files)
10681 * // All the files in the current staging area
10682 * files = await git.listFiles({ fs, dir: '/tutorial' })
10683 * console.log(files)
10684 *
10685 */
10686async function listFiles({
10687 fs,
10688 dir,
10689 gitdir = join(dir, '.git'),
10690 ref,
10691 cache = {},
10692}) {
10693 try {
10694 assertParameter('fs', fs);
10695 assertParameter('gitdir', gitdir);
10696
10697 return await _listFiles({
10698 fs: new FileSystem(fs),
10699 cache,
10700 gitdir,
10701 ref,
10702 })
10703 } catch (err) {
10704 err.caller = 'git.listFiles';
10705 throw err
10706 }
10707}
10708
10709// @ts-check
10710
10711/**
10712 * List all the object notes
10713 *
10714 * @param {object} args
10715 * @param {import('../models/FileSystem.js').FileSystem} args.fs
10716 * @param {any} args.cache
10717 * @param {string} args.gitdir
10718 * @param {string} args.ref
10719 *
10720 * @returns {Promise<Array<{target: string, note: string}>>}
10721 */
10722
10723async function _listNotes({ fs, cache, gitdir, ref }) {
10724 // Get the current note commit
10725 let parent;
10726 try {
10727 parent = await GitRefManager.resolve({ gitdir, fs, ref });
10728 } catch (err) {
10729 if (err instanceof NotFoundError) {
10730 return []
10731 }
10732 }
10733
10734 // Create the current note tree
10735 const result = await _readTree({
10736 fs,
10737 cache,
10738 gitdir,
10739 oid: parent,
10740 });
10741
10742 // Format the tree entries
10743 const notes = result.tree.map(entry => ({
10744 target: entry.path,
10745 note: entry.oid,
10746 }));
10747 return notes
10748}
10749
10750// @ts-check
10751
10752/**
10753 * List all the object notes
10754 *
10755 * @param {object} args
10756 * @param {FsClient} args.fs - a file system client
10757 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
10758 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10759 * @param {string} [args.ref] - The notes ref to look under
10760 * @param {object} [args.cache] - a [cache](cache.md) object
10761 *
10762 * @returns {Promise<Array<{target: string, note: string}>>} Resolves successfully with an array of entries containing SHA-1 object ids of the note and the object the note targets
10763 */
10764
10765async function listNotes({
10766 fs,
10767 dir,
10768 gitdir = join(dir, '.git'),
10769 ref = 'refs/notes/commits',
10770 cache = {},
10771}) {
10772 try {
10773 assertParameter('fs', fs);
10774 assertParameter('gitdir', gitdir);
10775 assertParameter('ref', ref);
10776
10777 return await _listNotes({
10778 fs: new FileSystem(fs),
10779 cache,
10780 gitdir,
10781 ref,
10782 })
10783 } catch (err) {
10784 err.caller = 'git.listNotes';
10785 throw err
10786 }
10787}
10788
10789// @ts-check
10790
10791/**
10792 * @param {object} args
10793 * @param {import('../models/FileSystem.js').FileSystem} args.fs
10794 * @param {string} args.gitdir
10795 *
10796 * @returns {Promise<Array<{remote: string, url: string}>>}
10797 */
10798async function _listRemotes({ fs, gitdir }) {
10799 const config = await GitConfigManager.get({ fs, gitdir });
10800 const remoteNames = await config.getSubsections('remote');
10801 const remotes = Promise.all(
10802 remoteNames.map(async remote => {
10803 const url = await config.get(`remote.${remote}.url`);
10804 return { remote, url }
10805 })
10806 );
10807 return remotes
10808}
10809
10810// @ts-check
10811
10812/**
10813 * List remotes
10814 *
10815 * @param {object} args
10816 * @param {FsClient} args.fs - a file system client
10817 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
10818 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
10819 *
10820 * @returns {Promise<Array<{remote: string, url: string}>>} Resolves successfully with an array of `{remote, url}` objects
10821 *
10822 * @example
10823 * let remotes = await git.listRemotes({ fs, dir: '/tutorial' })
10824 * console.log(remotes)
10825 *
10826 */
10827async function listRemotes({ fs, dir, gitdir = join(dir, '.git') }) {
10828 try {
10829 assertParameter('fs', fs);
10830 assertParameter('gitdir', gitdir);
10831
10832 return await _listRemotes({
10833 fs: new FileSystem(fs),
10834 gitdir,
10835 })
10836 } catch (err) {
10837 err.caller = 'git.listRemotes';
10838 throw err
10839 }
10840}
10841
10842/**
10843 * @typedef {Object} ServerRef - This object has the following schema:
10844 * @property {string} ref - The name of the ref
10845 * @property {string} oid - The SHA-1 object id the ref points to
10846 * @property {string} [target] - The target ref pointed to by a symbolic ref
10847 * @property {string} [peeled] - If the oid is the SHA-1 object id of an annotated tag, this is the SHA-1 object id that the annotated tag points to
10848 */
10849
10850async function parseListRefsResponse(stream) {
10851 const read = GitPktLine.streamReader(stream);
10852
10853 // TODO: when we re-write everything to minimize memory usage,
10854 // we could make this a generator
10855 const refs = [];
10856
10857 let line;
10858 while (true) {
10859 line = await read();
10860 if (line === true) break
10861 if (line === null) continue
10862 line = line.toString('utf8').replace(/\n$/, '');
10863 const [oid, ref, ...attrs] = line.split(' ');
10864 const r = { ref, oid };
10865 for (const attr of attrs) {
10866 const [name, value] = attr.split(':');
10867 if (name === 'symref-target') {
10868 r.target = value;
10869 } else if (name === 'peeled') {
10870 r.peeled = value;
10871 }
10872 }
10873 refs.push(r);
10874 }
10875
10876 return refs
10877}
10878
10879/**
10880 * @param {object} args
10881 * @param {string} [args.prefix] - Only list refs that start with this prefix
10882 * @param {boolean} [args.symrefs = false] - Include symbolic ref targets
10883 * @param {boolean} [args.peelTags = false] - Include peeled tags values
10884 * @returns {Uint8Array[]}
10885 */
10886async function writeListRefsRequest({ prefix, symrefs, peelTags }) {
10887 const packstream = [];
10888 // command
10889 packstream.push(GitPktLine.encode('command=ls-refs\n'));
10890 // capability-list
10891 packstream.push(GitPktLine.encode(`agent=${pkg.agent}\n`));
10892 // [command-args]
10893 if (peelTags || symrefs || prefix) {
10894 packstream.push(GitPktLine.delim());
10895 }
10896 if (peelTags) packstream.push(GitPktLine.encode('peel'));
10897 if (symrefs) packstream.push(GitPktLine.encode('symrefs'));
10898 if (prefix) packstream.push(GitPktLine.encode(`ref-prefix ${prefix}`));
10899 packstream.push(GitPktLine.flush());
10900 return packstream
10901}
10902
10903// @ts-check
10904
10905/**
10906 * Fetch a list of refs (branches, tags, etc) from a server.
10907 *
10908 * This is a rare command that doesn't require an `fs`, `dir`, or even `gitdir` argument.
10909 * It just requires an `http` argument.
10910 *
10911 * ### About `protocolVersion`
10912 *
10913 * There's a rather fun trade-off between Git Protocol Version 1 and Git Protocol Version 2.
10914 * Version 2 actually requires 2 HTTP requests instead of 1, making it similar to fetch or push in that regard.
10915 * However, version 2 supports server-side filtering by prefix, whereas that filtering is done client-side in version 1.
10916 * Which protocol is most efficient therefore depends on the number of refs on the remote, the latency of the server, and speed of the network connection.
10917 * For an small repos (or fast Internet connections), the requirement to make two trips to the server makes protocol 2 slower.
10918 * But for large repos (or slow Internet connections), the decreased payload size of the second request makes up for the additional request.
10919 *
10920 * Hard numbers vary by situation, but here's some numbers from my machine:
10921 *
10922 * Using isomorphic-git in a browser, with a CORS proxy, listing only the branches (refs/heads) of https://github.com/isomorphic-git/isomorphic-git
10923 * - Protocol Version 1 took ~300ms and transfered 84 KB.
10924 * - Protocol Version 2 took ~500ms and transfered 4.1 KB.
10925 *
10926 * Using isomorphic-git in a browser, with a CORS proxy, listing only the branches (refs/heads) of https://gitlab.com/gitlab-org/gitlab
10927 * - Protocol Version 1 took ~4900ms and transfered 9.41 MB.
10928 * - Protocol Version 2 took ~1280ms and transfered 433 KB.
10929 *
10930 * Finally, there is a fun quirk regarding the `symrefs` parameter.
10931 * Protocol Version 1 will generally only return the `HEAD` symref and not others.
10932 * Historically, this meant that servers don't use symbolic refs except for `HEAD`, which is used to point at the "default branch".
10933 * However Protocol Version 2 can return *all* the symbolic refs on the server.
10934 * So if you are running your own git server, you could take advantage of that I guess.
10935 *
10936 * #### TL;DR
10937 * If you are _not_ taking advantage of `prefix` I would recommend `protocolVersion: 1`.
10938 * Otherwise, I recommend to use the default which is `protocolVersion: 2`.
10939 *
10940 * @param {object} args
10941 * @param {HttpClient} args.http - an HTTP client
10942 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
10943 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
10944 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
10945 * @param {string} args.url - The URL of the remote repository. Will be gotten from gitconfig if absent.
10946 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Overrides value in repo config.
10947 * @param {boolean} [args.forPush = false] - By default, the command queries the 'fetch' capabilities. If true, it will ask for the 'push' capabilities.
10948 * @param {Object<string, string>} [args.headers] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
10949 * @param {1 | 2} [args.protocolVersion = 2] - Which version of the Git Protocol to use.
10950 * @param {string} [args.prefix] - Only list refs that start with this prefix
10951 * @param {boolean} [args.symrefs = false] - Include symbolic ref targets
10952 * @param {boolean} [args.peelTags = false] - Include annotated tag peeled targets
10953 *
10954 * @returns {Promise<ServerRef[]>} Resolves successfully with an array of ServerRef objects
10955 * @see ServerRef
10956 *
10957 * @example
10958 * // List all the branches on a repo
10959 * let refs = await git.listServerRefs({
10960 * http,
10961 * corsProxy: "https://cors.isomorphic-git.org",
10962 * url: "https://github.com/isomorphic-git/isomorphic-git.git",
10963 * prefix: "refs/heads/",
10964 * });
10965 * console.log(refs);
10966 *
10967 * @example
10968 * // Get the default branch on a repo
10969 * let refs = await git.listServerRefs({
10970 * http,
10971 * corsProxy: "https://cors.isomorphic-git.org",
10972 * url: "https://github.com/isomorphic-git/isomorphic-git.git",
10973 * prefix: "HEAD",
10974 * symrefs: true,
10975 * });
10976 * console.log(refs);
10977 *
10978 * @example
10979 * // List all the tags on a repo
10980 * let refs = await git.listServerRefs({
10981 * http,
10982 * corsProxy: "https://cors.isomorphic-git.org",
10983 * url: "https://github.com/isomorphic-git/isomorphic-git.git",
10984 * prefix: "refs/tags/",
10985 * peelTags: true,
10986 * });
10987 * console.log(refs);
10988 *
10989 * @example
10990 * // List all the pull requests on a repo
10991 * let refs = await git.listServerRefs({
10992 * http,
10993 * corsProxy: "https://cors.isomorphic-git.org",
10994 * url: "https://github.com/isomorphic-git/isomorphic-git.git",
10995 * prefix: "refs/pull/",
10996 * });
10997 * console.log(refs);
10998 *
10999 */
11000async function listServerRefs({
11001 http,
11002 onAuth,
11003 onAuthSuccess,
11004 onAuthFailure,
11005 corsProxy,
11006 url,
11007 headers = {},
11008 forPush = false,
11009 protocolVersion = 2,
11010 prefix,
11011 symrefs,
11012 peelTags,
11013}) {
11014 try {
11015 assertParameter('http', http);
11016 assertParameter('url', url);
11017
11018 const remote = await GitRemoteHTTP.discover({
11019 http,
11020 onAuth,
11021 onAuthSuccess,
11022 onAuthFailure,
11023 corsProxy,
11024 service: forPush ? 'git-receive-pack' : 'git-upload-pack',
11025 url,
11026 headers,
11027 protocolVersion,
11028 });
11029
11030 if (remote.protocolVersion === 1) {
11031 return formatInfoRefs(remote, prefix, symrefs, peelTags)
11032 }
11033
11034 // Protocol Version 2
11035 const body = await writeListRefsRequest({ prefix, symrefs, peelTags });
11036
11037 const res = await GitRemoteHTTP.connect({
11038 http,
11039 auth: remote.auth,
11040 headers,
11041 corsProxy,
11042 service: forPush ? 'git-receive-pack' : 'git-upload-pack',
11043 url,
11044 body,
11045 });
11046
11047 return parseListRefsResponse(res.body)
11048 } catch (err) {
11049 err.caller = 'git.listServerRefs';
11050 throw err
11051 }
11052}
11053
11054// @ts-check
11055
11056/**
11057 * List tags
11058 *
11059 * @param {object} args
11060 * @param {FsClient} args.fs - a file system client
11061 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
11062 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
11063 *
11064 * @returns {Promise<Array<string>>} Resolves successfully with an array of tag names
11065 *
11066 * @example
11067 * let tags = await git.listTags({ fs, dir: '/tutorial' })
11068 * console.log(tags)
11069 *
11070 */
11071async function listTags({ fs, dir, gitdir = join(dir, '.git') }) {
11072 try {
11073 assertParameter('fs', fs);
11074 assertParameter('gitdir', gitdir);
11075 return GitRefManager.listTags({ fs: new FileSystem(fs), gitdir })
11076 } catch (err) {
11077 err.caller = 'git.listTags';
11078 throw err
11079 }
11080}
11081
11082async function resolveCommit({ fs, cache, gitdir, oid }) {
11083 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
11084 // Resolve annotated tag objects to whatever
11085 if (type === 'tag') {
11086 oid = GitAnnotatedTag.from(object).parse().object;
11087 return resolveCommit({ fs, cache, gitdir, oid })
11088 }
11089 if (type !== 'commit') {
11090 throw new ObjectTypeError(oid, type, 'commit')
11091 }
11092 return { commit: GitCommit.from(object), oid }
11093}
11094
11095// @ts-check
11096
11097/**
11098 * @param {object} args
11099 * @param {import('../models/FileSystem.js').FileSystem} args.fs
11100 * @param {any} args.cache
11101 * @param {string} args.gitdir
11102 * @param {string} args.oid
11103 *
11104 * @returns {Promise<ReadCommitResult>} Resolves successfully with a git commit object
11105 * @see ReadCommitResult
11106 * @see CommitObject
11107 *
11108 */
11109async function _readCommit({ fs, cache, gitdir, oid }) {
11110 const { commit, oid: commitOid } = await resolveCommit({
11111 fs,
11112 cache,
11113 gitdir,
11114 oid,
11115 });
11116 const result = {
11117 oid: commitOid,
11118 commit: commit.parse(),
11119 payload: commit.withoutSignature(),
11120 };
11121 // @ts-ignore
11122 return result
11123}
11124
11125function compareAge(a, b) {
11126 return a.committer.timestamp - b.committer.timestamp
11127}
11128
11129// @ts-check
11130
11131// the empty file content object id
11132const EMPTY_OID = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391';
11133
11134async function resolveFileIdInTree({ fs, cache, gitdir, oid, fileId }) {
11135 if (fileId === EMPTY_OID) return
11136 const _oid = oid;
11137 let filepath;
11138 const result = await resolveTree({ fs, cache, gitdir, oid });
11139 const tree = result.tree;
11140 if (fileId === result.oid) {
11141 filepath = result.path;
11142 } else {
11143 filepath = await _resolveFileId({
11144 fs,
11145 cache,
11146 gitdir,
11147 tree,
11148 fileId,
11149 oid: _oid,
11150 });
11151 if (Array.isArray(filepath)) {
11152 if (filepath.length === 0) filepath = undefined;
11153 else if (filepath.length === 1) filepath = filepath[0];
11154 }
11155 }
11156 return filepath
11157}
11158
11159async function _resolveFileId({
11160 fs,
11161 cache,
11162 gitdir,
11163 tree,
11164 fileId,
11165 oid,
11166 filepaths = [],
11167 parentPath = '',
11168}) {
11169 const walks = tree.entries().map(function(entry) {
11170 let result;
11171 if (entry.oid === fileId) {
11172 result = join(parentPath, entry.path);
11173 filepaths.push(result);
11174 } else if (entry.type === 'tree') {
11175 result = _readObject({
11176 fs,
11177 cache,
11178 gitdir,
11179 oid: entry.oid,
11180 }).then(function({ object }) {
11181 return _resolveFileId({
11182 fs,
11183 cache,
11184 gitdir,
11185 tree: GitTree.from(object),
11186 fileId,
11187 oid,
11188 filepaths,
11189 parentPath: join(parentPath, entry.path),
11190 })
11191 });
11192 }
11193 return result
11194 });
11195
11196 await Promise.all(walks);
11197 return filepaths
11198}
11199
11200// @ts-check
11201
11202/**
11203 * Get commit descriptions from the git history
11204 *
11205 * @param {object} args
11206 * @param {import('../models/FileSystem.js').FileSystem} args.fs
11207 * @param {any} args.cache
11208 * @param {string} args.gitdir
11209 * @param {string=} args.filepath optional get the commit for the filepath only
11210 * @param {string} args.ref
11211 * @param {number|void} args.depth
11212 * @param {boolean=} [args.force=false] do not throw error if filepath is not exist (works only for a single file). defaults to false
11213 * @param {boolean=} [args.follow=false] Continue listing the history of a file beyond renames (works only for a single file). defaults to false
11214 * @param {boolean=} args.follow Continue listing the history of a file beyond renames (works only for a single file). defaults to false
11215 *
11216 * @returns {Promise<Array<ReadCommitResult>>} Resolves to an array of ReadCommitResult objects
11217 * @see ReadCommitResult
11218 * @see CommitObject
11219 *
11220 * @example
11221 * let commits = await git.log({ dir: '$input((/))', depth: $input((5)), ref: '$input((master))' })
11222 * console.log(commits)
11223 *
11224 */
11225async function _log({
11226 fs,
11227 cache,
11228 gitdir,
11229 filepath,
11230 ref,
11231 depth,
11232 since,
11233 force,
11234 follow,
11235}) {
11236 const sinceTimestamp =
11237 typeof since === 'undefined'
11238 ? undefined
11239 : Math.floor(since.valueOf() / 1000);
11240 // TODO: In the future, we may want to have an API where we return a
11241 // async iterator that emits commits.
11242 const commits = [];
11243 const shallowCommits = await GitShallowManager.read({ fs, gitdir });
11244 const oid = await GitRefManager.resolve({ fs, gitdir, ref });
11245 const tips = [await _readCommit({ fs, cache, gitdir, oid })];
11246 let lastFileOid;
11247 let lastCommit;
11248 let isOk;
11249
11250 function endCommit(commit) {
11251 if (isOk && filepath) commits.push(commit);
11252 }
11253
11254 while (tips.length > 0) {
11255 const commit = tips.pop();
11256
11257 // Stop the log if we've hit the age limit
11258 if (
11259 sinceTimestamp !== undefined &&
11260 commit.commit.committer.timestamp <= sinceTimestamp
11261 ) {
11262 break
11263 }
11264
11265 if (filepath) {
11266 let vFileOid;
11267 try {
11268 vFileOid = await resolveFilepath({
11269 fs,
11270 cache,
11271 gitdir,
11272 oid: commit.commit.tree,
11273 filepath,
11274 });
11275 if (lastCommit && lastFileOid !== vFileOid) {
11276 commits.push(lastCommit);
11277 }
11278 lastFileOid = vFileOid;
11279 lastCommit = commit;
11280 isOk = true;
11281 } catch (e) {
11282 if (e instanceof NotFoundError) {
11283 let found = follow && lastFileOid;
11284 if (found) {
11285 found = await resolveFileIdInTree({
11286 fs,
11287 cache,
11288 gitdir,
11289 oid: commit.commit.tree,
11290 fileId: lastFileOid,
11291 });
11292 if (found) {
11293 if (Array.isArray(found)) {
11294 if (lastCommit) {
11295 const lastFound = await resolveFileIdInTree({
11296 fs,
11297 cache,
11298 gitdir,
11299 oid: lastCommit.commit.tree,
11300 fileId: lastFileOid,
11301 });
11302 if (Array.isArray(lastFound)) {
11303 found = found.filter(p => lastFound.indexOf(p) === -1);
11304 if (found.length === 1) {
11305 found = found[0];
11306 filepath = found;
11307 if (lastCommit) commits.push(lastCommit);
11308 } else {
11309 found = false;
11310 if (lastCommit) commits.push(lastCommit);
11311 break
11312 }
11313 }
11314 }
11315 } else {
11316 filepath = found;
11317 if (lastCommit) commits.push(lastCommit);
11318 }
11319 }
11320 }
11321 if (!found) {
11322 if (isOk && lastFileOid) {
11323 commits.push(lastCommit);
11324 if (!force) break
11325 }
11326 if (!force && !follow) throw e
11327 }
11328 lastCommit = commit;
11329 isOk = false;
11330 } else throw e
11331 }
11332 } else {
11333 commits.push(commit);
11334 }
11335
11336 // Stop the loop if we have enough commits now.
11337 if (depth !== undefined && commits.length === depth) {
11338 endCommit(commit);
11339 break
11340 }
11341
11342 // If this is not a shallow commit...
11343 if (!shallowCommits.has(commit.oid)) {
11344 // Add the parents of this commit to the queue
11345 // Note: for the case of a commit with no parents, it will concat an empty array, having no net effect.
11346 for (const oid of commit.commit.parent) {
11347 const commit = await _readCommit({ fs, cache, gitdir, oid });
11348 if (!tips.map(commit => commit.oid).includes(commit.oid)) {
11349 tips.push(commit);
11350 }
11351 }
11352 }
11353
11354 // Stop the loop if there are no more commit parents
11355 if (tips.length === 0) {
11356 endCommit(commit);
11357 }
11358
11359 // Process tips in order by age
11360 tips.sort((a, b) => compareAge(a.commit, b.commit));
11361 }
11362 return commits
11363}
11364
11365// @ts-check
11366
11367/**
11368 * Get commit descriptions from the git history
11369 *
11370 * @param {object} args
11371 * @param {FsClient} args.fs - a file system client
11372 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
11373 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
11374 * @param {string=} args.filepath optional get the commit for the filepath only
11375 * @param {string} [args.ref = 'HEAD'] - The commit to begin walking backwards through the history from
11376 * @param {number=} [args.depth] - Limit the number of commits returned. No limit by default.
11377 * @param {Date} [args.since] - Return history newer than the given date. Can be combined with `depth` to get whichever is shorter.
11378 * @param {boolean=} [args.force=false] do not throw error if filepath is not exist (works only for a single file). defaults to false
11379 * @param {boolean=} [args.follow=false] Continue listing the history of a file beyond renames (works only for a single file). defaults to false
11380 * @param {object} [args.cache] - a [cache](cache.md) object
11381 *
11382 * @returns {Promise<Array<ReadCommitResult>>} Resolves to an array of ReadCommitResult objects
11383 * @see ReadCommitResult
11384 * @see CommitObject
11385 *
11386 * @example
11387 * let commits = await git.log({
11388 * fs,
11389 * dir: '/tutorial',
11390 * depth: 5,
11391 * ref: 'main'
11392 * })
11393 * console.log(commits)
11394 *
11395 */
11396async function log({
11397 fs,
11398 dir,
11399 gitdir = join(dir, '.git'),
11400 filepath,
11401 ref = 'HEAD',
11402 depth,
11403 since, // Date
11404 force,
11405 follow,
11406 cache = {},
11407}) {
11408 try {
11409 assertParameter('fs', fs);
11410 assertParameter('gitdir', gitdir);
11411 assertParameter('ref', ref);
11412
11413 return await _log({
11414 fs: new FileSystem(fs),
11415 cache,
11416 gitdir,
11417 filepath,
11418 ref,
11419 depth,
11420 since,
11421 force,
11422 follow,
11423 })
11424 } catch (err) {
11425 err.caller = 'git.log';
11426 throw err
11427 }
11428}
11429
11430// @ts-check
11431
11432/**
11433 *
11434 * @typedef {Object} MergeResult - Returns an object with a schema like this:
11435 * @property {string} [oid] - The SHA-1 object id that is now at the head of the branch. Absent only if `dryRun` was specified and `mergeCommit` is true.
11436 * @property {boolean} [alreadyMerged] - True if the branch was already merged so no changes were made
11437 * @property {boolean} [fastForward] - True if it was a fast-forward merge
11438 * @property {boolean} [mergeCommit] - True if merge resulted in a merge commit
11439 * @property {string} [tree] - The SHA-1 object id of the tree resulting from a merge commit
11440 *
11441 */
11442
11443/**
11444 * Merge two branches
11445 *
11446 * Currently it will fail if multiple candidate merge bases are found. (It doesn't yet implement the recursive merge strategy.)
11447 *
11448 * Currently it does not support selecting alternative merge strategies.
11449 *
11450 * Currently it is not possible to abort an incomplete merge. To restore the worktree to a clean state, you will need to checkout an earlier commit.
11451 *
11452 * Currently it does not directly support the behavior of `git merge --continue`. To complete a merge after manual conflict resolution, you will need to add and commit the files manually, and specify the appropriate parent commits.
11453 *
11454 * ## Manually resolving merge conflicts
11455 * By default, if isomorphic-git encounters a merge conflict it cannot resolve using the builtin diff3 algorithm or provided merge driver, it will abort and throw a `MergeNotSupportedError`.
11456 * This leaves the index and working tree untouched.
11457 *
11458 * When `abortOnConflict` is set to `false`, and a merge conflict cannot be automatically resolved, a `MergeConflictError` is thrown and the results of the incomplete merge will be written to the working directory.
11459 * This includes conflict markers in files with unresolved merge conflicts.
11460 *
11461 * To complete the merge, edit the conflicting files as you see fit, and then add and commit the resolved merge.
11462 *
11463 * For a proper merge commit, be sure to specify the branches or commits you are merging in the `parent` argument to `git.commit`.
11464 * For example, say we are merging the branch `feature` into the branch `main` and there is a conflict we want to resolve manually.
11465 * The flow would look like this:
11466 *
11467 * ```
11468 * await git.merge({
11469 * fs,
11470 * dir,
11471 * ours: 'main',
11472 * theirs: 'feature',
11473 * abortOnConflict: false,
11474 * }).catch(e => {
11475 * if (e instanceof Errors.MergeConflictError) {
11476 * console.log(
11477 * 'Automatic merge failed for the following files: '
11478 * + `${e.data}. `
11479 * + 'Resolve these conflicts and then commit your changes.'
11480 * )
11481 * } else throw e
11482 * })
11483 *
11484 * // This is the where we manually edit the files that have been written to the working directory
11485 * // ...
11486 * // Files have been edited and we are ready to commit
11487 *
11488 * await git.add({
11489 * fs,
11490 * dir,
11491 * filepath: '.',
11492 * })
11493 *
11494 * await git.commit({
11495 * fs,
11496 * dir,
11497 * ref: 'main',
11498 * message: "Merge branch 'feature' into main",
11499 * parent: ['main', 'feature'], // Be sure to specify the parents when creating a merge commit
11500 * })
11501 * ```
11502 *
11503 * @param {object} args
11504 * @param {FsClient} args.fs - a file system client
11505 * @param {SignCallback} [args.onSign] - a PGP signing implementation
11506 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
11507 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
11508 * @param {string} [args.ours] - The branch receiving the merge. If undefined, defaults to the current branch.
11509 * @param {string} args.theirs - The branch to be merged
11510 * @param {boolean} [args.fastForward = true] - If false, create a merge commit in all cases.
11511 * @param {boolean} [args.fastForwardOnly = false] - If true, then non-fast-forward merges will throw an Error instead of performing a merge.
11512 * @param {boolean} [args.dryRun = false] - If true, simulates a merge so you can test whether it would succeed.
11513 * @param {boolean} [args.noUpdateBranch = false] - If true, does not update the branch pointer after creating the commit.
11514 * @param {boolean} [args.abortOnConflict = true] - If true, merges with conflicts will not update the worktree or index.
11515 * @param {string} [args.message] - Overrides the default auto-generated merge commit message
11516 * @param {Object} [args.author] - passed to [commit](commit.md) when creating a merge commit
11517 * @param {string} [args.author.name] - Default is `user.name` config.
11518 * @param {string} [args.author.email] - Default is `user.email` config.
11519 * @param {number} [args.author.timestamp=Math.floor(Date.now()/1000)] - Set the author timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
11520 * @param {number} [args.author.timezoneOffset] - Set the author timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
11521 * @param {Object} [args.committer] - passed to [commit](commit.md) when creating a merge commit
11522 * @param {string} [args.committer.name] - Default is `user.name` config.
11523 * @param {string} [args.committer.email] - Default is `user.email` config.
11524 * @param {number} [args.committer.timestamp=Math.floor(Date.now()/1000)] - Set the committer timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
11525 * @param {number} [args.committer.timezoneOffset] - Set the committer timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
11526 * @param {string} [args.signingKey] - passed to [commit](commit.md) when creating a merge commit
11527 * @param {object} [args.cache] - a [cache](cache.md) object
11528 * @param {MergeDriverCallback} [args.mergeDriver] - a [merge driver](mergeDriver.md) implementation
11529 *
11530 * @returns {Promise<MergeResult>} Resolves to a description of the merge operation
11531 * @see MergeResult
11532 *
11533 * @example
11534 * let m = await git.merge({
11535 * fs,
11536 * dir: '/tutorial',
11537 * ours: 'main',
11538 * theirs: 'remotes/origin/main'
11539 * })
11540 * console.log(m)
11541 *
11542 */
11543async function merge({
11544 fs: _fs,
11545 onSign,
11546 dir,
11547 gitdir = join(dir, '.git'),
11548 ours,
11549 theirs,
11550 fastForward = true,
11551 fastForwardOnly = false,
11552 dryRun = false,
11553 noUpdateBranch = false,
11554 abortOnConflict = true,
11555 message,
11556 author: _author,
11557 committer: _committer,
11558 signingKey,
11559 cache = {},
11560 mergeDriver,
11561}) {
11562 try {
11563 assertParameter('fs', _fs);
11564 if (signingKey) {
11565 assertParameter('onSign', onSign);
11566 }
11567 const fs = new FileSystem(_fs);
11568
11569 const author = await normalizeAuthorObject({ fs, gitdir, author: _author });
11570 if (!author && (!fastForwardOnly || !fastForward)) {
11571 throw new MissingNameError('author')
11572 }
11573
11574 const committer = await normalizeCommitterObject({
11575 fs,
11576 gitdir,
11577 author,
11578 committer: _committer,
11579 });
11580 if (!committer && (!fastForwardOnly || !fastForward)) {
11581 throw new MissingNameError('committer')
11582 }
11583
11584 return await _merge({
11585 fs,
11586 cache,
11587 dir,
11588 gitdir,
11589 ours,
11590 theirs,
11591 fastForward,
11592 fastForwardOnly,
11593 dryRun,
11594 noUpdateBranch,
11595 abortOnConflict,
11596 message,
11597 author,
11598 committer,
11599 signingKey,
11600 onSign,
11601 mergeDriver,
11602 })
11603 } catch (err) {
11604 err.caller = 'git.merge';
11605 throw err
11606 }
11607}
11608
11609/**
11610 * @enum {number}
11611 */
11612const types = {
11613 commit: 0b0010000,
11614 tree: 0b0100000,
11615 blob: 0b0110000,
11616 tag: 0b1000000,
11617 ofs_delta: 0b1100000,
11618 ref_delta: 0b1110000,
11619};
11620
11621/**
11622 * @param {object} args
11623 * @param {import('../models/FileSystem.js').FileSystem} args.fs
11624 * @param {any} args.cache
11625 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
11626 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
11627 * @param {string[]} args.oids
11628 */
11629async function _pack({
11630 fs,
11631 cache,
11632 dir,
11633 gitdir = join(dir, '.git'),
11634 oids,
11635}) {
11636 const hash = new Hash();
11637 const outputStream = [];
11638 function write(chunk, enc) {
11639 const buff = Buffer.from(chunk, enc);
11640 outputStream.push(buff);
11641 hash.update(buff);
11642 }
11643 async function writeObject({ stype, object }) {
11644 // Object type is encoded in bits 654
11645 const type = types[stype];
11646 // The length encoding gets complicated.
11647 let length = object.length;
11648 // Whether the next byte is part of the variable-length encoded number
11649 // is encoded in bit 7
11650 let multibyte = length > 0b1111 ? 0b10000000 : 0b0;
11651 // Last four bits of length is encoded in bits 3210
11652 const lastFour = length & 0b1111;
11653 // Discard those bits
11654 length = length >>> 4;
11655 // The first byte is then (1-bit multibyte?), (3-bit type), (4-bit least sig 4-bits of length)
11656 let byte = (multibyte | type | lastFour).toString(16);
11657 write(byte, 'hex');
11658 // Now we keep chopping away at length 7-bits at a time until its zero,
11659 // writing out the bytes in what amounts to little-endian order.
11660 while (multibyte) {
11661 multibyte = length > 0b01111111 ? 0b10000000 : 0b0;
11662 byte = multibyte | (length & 0b01111111);
11663 write(padHex(2, byte), 'hex');
11664 length = length >>> 7;
11665 }
11666 // Lastly, we can compress and write the object.
11667 write(Buffer.from(await deflate(object)));
11668 }
11669 write('PACK');
11670 write('00000002', 'hex');
11671 // Write a 4 byte (32-bit) int
11672 write(padHex(8, oids.length), 'hex');
11673 for (const oid of oids) {
11674 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
11675 await writeObject({ write, object, stype: type });
11676 }
11677 // Write SHA1 checksum
11678 const digest = hash.digest();
11679 outputStream.push(digest);
11680 return outputStream
11681}
11682
11683// @ts-check
11684
11685/**
11686 *
11687 * @typedef {Object} PackObjectsResult The packObjects command returns an object with two properties:
11688 * @property {string} filename - The suggested filename for the packfile if you want to save it to disk somewhere. It includes the packfile SHA.
11689 * @property {Uint8Array} [packfile] - The packfile contents. Not present if `write` parameter was true, in which case the packfile was written straight to disk.
11690 */
11691
11692/**
11693 * @param {object} args
11694 * @param {import('../models/FileSystem.js').FileSystem} args.fs
11695 * @param {any} args.cache
11696 * @param {string} args.gitdir
11697 * @param {string[]} args.oids
11698 * @param {boolean} args.write
11699 *
11700 * @returns {Promise<PackObjectsResult>}
11701 * @see PackObjectsResult
11702 */
11703async function _packObjects({ fs, cache, gitdir, oids, write }) {
11704 const buffers = await _pack({ fs, cache, gitdir, oids });
11705 const packfile = Buffer.from(await collect(buffers));
11706 const packfileSha = packfile.slice(-20).toString('hex');
11707 const filename = `pack-${packfileSha}.pack`;
11708 if (write) {
11709 await fs.write(join(gitdir, `objects/pack/${filename}`), packfile);
11710 return { filename }
11711 }
11712 return {
11713 filename,
11714 packfile: new Uint8Array(packfile),
11715 }
11716}
11717
11718// @ts-check
11719
11720/**
11721 *
11722 * @typedef {Object} PackObjectsResult The packObjects command returns an object with two properties:
11723 * @property {string} filename - The suggested filename for the packfile if you want to save it to disk somewhere. It includes the packfile SHA.
11724 * @property {Uint8Array} [packfile] - The packfile contents. Not present if `write` parameter was true, in which case the packfile was written straight to disk.
11725 */
11726
11727/**
11728 * Create a packfile from an array of SHA-1 object ids
11729 *
11730 * @param {object} args
11731 * @param {FsClient} args.fs - a file system client
11732 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
11733 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
11734 * @param {string[]} args.oids - An array of SHA-1 object ids to be included in the packfile
11735 * @param {boolean} [args.write = false] - Whether to save the packfile to disk or not
11736 * @param {object} [args.cache] - a [cache](cache.md) object
11737 *
11738 * @returns {Promise<PackObjectsResult>} Resolves successfully when the packfile is ready with the filename and buffer
11739 * @see PackObjectsResult
11740 *
11741 * @example
11742 * // Create a packfile containing only an empty tree
11743 * let { packfile } = await git.packObjects({
11744 * fs,
11745 * dir: '/tutorial',
11746 * oids: ['4b825dc642cb6eb9a060e54bf8d69288fbee4904']
11747 * })
11748 * console.log(packfile)
11749 *
11750 */
11751async function packObjects({
11752 fs,
11753 dir,
11754 gitdir = join(dir, '.git'),
11755 oids,
11756 write = false,
11757 cache = {},
11758}) {
11759 try {
11760 assertParameter('fs', fs);
11761 assertParameter('gitdir', gitdir);
11762 assertParameter('oids', oids);
11763
11764 return await _packObjects({
11765 fs: new FileSystem(fs),
11766 cache,
11767 gitdir,
11768 oids,
11769 write,
11770 })
11771 } catch (err) {
11772 err.caller = 'git.packObjects';
11773 throw err
11774 }
11775}
11776
11777// @ts-check
11778
11779/**
11780 * Fetch and merge commits from a remote repository
11781 *
11782 * @param {object} args
11783 * @param {FsClient} args.fs - a file system client
11784 * @param {HttpClient} args.http - an HTTP client
11785 * @param {ProgressCallback} [args.onProgress] - optional progress event callback
11786 * @param {MessageCallback} [args.onMessage] - optional message event callback
11787 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
11788 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
11789 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
11790 * @param {string} args.dir] - The [working tree](dir-vs-gitdir.md) directory path
11791 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
11792 * @param {string} [args.ref] - Which branch to merge into. By default this is the currently checked out branch.
11793 * @param {string} [args.url] - (Added in 1.1.0) The URL of the remote repository. The default is the value set in the git config for that remote.
11794 * @param {string} [args.remote] - (Added in 1.1.0) If URL is not specified, determines which remote to use.
11795 * @param {string} [args.remoteRef] - (Added in 1.1.0) The name of the branch on the remote to fetch. By default this is the configured remote tracking branch.
11796 * @param {boolean} [args.prune = false] - Delete local remote-tracking branches that are not present on the remote
11797 * @param {boolean} [args.pruneTags = false] - Prune local tags that don’t exist on the remote, and force-update those tags that differ
11798 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Overrides value in repo config.
11799 * @param {boolean} [args.singleBranch = false] - Instead of the default behavior of fetching all the branches, only fetch a single branch.
11800 * @param {boolean} [args.fastForward = true] - If false, only create merge commits.
11801 * @param {boolean} [args.fastForwardOnly = false] - Only perform simple fast-forward merges. (Don't create merge commits.)
11802 * @param {Object<string, string>} [args.headers] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
11803 * @param {Object} [args.author] - The details about the author.
11804 * @param {string} [args.author.name] - Default is `user.name` config.
11805 * @param {string} [args.author.email] - Default is `user.email` config.
11806 * @param {number} [args.author.timestamp=Math.floor(Date.now()/1000)] - Set the author timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
11807 * @param {number} [args.author.timezoneOffset] - Set the author timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
11808 * @param {Object} [args.committer = author] - The details about the commit committer, in the same format as the author parameter. If not specified, the author details are used.
11809 * @param {string} [args.committer.name] - Default is `user.name` config.
11810 * @param {string} [args.committer.email] - Default is `user.email` config.
11811 * @param {number} [args.committer.timestamp=Math.floor(Date.now()/1000)] - Set the committer timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
11812 * @param {number} [args.committer.timezoneOffset] - Set the committer timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
11813 * @param {string} [args.signingKey] - passed to [commit](commit.md) when creating a merge commit
11814 * @param {object} [args.cache] - a [cache](cache.md) object
11815 *
11816 * @returns {Promise<void>} Resolves successfully when pull operation completes
11817 *
11818 * @example
11819 * await git.pull({
11820 * fs,
11821 * http,
11822 * dir: '/tutorial',
11823 * ref: 'main',
11824 * singleBranch: true
11825 * })
11826 * console.log('done')
11827 *
11828 */
11829async function pull({
11830 fs: _fs,
11831 http,
11832 onProgress,
11833 onMessage,
11834 onAuth,
11835 onAuthSuccess,
11836 onAuthFailure,
11837 dir,
11838 gitdir = join(dir, '.git'),
11839 ref,
11840 url,
11841 remote,
11842 remoteRef,
11843 prune = false,
11844 pruneTags = false,
11845 fastForward = true,
11846 fastForwardOnly = false,
11847 corsProxy,
11848 singleBranch,
11849 headers = {},
11850 author: _author,
11851 committer: _committer,
11852 signingKey,
11853 cache = {},
11854}) {
11855 try {
11856 assertParameter('fs', _fs);
11857 assertParameter('gitdir', gitdir);
11858
11859 const fs = new FileSystem(_fs);
11860
11861 const author = await normalizeAuthorObject({ fs, gitdir, author: _author });
11862 if (!author) throw new MissingNameError('author')
11863
11864 const committer = await normalizeCommitterObject({
11865 fs,
11866 gitdir,
11867 author,
11868 committer: _committer,
11869 });
11870 if (!committer) throw new MissingNameError('committer')
11871
11872 return await _pull({
11873 fs,
11874 cache,
11875 http,
11876 onProgress,
11877 onMessage,
11878 onAuth,
11879 onAuthSuccess,
11880 onAuthFailure,
11881 dir,
11882 gitdir,
11883 ref,
11884 url,
11885 remote,
11886 remoteRef,
11887 fastForward,
11888 fastForwardOnly,
11889 corsProxy,
11890 singleBranch,
11891 headers,
11892 author,
11893 committer,
11894 signingKey,
11895 prune,
11896 pruneTags,
11897 })
11898 } catch (err) {
11899 err.caller = 'git.pull';
11900 throw err
11901 }
11902}
11903
11904/**
11905 * @param {object} args
11906 * @param {import('../models/FileSystem.js').FileSystem} args.fs
11907 * @param {any} args.cache
11908 * @param {string} [args.dir]
11909 * @param {string} args.gitdir
11910 * @param {Iterable<string>} args.start
11911 * @param {Iterable<string>} args.finish
11912 * @returns {Promise<Set<string>>}
11913 */
11914async function listCommitsAndTags({
11915 fs,
11916 cache,
11917 dir,
11918 gitdir = join(dir, '.git'),
11919 start,
11920 finish,
11921}) {
11922 const shallows = await GitShallowManager.read({ fs, gitdir });
11923 const startingSet = new Set();
11924 const finishingSet = new Set();
11925 for (const ref of start) {
11926 startingSet.add(await GitRefManager.resolve({ fs, gitdir, ref }));
11927 }
11928 for (const ref of finish) {
11929 // We may not have these refs locally so we must try/catch
11930 try {
11931 const oid = await GitRefManager.resolve({ fs, gitdir, ref });
11932 finishingSet.add(oid);
11933 } catch (err) {}
11934 }
11935 const visited = new Set();
11936 // Because git commits are named by their hash, there is no
11937 // way to construct a cycle. Therefore we won't worry about
11938 // setting a default recursion limit.
11939 async function walk(oid) {
11940 visited.add(oid);
11941 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
11942 // Recursively resolve annotated tags
11943 if (type === 'tag') {
11944 const tag = GitAnnotatedTag.from(object);
11945 const commit = tag.headers().object;
11946 return walk(commit)
11947 }
11948 if (type !== 'commit') {
11949 throw new ObjectTypeError(oid, type, 'commit')
11950 }
11951 if (!shallows.has(oid)) {
11952 const commit = GitCommit.from(object);
11953 const parents = commit.headers().parent;
11954 for (oid of parents) {
11955 if (!finishingSet.has(oid) && !visited.has(oid)) {
11956 await walk(oid);
11957 }
11958 }
11959 }
11960 }
11961 // Let's go walking!
11962 for (const oid of startingSet) {
11963 await walk(oid);
11964 }
11965 return visited
11966}
11967
11968/**
11969 * @param {object} args
11970 * @param {import('../models/FileSystem.js').FileSystem} args.fs
11971 * @param {any} args.cache
11972 * @param {string} [args.dir]
11973 * @param {string} args.gitdir
11974 * @param {Iterable<string>} args.oids
11975 * @returns {Promise<Set<string>>}
11976 */
11977async function listObjects({
11978 fs,
11979 cache,
11980 dir,
11981 gitdir = join(dir, '.git'),
11982 oids,
11983}) {
11984 const visited = new Set();
11985 // We don't do the purest simplest recursion, because we can
11986 // avoid reading Blob objects entirely since the Tree objects
11987 // tell us which oids are Blobs and which are Trees.
11988 async function walk(oid) {
11989 if (visited.has(oid)) return
11990 visited.add(oid);
11991 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
11992 if (type === 'tag') {
11993 const tag = GitAnnotatedTag.from(object);
11994 const obj = tag.headers().object;
11995 await walk(obj);
11996 } else if (type === 'commit') {
11997 const commit = GitCommit.from(object);
11998 const tree = commit.headers().tree;
11999 await walk(tree);
12000 } else if (type === 'tree') {
12001 const tree = GitTree.from(object);
12002 for (const entry of tree) {
12003 // add blobs to the set
12004 // skip over submodules whose type is 'commit'
12005 if (entry.type === 'blob') {
12006 visited.add(entry.oid);
12007 }
12008 // recurse for trees
12009 if (entry.type === 'tree') {
12010 await walk(entry.oid);
12011 }
12012 }
12013 }
12014 }
12015 // Let's go walking!
12016 for (const oid of oids) {
12017 await walk(oid);
12018 }
12019 return visited
12020}
12021
12022async function parseReceivePackResponse(packfile) {
12023 /** @type PushResult */
12024 const result = {};
12025 let response = '';
12026 const read = GitPktLine.streamReader(packfile);
12027 let line = await read();
12028 while (line !== true) {
12029 if (line !== null) response += line.toString('utf8') + '\n';
12030 line = await read();
12031 }
12032
12033 const lines = response.toString('utf8').split('\n');
12034 // We're expecting "unpack {unpack-result}"
12035 line = lines.shift();
12036 if (!line.startsWith('unpack ')) {
12037 throw new ParseError('unpack ok" or "unpack [error message]', line)
12038 }
12039 result.ok = line === 'unpack ok';
12040 if (!result.ok) {
12041 result.error = line.slice('unpack '.length);
12042 }
12043 result.refs = {};
12044 for (const line of lines) {
12045 if (line.trim() === '') continue
12046 const status = line.slice(0, 2);
12047 const refAndMessage = line.slice(3);
12048 let space = refAndMessage.indexOf(' ');
12049 if (space === -1) space = refAndMessage.length;
12050 const ref = refAndMessage.slice(0, space);
12051 const error = refAndMessage.slice(space + 1);
12052 result.refs[ref] = {
12053 ok: status === 'ok',
12054 error,
12055 };
12056 }
12057 return result
12058}
12059
12060async function writeReceivePackRequest({
12061 capabilities = [],
12062 triplets = [],
12063}) {
12064 const packstream = [];
12065 let capsFirstLine = `\x00 ${capabilities.join(' ')}`;
12066 for (const trip of triplets) {
12067 packstream.push(
12068 GitPktLine.encode(
12069 `${trip.oldoid} ${trip.oid} ${trip.fullRef}${capsFirstLine}\n`
12070 )
12071 );
12072 capsFirstLine = '';
12073 }
12074 packstream.push(GitPktLine.flush());
12075 return packstream
12076}
12077
12078// @ts-check
12079
12080/**
12081 * @param {object} args
12082 * @param {import('../models/FileSystem.js').FileSystem} args.fs
12083 * @param {any} args.cache
12084 * @param {HttpClient} args.http
12085 * @param {ProgressCallback} [args.onProgress]
12086 * @param {MessageCallback} [args.onMessage]
12087 * @param {AuthCallback} [args.onAuth]
12088 * @param {AuthFailureCallback} [args.onAuthFailure]
12089 * @param {AuthSuccessCallback} [args.onAuthSuccess]
12090 * @param {string} args.gitdir
12091 * @param {string} [args.ref]
12092 * @param {string} [args.remoteRef]
12093 * @param {string} [args.remote]
12094 * @param {boolean} [args.force = false]
12095 * @param {boolean} [args.delete = false]
12096 * @param {string} [args.url]
12097 * @param {string} [args.corsProxy]
12098 * @param {Object<string, string>} [args.headers]
12099 *
12100 * @returns {Promise<PushResult>}
12101 */
12102async function _push({
12103 fs,
12104 cache,
12105 http,
12106 onProgress,
12107 onMessage,
12108 onAuth,
12109 onAuthSuccess,
12110 onAuthFailure,
12111 gitdir,
12112 ref: _ref,
12113 remoteRef: _remoteRef,
12114 remote,
12115 url: _url,
12116 force = false,
12117 delete: _delete = false,
12118 corsProxy,
12119 headers = {},
12120}) {
12121 const ref = _ref || (await _currentBranch({ fs, gitdir }));
12122 if (typeof ref === 'undefined') {
12123 throw new MissingParameterError('ref')
12124 }
12125 const config = await GitConfigManager.get({ fs, gitdir });
12126 // Figure out what remote to use.
12127 remote =
12128 remote ||
12129 (await config.get(`branch.${ref}.pushRemote`)) ||
12130 (await config.get('remote.pushDefault')) ||
12131 (await config.get(`branch.${ref}.remote`)) ||
12132 'origin';
12133 // Lookup the URL for the given remote.
12134 const url =
12135 _url ||
12136 (await config.get(`remote.${remote}.pushurl`)) ||
12137 (await config.get(`remote.${remote}.url`));
12138 if (typeof url === 'undefined') {
12139 throw new MissingParameterError('remote OR url')
12140 }
12141 // Figure out what remote ref to use.
12142 const remoteRef = _remoteRef || (await config.get(`branch.${ref}.merge`));
12143 if (typeof url === 'undefined') {
12144 throw new MissingParameterError('remoteRef')
12145 }
12146
12147 if (corsProxy === undefined) {
12148 corsProxy = await config.get('http.corsProxy');
12149 }
12150
12151 const fullRef = await GitRefManager.expand({ fs, gitdir, ref });
12152 const oid = _delete
12153 ? '0000000000000000000000000000000000000000'
12154 : await GitRefManager.resolve({ fs, gitdir, ref: fullRef });
12155
12156 /** @type typeof import("../managers/GitRemoteHTTP").GitRemoteHTTP */
12157 const GitRemoteHTTP = GitRemoteManager.getRemoteHelperFor({ url });
12158 const httpRemote = await GitRemoteHTTP.discover({
12159 http,
12160 onAuth,
12161 onAuthSuccess,
12162 onAuthFailure,
12163 corsProxy,
12164 service: 'git-receive-pack',
12165 url,
12166 headers,
12167 protocolVersion: 1,
12168 });
12169 const auth = httpRemote.auth; // hack to get new credentials from CredentialManager API
12170 let fullRemoteRef;
12171 if (!remoteRef) {
12172 fullRemoteRef = fullRef;
12173 } else {
12174 try {
12175 fullRemoteRef = await GitRefManager.expandAgainstMap({
12176 ref: remoteRef,
12177 map: httpRemote.refs,
12178 });
12179 } catch (err) {
12180 if (err instanceof NotFoundError) {
12181 // The remote reference doesn't exist yet.
12182 // If it is fully specified, use that value. Otherwise, treat it as a branch.
12183 fullRemoteRef = remoteRef.startsWith('refs/')
12184 ? remoteRef
12185 : `refs/heads/${remoteRef}`;
12186 } else {
12187 throw err
12188 }
12189 }
12190 }
12191 const oldoid =
12192 httpRemote.refs.get(fullRemoteRef) ||
12193 '0000000000000000000000000000000000000000';
12194
12195 // Remotes can always accept thin-packs UNLESS they specify the 'no-thin' capability
12196 const thinPack = !httpRemote.capabilities.has('no-thin');
12197
12198 let objects = new Set();
12199 if (!_delete) {
12200 const finish = [...httpRemote.refs.values()];
12201 let skipObjects = new Set();
12202
12203 // If remote branch is present, look for a common merge base.
12204 if (oldoid !== '0000000000000000000000000000000000000000') {
12205 // trick to speed up common force push scenarios
12206 const mergebase = await _findMergeBase({
12207 fs,
12208 cache,
12209 gitdir,
12210 oids: [oid, oldoid],
12211 });
12212 for (const oid of mergebase) finish.push(oid);
12213 if (thinPack) {
12214 skipObjects = await listObjects({ fs, cache, gitdir, oids: mergebase });
12215 }
12216 }
12217
12218 // If remote does not have the commit, figure out the objects to send
12219 if (!finish.includes(oid)) {
12220 const commits = await listCommitsAndTags({
12221 fs,
12222 cache,
12223 gitdir,
12224 start: [oid],
12225 finish,
12226 });
12227 objects = await listObjects({ fs, cache, gitdir, oids: commits });
12228 }
12229
12230 if (thinPack) {
12231 // If there's a default branch for the remote lets skip those objects too.
12232 // Since this is an optional optimization, we just catch and continue if there is
12233 // an error (because we can't find a default branch, or can't find a commit, etc)
12234 try {
12235 // Sadly, the discovery phase with 'forPush' doesn't return symrefs, so we have to
12236 // rely on existing ones.
12237 const ref = await GitRefManager.resolve({
12238 fs,
12239 gitdir,
12240 ref: `refs/remotes/${remote}/HEAD`,
12241 depth: 2,
12242 });
12243 const { oid } = await GitRefManager.resolveAgainstMap({
12244 ref: ref.replace(`refs/remotes/${remote}/`, ''),
12245 fullref: ref,
12246 map: httpRemote.refs,
12247 });
12248 const oids = [oid];
12249 for (const oid of await listObjects({ fs, cache, gitdir, oids })) {
12250 skipObjects.add(oid);
12251 }
12252 } catch (e) {}
12253
12254 // Remove objects that we know the remote already has
12255 for (const oid of skipObjects) {
12256 objects.delete(oid);
12257 }
12258 }
12259
12260 if (oid === oldoid) force = true;
12261 if (!force) {
12262 // Is it a tag that already exists?
12263 if (
12264 fullRef.startsWith('refs/tags') &&
12265 oldoid !== '0000000000000000000000000000000000000000'
12266 ) {
12267 throw new PushRejectedError('tag-exists')
12268 }
12269 // Is it a non-fast-forward commit?
12270 if (
12271 oid !== '0000000000000000000000000000000000000000' &&
12272 oldoid !== '0000000000000000000000000000000000000000' &&
12273 !(await _isDescendent({
12274 fs,
12275 cache,
12276 gitdir,
12277 oid,
12278 ancestor: oldoid,
12279 depth: -1,
12280 }))
12281 ) {
12282 throw new PushRejectedError('not-fast-forward')
12283 }
12284 }
12285 }
12286 // We can only safely use capabilities that the server also understands.
12287 // For instance, AWS CodeCommit aborts a push if you include the `agent`!!!
12288 const capabilities = filterCapabilities(
12289 [...httpRemote.capabilities],
12290 ['report-status', 'side-band-64k', `agent=${pkg.agent}`]
12291 );
12292 const packstream1 = await writeReceivePackRequest({
12293 capabilities,
12294 triplets: [{ oldoid, oid, fullRef: fullRemoteRef }],
12295 });
12296 const packstream2 = _delete
12297 ? []
12298 : await _pack({
12299 fs,
12300 cache,
12301 gitdir,
12302 oids: [...objects],
12303 });
12304 const res = await GitRemoteHTTP.connect({
12305 http,
12306 onProgress,
12307 corsProxy,
12308 service: 'git-receive-pack',
12309 url,
12310 auth,
12311 headers,
12312 body: [...packstream1, ...packstream2],
12313 });
12314 const { packfile, progress } = await GitSideBand.demux(res.body);
12315 if (onMessage) {
12316 const lines = splitLines(progress);
12317 forAwait(lines, async line => {
12318 await onMessage(line);
12319 });
12320 }
12321 // Parse the response!
12322 const result = await parseReceivePackResponse(packfile);
12323 if (res.headers) {
12324 result.headers = res.headers;
12325 }
12326
12327 // Update the local copy of the remote ref
12328 if (remote && result.ok && result.refs[fullRemoteRef].ok) {
12329 // TODO: I think this should actually be using a refspec transform rather than assuming 'refs/remotes/{remote}'
12330 const ref = `refs/remotes/${remote}/${fullRemoteRef.replace(
12331 'refs/heads',
12332 ''
12333 )}`;
12334 if (_delete) {
12335 await GitRefManager.deleteRef({ fs, gitdir, ref });
12336 } else {
12337 await GitRefManager.writeRef({ fs, gitdir, ref, value: oid });
12338 }
12339 }
12340 if (result.ok && Object.values(result.refs).every(result => result.ok)) {
12341 return result
12342 } else {
12343 const prettyDetails = Object.entries(result.refs)
12344 .filter(([k, v]) => !v.ok)
12345 .map(([k, v]) => `\n - ${k}: ${v.error}`)
12346 .join('');
12347 throw new GitPushError(prettyDetails, result)
12348 }
12349}
12350
12351// @ts-check
12352
12353/**
12354 * Push a branch or tag
12355 *
12356 * The push command returns an object that describes the result of the attempted push operation.
12357 * *Notes:* If there were no errors, then there will be no `errors` property. There can be a mix of `ok` messages and `errors` messages.
12358 *
12359 * | param | type [= default] | description |
12360 * | ------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12361 * | ok | Array\<string\> | The first item is "unpack" if the overall operation was successful. The remaining items are the names of refs that were updated successfully. |
12362 * | errors | Array\<string\> | If the overall operation threw and error, the first item will be "unpack {Overall error message}". The remaining items are individual refs that failed to be updated in the format "{ref name} {error message}". |
12363 *
12364 * @param {object} args
12365 * @param {FsClient} args.fs - a file system client
12366 * @param {HttpClient} args.http - an HTTP client
12367 * @param {ProgressCallback} [args.onProgress] - optional progress event callback
12368 * @param {MessageCallback} [args.onMessage] - optional message event callback
12369 * @param {AuthCallback} [args.onAuth] - optional auth fill callback
12370 * @param {AuthFailureCallback} [args.onAuthFailure] - optional auth rejected callback
12371 * @param {AuthSuccessCallback} [args.onAuthSuccess] - optional auth approved callback
12372 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
12373 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
12374 * @param {string} [args.ref] - Which branch to push. By default this is the currently checked out branch.
12375 * @param {string} [args.url] - The URL of the remote repository. The default is the value set in the git config for that remote.
12376 * @param {string} [args.remote] - If URL is not specified, determines which remote to use.
12377 * @param {string} [args.remoteRef] - The name of the receiving branch on the remote. By default this is the configured remote tracking branch.
12378 * @param {boolean} [args.force = false] - If true, behaves the same as `git push --force`
12379 * @param {boolean} [args.delete = false] - If true, delete the remote ref
12380 * @param {string} [args.corsProxy] - Optional [CORS proxy](https://www.npmjs.com/%40isomorphic-git/cors-proxy). Overrides value in repo config.
12381 * @param {Object<string, string>} [args.headers] - Additional headers to include in HTTP requests, similar to git's `extraHeader` config
12382 * @param {object} [args.cache] - a [cache](cache.md) object
12383 *
12384 * @returns {Promise<PushResult>} Resolves successfully when push completes with a detailed description of the operation from the server.
12385 * @see PushResult
12386 * @see RefUpdateStatus
12387 *
12388 * @example
12389 * let pushResult = await git.push({
12390 * fs,
12391 * http,
12392 * dir: '/tutorial',
12393 * remote: 'origin',
12394 * ref: 'main',
12395 * onAuth: () => ({ username: process.env.GITHUB_TOKEN }),
12396 * })
12397 * console.log(pushResult)
12398 *
12399 */
12400async function push({
12401 fs,
12402 http,
12403 onProgress,
12404 onMessage,
12405 onAuth,
12406 onAuthSuccess,
12407 onAuthFailure,
12408 dir,
12409 gitdir = join(dir, '.git'),
12410 ref,
12411 remoteRef,
12412 remote = 'origin',
12413 url,
12414 force = false,
12415 delete: _delete = false,
12416 corsProxy,
12417 headers = {},
12418 cache = {},
12419}) {
12420 try {
12421 assertParameter('fs', fs);
12422 assertParameter('http', http);
12423 assertParameter('gitdir', gitdir);
12424
12425 return await _push({
12426 fs: new FileSystem(fs),
12427 cache,
12428 http,
12429 onProgress,
12430 onMessage,
12431 onAuth,
12432 onAuthSuccess,
12433 onAuthFailure,
12434 gitdir,
12435 ref,
12436 remoteRef,
12437 remote,
12438 url,
12439 force,
12440 delete: _delete,
12441 corsProxy,
12442 headers,
12443 })
12444 } catch (err) {
12445 err.caller = 'git.push';
12446 throw err
12447 }
12448}
12449
12450async function resolveBlob({ fs, cache, gitdir, oid }) {
12451 const { type, object } = await _readObject({ fs, cache, gitdir, oid });
12452 // Resolve annotated tag objects to whatever
12453 if (type === 'tag') {
12454 oid = GitAnnotatedTag.from(object).parse().object;
12455 return resolveBlob({ fs, cache, gitdir, oid })
12456 }
12457 if (type !== 'blob') {
12458 throw new ObjectTypeError(oid, type, 'blob')
12459 }
12460 return { oid, blob: new Uint8Array(object) }
12461}
12462
12463// @ts-check
12464
12465/**
12466 *
12467 * @typedef {Object} ReadBlobResult - The object returned has the following schema:
12468 * @property {string} oid
12469 * @property {Uint8Array} blob
12470 *
12471 */
12472
12473/**
12474 * @param {object} args
12475 * @param {import('../models/FileSystem.js').FileSystem} args.fs
12476 * @param {any} args.cache
12477 * @param {string} args.gitdir
12478 * @param {string} args.oid
12479 * @param {string} [args.filepath]
12480 *
12481 * @returns {Promise<ReadBlobResult>} Resolves successfully with a blob object description
12482 * @see ReadBlobResult
12483 */
12484async function _readBlob({
12485 fs,
12486 cache,
12487 gitdir,
12488 oid,
12489 filepath = undefined,
12490}) {
12491 if (filepath !== undefined) {
12492 oid = await resolveFilepath({ fs, cache, gitdir, oid, filepath });
12493 }
12494 const blob = await resolveBlob({
12495 fs,
12496 cache,
12497 gitdir,
12498 oid,
12499 });
12500 return blob
12501}
12502
12503// @ts-check
12504
12505/**
12506 *
12507 * @typedef {Object} ReadBlobResult - The object returned has the following schema:
12508 * @property {string} oid
12509 * @property {Uint8Array} blob
12510 *
12511 */
12512
12513/**
12514 * Read a blob object directly
12515 *
12516 * @param {object} args
12517 * @param {FsClient} args.fs - a file system client
12518 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
12519 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
12520 * @param {string} args.oid - The SHA-1 object id to get. Annotated tags, commits, and trees are peeled.
12521 * @param {string} [args.filepath] - Don't return the object with `oid` itself, but resolve `oid` to a tree and then return the blob object at that filepath.
12522 * @param {object} [args.cache] - a [cache](cache.md) object
12523 *
12524 * @returns {Promise<ReadBlobResult>} Resolves successfully with a blob object description
12525 * @see ReadBlobResult
12526 *
12527 * @example
12528 * // Get the contents of 'README.md' in the main branch.
12529 * let commitOid = await git.resolveRef({ fs, dir: '/tutorial', ref: 'main' })
12530 * console.log(commitOid)
12531 * let { blob } = await git.readBlob({
12532 * fs,
12533 * dir: '/tutorial',
12534 * oid: commitOid,
12535 * filepath: 'README.md'
12536 * })
12537 * console.log(Buffer.from(blob).toString('utf8'))
12538 *
12539 */
12540async function readBlob({
12541 fs,
12542 dir,
12543 gitdir = join(dir, '.git'),
12544 oid,
12545 filepath,
12546 cache = {},
12547}) {
12548 try {
12549 assertParameter('fs', fs);
12550 assertParameter('gitdir', gitdir);
12551 assertParameter('oid', oid);
12552
12553 return await _readBlob({
12554 fs: new FileSystem(fs),
12555 cache,
12556 gitdir,
12557 oid,
12558 filepath,
12559 })
12560 } catch (err) {
12561 err.caller = 'git.readBlob';
12562 throw err
12563 }
12564}
12565
12566// @ts-check
12567
12568/**
12569 * Read a commit object directly
12570 *
12571 * @param {object} args
12572 * @param {FsClient} args.fs - a file system client
12573 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
12574 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
12575 * @param {string} args.oid - The SHA-1 object id to get. Annotated tags are peeled.
12576 * @param {object} [args.cache] - a [cache](cache.md) object
12577 *
12578 * @returns {Promise<ReadCommitResult>} Resolves successfully with a git commit object
12579 * @see ReadCommitResult
12580 * @see CommitObject
12581 *
12582 * @example
12583 * // Read a commit object
12584 * let sha = await git.resolveRef({ fs, dir: '/tutorial', ref: 'main' })
12585 * console.log(sha)
12586 * let commit = await git.readCommit({ fs, dir: '/tutorial', oid: sha })
12587 * console.log(commit)
12588 *
12589 */
12590async function readCommit({
12591 fs,
12592 dir,
12593 gitdir = join(dir, '.git'),
12594 oid,
12595 cache = {},
12596}) {
12597 try {
12598 assertParameter('fs', fs);
12599 assertParameter('gitdir', gitdir);
12600 assertParameter('oid', oid);
12601
12602 return await _readCommit({
12603 fs: new FileSystem(fs),
12604 cache,
12605 gitdir,
12606 oid,
12607 })
12608 } catch (err) {
12609 err.caller = 'git.readCommit';
12610 throw err
12611 }
12612}
12613
12614// @ts-check
12615
12616/**
12617 * Read the contents of a note
12618 *
12619 * @param {object} args
12620 * @param {import('../models/FileSystem.js').FileSystem} args.fs
12621 * @param {any} args.cache
12622 * @param {string} args.gitdir
12623 * @param {string} [args.ref] - The notes ref to look under
12624 * @param {string} args.oid
12625 *
12626 * @returns {Promise<Uint8Array>} Resolves successfully with note contents as a Buffer.
12627 */
12628
12629async function _readNote({
12630 fs,
12631 cache,
12632 gitdir,
12633 ref = 'refs/notes/commits',
12634 oid,
12635}) {
12636 const parent = await GitRefManager.resolve({ gitdir, fs, ref });
12637 const { blob } = await _readBlob({
12638 fs,
12639 cache,
12640 gitdir,
12641 oid: parent,
12642 filepath: oid,
12643 });
12644
12645 return blob
12646}
12647
12648// @ts-check
12649
12650/**
12651 * Read the contents of a note
12652 *
12653 * @param {object} args
12654 * @param {FsClient} args.fs - a file system client
12655 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
12656 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
12657 * @param {string} [args.ref] - The notes ref to look under
12658 * @param {string} args.oid - The SHA-1 object id of the object to get the note for.
12659 * @param {object} [args.cache] - a [cache](cache.md) object
12660 *
12661 * @returns {Promise<Uint8Array>} Resolves successfully with note contents as a Buffer.
12662 */
12663
12664async function readNote({
12665 fs,
12666 dir,
12667 gitdir = join(dir, '.git'),
12668 ref = 'refs/notes/commits',
12669 oid,
12670 cache = {},
12671}) {
12672 try {
12673 assertParameter('fs', fs);
12674 assertParameter('gitdir', gitdir);
12675 assertParameter('ref', ref);
12676 assertParameter('oid', oid);
12677
12678 return await _readNote({
12679 fs: new FileSystem(fs),
12680 cache,
12681 gitdir,
12682 ref,
12683 oid,
12684 })
12685 } catch (err) {
12686 err.caller = 'git.readNote';
12687 throw err
12688 }
12689}
12690
12691// @ts-check
12692
12693/**
12694 *
12695 * @typedef {Object} DeflatedObject
12696 * @property {string} oid
12697 * @property {'deflated'} type
12698 * @property {'deflated'} format
12699 * @property {Uint8Array} object
12700 * @property {string} [source]
12701 *
12702 */
12703
12704/**
12705 *
12706 * @typedef {Object} WrappedObject
12707 * @property {string} oid
12708 * @property {'wrapped'} type
12709 * @property {'wrapped'} format
12710 * @property {Uint8Array} object
12711 * @property {string} [source]
12712 *
12713 */
12714
12715/**
12716 *
12717 * @typedef {Object} RawObject
12718 * @property {string} oid
12719 * @property {'blob'|'commit'|'tree'|'tag'} type
12720 * @property {'content'} format
12721 * @property {Uint8Array} object
12722 * @property {string} [source]
12723 *
12724 */
12725
12726/**
12727 *
12728 * @typedef {Object} ParsedBlobObject
12729 * @property {string} oid
12730 * @property {'blob'} type
12731 * @property {'parsed'} format
12732 * @property {string} object
12733 * @property {string} [source]
12734 *
12735 */
12736
12737/**
12738 *
12739 * @typedef {Object} ParsedCommitObject
12740 * @property {string} oid
12741 * @property {'commit'} type
12742 * @property {'parsed'} format
12743 * @property {CommitObject} object
12744 * @property {string} [source]
12745 *
12746 */
12747
12748/**
12749 *
12750 * @typedef {Object} ParsedTreeObject
12751 * @property {string} oid
12752 * @property {'tree'} type
12753 * @property {'parsed'} format
12754 * @property {TreeObject} object
12755 * @property {string} [source]
12756 *
12757 */
12758
12759/**
12760 *
12761 * @typedef {Object} ParsedTagObject
12762 * @property {string} oid
12763 * @property {'tag'} type
12764 * @property {'parsed'} format
12765 * @property {TagObject} object
12766 * @property {string} [source]
12767 *
12768 */
12769
12770/**
12771 *
12772 * @typedef {ParsedBlobObject | ParsedCommitObject | ParsedTreeObject | ParsedTagObject} ParsedObject
12773 */
12774
12775/**
12776 *
12777 * @typedef {DeflatedObject | WrappedObject | RawObject | ParsedObject } ReadObjectResult
12778 */
12779
12780/**
12781 * Read a git object directly by its SHA-1 object id
12782 *
12783 * Regarding `ReadObjectResult`:
12784 *
12785 * - `oid` will be the same as the `oid` argument unless the `filepath` argument is provided, in which case it will be the oid of the tree or blob being returned.
12786 * - `type` of deflated objects is `'deflated'`, and `type` of wrapped objects is `'wrapped'`
12787 * - `format` is usually, but not always, the format you requested. Packfiles do not store each object individually compressed so if you end up reading the object from a packfile it will be returned in format 'content' even if you requested 'deflated' or 'wrapped'.
12788 * - `object` will be an actual Object if format is 'parsed' and the object is a commit, tree, or annotated tag. Blobs are still formatted as Buffers unless an encoding is provided in which case they'll be strings. If format is anything other than 'parsed', object will be a Buffer.
12789 * - `source` is the name of the packfile or loose object file where the object was found.
12790 *
12791 * The `format` parameter can have the following values:
12792 *
12793 * | param | description |
12794 * | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12795 * | 'deflated' | Return the raw deflate-compressed buffer for an object if possible. Useful for efficiently shuffling around loose objects when you don't care about the contents and can save time by not inflating them. |
12796 * | 'wrapped' | Return the inflated object buffer wrapped in the git object header if possible. This is the raw data used when calculating the SHA-1 object id of a git object. |
12797 * | 'content' | Return the object buffer without the git header. |
12798 * | 'parsed' | Returns a parsed representation of the object. |
12799 *
12800 * The result will be in one of the following schemas:
12801 *
12802 * ## `'deflated'` format
12803 *
12804 * {@link DeflatedObject typedef}
12805 *
12806 * ## `'wrapped'` format
12807 *
12808 * {@link WrappedObject typedef}
12809 *
12810 * ## `'content'` format
12811 *
12812 * {@link RawObject typedef}
12813 *
12814 * ## `'parsed'` format
12815 *
12816 * ### parsed `'blob'` type
12817 *
12818 * {@link ParsedBlobObject typedef}
12819 *
12820 * ### parsed `'commit'` type
12821 *
12822 * {@link ParsedCommitObject typedef}
12823 * {@link CommitObject typedef}
12824 *
12825 * ### parsed `'tree'` type
12826 *
12827 * {@link ParsedTreeObject typedef}
12828 * {@link TreeObject typedef}
12829 * {@link TreeEntry typedef}
12830 *
12831 * ### parsed `'tag'` type
12832 *
12833 * {@link ParsedTagObject typedef}
12834 * {@link TagObject typedef}
12835 *
12836 * @deprecated
12837 * > This command is overly complicated.
12838 * >
12839 * > If you know the type of object you are reading, use [`readBlob`](./readBlob.md), [`readCommit`](./readCommit.md), [`readTag`](./readTag.md), or [`readTree`](./readTree.md).
12840 *
12841 * @param {object} args
12842 * @param {FsClient} args.fs - a file system client
12843 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
12844 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
12845 * @param {string} args.oid - The SHA-1 object id to get
12846 * @param {'deflated' | 'wrapped' | 'content' | 'parsed'} [args.format = 'parsed'] - What format to return the object in. The choices are described in more detail below.
12847 * @param {string} [args.filepath] - Don't return the object with `oid` itself, but resolve `oid` to a tree and then return the object at that filepath. To return the root directory of a tree set filepath to `''`
12848 * @param {string} [args.encoding] - A convenience argument that only affects blobs. Instead of returning `object` as a buffer, it returns a string parsed using the given encoding.
12849 * @param {object} [args.cache] - a [cache](cache.md) object
12850 *
12851 * @returns {Promise<ReadObjectResult>} Resolves successfully with a git object description
12852 * @see ReadObjectResult
12853 *
12854 * @example
12855 * // Given a ransom SHA-1 object id, figure out what it is
12856 * let { type, object } = await git.readObject({
12857 * fs,
12858 * dir: '/tutorial',
12859 * oid: '0698a781a02264a6f37ba3ff41d78067eaf0f075'
12860 * })
12861 * switch (type) {
12862 * case 'commit': {
12863 * console.log(object)
12864 * break
12865 * }
12866 * case 'tree': {
12867 * console.log(object)
12868 * break
12869 * }
12870 * case 'blob': {
12871 * console.log(object)
12872 * break
12873 * }
12874 * case 'tag': {
12875 * console.log(object)
12876 * break
12877 * }
12878 * }
12879 *
12880 */
12881async function readObject({
12882 fs: _fs,
12883 dir,
12884 gitdir = join(dir, '.git'),
12885 oid,
12886 format = 'parsed',
12887 filepath = undefined,
12888 encoding = undefined,
12889 cache = {},
12890}) {
12891 try {
12892 assertParameter('fs', _fs);
12893 assertParameter('gitdir', gitdir);
12894 assertParameter('oid', oid);
12895
12896 const fs = new FileSystem(_fs);
12897 if (filepath !== undefined) {
12898 oid = await resolveFilepath({
12899 fs,
12900 cache,
12901 gitdir,
12902 oid,
12903 filepath,
12904 });
12905 }
12906 // GitObjectManager does not know how to parse content, so we tweak that parameter before passing it.
12907 const _format = format === 'parsed' ? 'content' : format;
12908 const result = await _readObject({
12909 fs,
12910 cache,
12911 gitdir,
12912 oid,
12913 format: _format,
12914 });
12915 result.oid = oid;
12916 if (format === 'parsed') {
12917 result.format = 'parsed';
12918 switch (result.type) {
12919 case 'commit':
12920 result.object = GitCommit.from(result.object).parse();
12921 break
12922 case 'tree':
12923 result.object = GitTree.from(result.object).entries();
12924 break
12925 case 'blob':
12926 // Here we consider returning a raw Buffer as the 'content' format
12927 // and returning a string as the 'parsed' format
12928 if (encoding) {
12929 result.object = result.object.toString(encoding);
12930 } else {
12931 result.object = new Uint8Array(result.object);
12932 result.format = 'content';
12933 }
12934 break
12935 case 'tag':
12936 result.object = GitAnnotatedTag.from(result.object).parse();
12937 break
12938 default:
12939 throw new ObjectTypeError(
12940 result.oid,
12941 result.type,
12942 'blob|commit|tag|tree'
12943 )
12944 }
12945 } else if (result.format === 'deflated' || result.format === 'wrapped') {
12946 result.type = result.format;
12947 }
12948 return result
12949 } catch (err) {
12950 err.caller = 'git.readObject';
12951 throw err
12952 }
12953}
12954
12955// @ts-check
12956
12957/**
12958 *
12959 * @typedef {Object} ReadTagResult - The object returned has the following schema:
12960 * @property {string} oid - SHA-1 object id of this tag
12961 * @property {TagObject} tag - the parsed tag object
12962 * @property {string} payload - PGP signing payload
12963 */
12964
12965/**
12966 * @param {object} args
12967 * @param {import('../models/FileSystem.js').FileSystem} args.fs
12968 * @param {any} args.cache
12969 * @param {string} args.gitdir
12970 * @param {string} args.oid
12971 *
12972 * @returns {Promise<ReadTagResult>}
12973 */
12974async function _readTag({ fs, cache, gitdir, oid }) {
12975 const { type, object } = await _readObject({
12976 fs,
12977 cache,
12978 gitdir,
12979 oid,
12980 format: 'content',
12981 });
12982 if (type !== 'tag') {
12983 throw new ObjectTypeError(oid, type, 'tag')
12984 }
12985 const tag = GitAnnotatedTag.from(object);
12986 const result = {
12987 oid,
12988 tag: tag.parse(),
12989 payload: tag.payload(),
12990 };
12991 // @ts-ignore
12992 return result
12993}
12994
12995/**
12996 *
12997 * @typedef {Object} ReadTagResult - The object returned has the following schema:
12998 * @property {string} oid - SHA-1 object id of this tag
12999 * @property {TagObject} tag - the parsed tag object
13000 * @property {string} payload - PGP signing payload
13001 */
13002
13003/**
13004 * Read an annotated tag object directly
13005 *
13006 * @param {object} args
13007 * @param {FsClient} args.fs - a file system client
13008 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13009 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13010 * @param {string} args.oid - The SHA-1 object id to get
13011 * @param {object} [args.cache] - a [cache](cache.md) object
13012 *
13013 * @returns {Promise<ReadTagResult>} Resolves successfully with a git object description
13014 * @see ReadTagResult
13015 * @see TagObject
13016 *
13017 */
13018async function readTag({
13019 fs,
13020 dir,
13021 gitdir = join(dir, '.git'),
13022 oid,
13023 cache = {},
13024}) {
13025 try {
13026 assertParameter('fs', fs);
13027 assertParameter('gitdir', gitdir);
13028 assertParameter('oid', oid);
13029
13030 return await _readTag({
13031 fs: new FileSystem(fs),
13032 cache,
13033 gitdir,
13034 oid,
13035 })
13036 } catch (err) {
13037 err.caller = 'git.readTag';
13038 throw err
13039 }
13040}
13041
13042// @ts-check
13043
13044/**
13045 *
13046 * @typedef {Object} ReadTreeResult - The object returned has the following schema:
13047 * @property {string} oid - SHA-1 object id of this tree
13048 * @property {TreeObject} tree - the parsed tree object
13049 */
13050
13051/**
13052 * Read a tree object directly
13053 *
13054 * @param {object} args
13055 * @param {FsClient} args.fs - a file system client
13056 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13057 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13058 * @param {string} args.oid - The SHA-1 object id to get. Annotated tags and commits are peeled.
13059 * @param {string} [args.filepath] - Don't return the object with `oid` itself, but resolve `oid` to a tree and then return the tree object at that filepath.
13060 * @param {object} [args.cache] - a [cache](cache.md) object
13061 *
13062 * @returns {Promise<ReadTreeResult>} Resolves successfully with a git tree object
13063 * @see ReadTreeResult
13064 * @see TreeObject
13065 * @see TreeEntry
13066 *
13067 */
13068async function readTree({
13069 fs,
13070 dir,
13071 gitdir = join(dir, '.git'),
13072 oid,
13073 filepath = undefined,
13074 cache = {},
13075}) {
13076 try {
13077 assertParameter('fs', fs);
13078 assertParameter('gitdir', gitdir);
13079 assertParameter('oid', oid);
13080
13081 return await _readTree({
13082 fs: new FileSystem(fs),
13083 cache,
13084 gitdir,
13085 oid,
13086 filepath,
13087 })
13088 } catch (err) {
13089 err.caller = 'git.readTree';
13090 throw err
13091 }
13092}
13093
13094// @ts-check
13095
13096/**
13097 * Remove a file from the git index (aka staging area)
13098 *
13099 * Note that this does NOT delete the file in the working directory.
13100 *
13101 * @param {object} args
13102 * @param {FsClient} args.fs - a file system client
13103 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13104 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13105 * @param {string} args.filepath - The path to the file to remove from the index
13106 * @param {object} [args.cache] - a [cache](cache.md) object
13107 *
13108 * @returns {Promise<void>} Resolves successfully once the git index has been updated
13109 *
13110 * @example
13111 * await git.remove({ fs, dir: '/tutorial', filepath: 'README.md' })
13112 * console.log('done')
13113 *
13114 */
13115async function remove({
13116 fs: _fs,
13117 dir,
13118 gitdir = join(dir, '.git'),
13119 filepath,
13120 cache = {},
13121}) {
13122 try {
13123 assertParameter('fs', _fs);
13124 assertParameter('gitdir', gitdir);
13125 assertParameter('filepath', filepath);
13126
13127 await GitIndexManager.acquire(
13128 { fs: new FileSystem(_fs), gitdir, cache },
13129 async function(index) {
13130 index.delete({ filepath });
13131 }
13132 );
13133 } catch (err) {
13134 err.caller = 'git.remove';
13135 throw err
13136 }
13137}
13138
13139// @ts-check
13140
13141/**
13142 * @param {object} args
13143 * @param {import('../models/FileSystem.js').FileSystem} args.fs
13144 * @param {object} args.cache
13145 * @param {SignCallback} [args.onSign]
13146 * @param {string} [args.dir]
13147 * @param {string} [args.gitdir=join(dir,'.git')]
13148 * @param {string} [args.ref]
13149 * @param {string} args.oid
13150 * @param {Object} args.author
13151 * @param {string} args.author.name
13152 * @param {string} args.author.email
13153 * @param {number} args.author.timestamp
13154 * @param {number} args.author.timezoneOffset
13155 * @param {Object} args.committer
13156 * @param {string} args.committer.name
13157 * @param {string} args.committer.email
13158 * @param {number} args.committer.timestamp
13159 * @param {number} args.committer.timezoneOffset
13160 * @param {string} [args.signingKey]
13161 *
13162 * @returns {Promise<string>}
13163 */
13164
13165async function _removeNote({
13166 fs,
13167 cache,
13168 onSign,
13169 gitdir,
13170 ref = 'refs/notes/commits',
13171 oid,
13172 author,
13173 committer,
13174 signingKey,
13175}) {
13176 // Get the current note commit
13177 let parent;
13178 try {
13179 parent = await GitRefManager.resolve({ gitdir, fs, ref });
13180 } catch (err) {
13181 if (!(err instanceof NotFoundError)) {
13182 throw err
13183 }
13184 }
13185
13186 // I'm using the "empty tree" magic number here for brevity
13187 const result = await _readTree({
13188 fs,
13189 gitdir,
13190 oid: parent || '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
13191 });
13192 let tree = result.tree;
13193
13194 // Remove the note blob entry from the tree
13195 tree = tree.filter(entry => entry.path !== oid);
13196
13197 // Create the new note tree
13198 const treeOid = await _writeTree({
13199 fs,
13200 gitdir,
13201 tree,
13202 });
13203
13204 // Create the new note commit
13205 const commitOid = await _commit({
13206 fs,
13207 cache,
13208 onSign,
13209 gitdir,
13210 ref,
13211 tree: treeOid,
13212 parent: parent && [parent],
13213 message: `Note removed by 'isomorphic-git removeNote'\n`,
13214 author,
13215 committer,
13216 signingKey,
13217 });
13218
13219 return commitOid
13220}
13221
13222// @ts-check
13223
13224/**
13225 * Remove an object note
13226 *
13227 * @param {object} args
13228 * @param {FsClient} args.fs - a file system client
13229 * @param {SignCallback} [args.onSign] - a PGP signing implementation
13230 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13231 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13232 * @param {string} [args.ref] - The notes ref to look under
13233 * @param {string} args.oid - The SHA-1 object id of the object to remove the note from.
13234 * @param {Object} [args.author] - The details about the author.
13235 * @param {string} [args.author.name] - Default is `user.name` config.
13236 * @param {string} [args.author.email] - Default is `user.email` config.
13237 * @param {number} [args.author.timestamp=Math.floor(Date.now()/1000)] - Set the author timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
13238 * @param {number} [args.author.timezoneOffset] - Set the author timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
13239 * @param {Object} [args.committer = author] - The details about the note committer, in the same format as the author parameter. If not specified, the author details are used.
13240 * @param {string} [args.committer.name] - Default is `user.name` config.
13241 * @param {string} [args.committer.email] - Default is `user.email` config.
13242 * @param {number} [args.committer.timestamp=Math.floor(Date.now()/1000)] - Set the committer timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
13243 * @param {number} [args.committer.timezoneOffset] - Set the committer timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
13244 * @param {string} [args.signingKey] - Sign the tag object using this private PGP key.
13245 * @param {object} [args.cache] - a [cache](cache.md) object
13246 *
13247 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the commit object for the note removal.
13248 */
13249
13250async function removeNote({
13251 fs: _fs,
13252 onSign,
13253 dir,
13254 gitdir = join(dir, '.git'),
13255 ref = 'refs/notes/commits',
13256 oid,
13257 author: _author,
13258 committer: _committer,
13259 signingKey,
13260 cache = {},
13261}) {
13262 try {
13263 assertParameter('fs', _fs);
13264 assertParameter('gitdir', gitdir);
13265 assertParameter('oid', oid);
13266
13267 const fs = new FileSystem(_fs);
13268
13269 const author = await normalizeAuthorObject({ fs, gitdir, author: _author });
13270 if (!author) throw new MissingNameError('author')
13271
13272 const committer = await normalizeCommitterObject({
13273 fs,
13274 gitdir,
13275 author,
13276 committer: _committer,
13277 });
13278 if (!committer) throw new MissingNameError('committer')
13279
13280 return await _removeNote({
13281 fs,
13282 cache,
13283 onSign,
13284 gitdir,
13285 ref,
13286 oid,
13287 author,
13288 committer,
13289 signingKey,
13290 })
13291 } catch (err) {
13292 err.caller = 'git.removeNote';
13293 throw err
13294 }
13295}
13296
13297// @ts-check
13298
13299/**
13300 * Rename a branch
13301 *
13302 * @param {object} args
13303 * @param {import('../models/FileSystem.js').FileSystem} args.fs
13304 * @param {string} args.gitdir
13305 * @param {string} args.ref - The name of the new branch
13306 * @param {string} args.oldref - The name of the old branch
13307 * @param {boolean} [args.checkout = false]
13308 *
13309 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
13310 */
13311async function _renameBranch({
13312 fs,
13313 gitdir,
13314 oldref,
13315 ref,
13316 checkout = false,
13317}) {
13318 if (ref !== cleanGitRef.clean(ref)) {
13319 throw new InvalidRefNameError(ref, cleanGitRef.clean(ref))
13320 }
13321
13322 if (oldref !== cleanGitRef.clean(oldref)) {
13323 throw new InvalidRefNameError(oldref, cleanGitRef.clean(oldref))
13324 }
13325
13326 const fulloldref = `refs/heads/${oldref}`;
13327 const fullnewref = `refs/heads/${ref}`;
13328
13329 const newexist = await GitRefManager.exists({ fs, gitdir, ref: fullnewref });
13330
13331 if (newexist) {
13332 throw new AlreadyExistsError('branch', ref, false)
13333 }
13334
13335 const value = await GitRefManager.resolve({
13336 fs,
13337 gitdir,
13338 ref: fulloldref,
13339 depth: 1,
13340 });
13341
13342 await GitRefManager.writeRef({ fs, gitdir, ref: fullnewref, value });
13343 await GitRefManager.deleteRef({ fs, gitdir, ref: fulloldref });
13344
13345 const fullCurrentBranchRef = await _currentBranch({
13346 fs,
13347 gitdir,
13348 fullname: true,
13349 });
13350 const isCurrentBranch = fullCurrentBranchRef === fulloldref;
13351
13352 if (checkout || isCurrentBranch) {
13353 // Update HEAD
13354 await GitRefManager.writeSymbolicRef({
13355 fs,
13356 gitdir,
13357 ref: 'HEAD',
13358 value: fullnewref,
13359 });
13360 }
13361}
13362
13363// @ts-check
13364
13365/**
13366 * Rename a branch
13367 *
13368 * @param {object} args
13369 * @param {FsClient} args.fs - a file system implementation
13370 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13371 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13372 * @param {string} args.ref - What to name the branch
13373 * @param {string} args.oldref - What the name of the branch was
13374 * @param {boolean} [args.checkout = false] - Update `HEAD` to point at the newly created branch
13375 *
13376 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
13377 *
13378 * @example
13379 * await git.renameBranch({ fs, dir: '/tutorial', ref: 'main', oldref: 'master' })
13380 * console.log('done')
13381 *
13382 */
13383async function renameBranch({
13384 fs,
13385 dir,
13386 gitdir = join(dir, '.git'),
13387 ref,
13388 oldref,
13389 checkout = false,
13390}) {
13391 try {
13392 assertParameter('fs', fs);
13393 assertParameter('gitdir', gitdir);
13394 assertParameter('ref', ref);
13395 assertParameter('oldref', oldref);
13396 return await _renameBranch({
13397 fs: new FileSystem(fs),
13398 gitdir,
13399 ref,
13400 oldref,
13401 checkout,
13402 })
13403 } catch (err) {
13404 err.caller = 'git.renameBranch';
13405 throw err
13406 }
13407}
13408
13409async function hashObject$1({ gitdir, type, object }) {
13410 return shasum(GitObject.wrap({ type, object }))
13411}
13412
13413// @ts-check
13414
13415/**
13416 * Reset a file in the git index (aka staging area)
13417 *
13418 * Note that this does NOT modify the file in the working directory.
13419 *
13420 * @param {object} args
13421 * @param {FsClient} args.fs - a file system client
13422 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13423 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13424 * @param {string} args.filepath - The path to the file to reset in the index
13425 * @param {string} [args.ref = 'HEAD'] - A ref to the commit to use
13426 * @param {object} [args.cache] - a [cache](cache.md) object
13427 *
13428 * @returns {Promise<void>} Resolves successfully once the git index has been updated
13429 *
13430 * @example
13431 * await git.resetIndex({ fs, dir: '/tutorial', filepath: 'README.md' })
13432 * console.log('done')
13433 *
13434 */
13435async function resetIndex({
13436 fs: _fs,
13437 dir,
13438 gitdir = join(dir, '.git'),
13439 filepath,
13440 ref,
13441 cache = {},
13442}) {
13443 try {
13444 assertParameter('fs', _fs);
13445 assertParameter('gitdir', gitdir);
13446 assertParameter('filepath', filepath);
13447
13448 const fs = new FileSystem(_fs);
13449
13450 let oid;
13451 let workdirOid;
13452
13453 try {
13454 // Resolve commit
13455 oid = await GitRefManager.resolve({ fs, gitdir, ref: ref || 'HEAD' });
13456 } catch (e) {
13457 if (ref) {
13458 // Only throw the error if a ref is explicitly provided
13459 throw e
13460 }
13461 }
13462
13463 // Not having an oid at this point means `resetIndex()` was called without explicit `ref` on a new git
13464 // repository. If that happens, we can skip resolving the file path.
13465 if (oid) {
13466 try {
13467 // Resolve blob
13468 oid = await resolveFilepath({
13469 fs,
13470 cache,
13471 gitdir,
13472 oid,
13473 filepath,
13474 });
13475 } catch (e) {
13476 // This means we're resetting the file to a "deleted" state
13477 oid = null;
13478 }
13479 }
13480
13481 // For files that aren't in the workdir use zeros
13482 let stats = {
13483 ctime: new Date(0),
13484 mtime: new Date(0),
13485 dev: 0,
13486 ino: 0,
13487 mode: 0,
13488 uid: 0,
13489 gid: 0,
13490 size: 0,
13491 };
13492 // If the file exists in the workdir...
13493 const object = dir && (await fs.read(join(dir, filepath)));
13494 if (object) {
13495 // ... and has the same hash as the desired state...
13496 workdirOid = await hashObject$1({
13497 gitdir,
13498 type: 'blob',
13499 object,
13500 });
13501 if (oid === workdirOid) {
13502 // ... use the workdir Stats object
13503 stats = await fs.lstat(join(dir, filepath));
13504 }
13505 }
13506 await GitIndexManager.acquire({ fs, gitdir, cache }, async function(index) {
13507 index.delete({ filepath });
13508 if (oid) {
13509 index.insert({ filepath, stats, oid });
13510 }
13511 });
13512 } catch (err) {
13513 err.caller = 'git.reset';
13514 throw err
13515 }
13516}
13517
13518// @ts-check
13519
13520/**
13521 * Get the value of a symbolic ref or resolve a ref to its SHA-1 object id
13522 *
13523 * @param {object} args
13524 * @param {FsClient} args.fs - a file system client
13525 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13526 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13527 * @param {string} args.ref - The ref to resolve
13528 * @param {number} [args.depth = undefined] - How many symbolic references to follow before returning
13529 *
13530 * @returns {Promise<string>} Resolves successfully with a SHA-1 object id or the value of a symbolic ref
13531 *
13532 * @example
13533 * let currentCommit = await git.resolveRef({ fs, dir: '/tutorial', ref: 'HEAD' })
13534 * console.log(currentCommit)
13535 * let currentBranch = await git.resolveRef({ fs, dir: '/tutorial', ref: 'HEAD', depth: 2 })
13536 * console.log(currentBranch)
13537 *
13538 */
13539async function resolveRef({
13540 fs,
13541 dir,
13542 gitdir = join(dir, '.git'),
13543 ref,
13544 depth,
13545}) {
13546 try {
13547 assertParameter('fs', fs);
13548 assertParameter('gitdir', gitdir);
13549 assertParameter('ref', ref);
13550
13551 const oid = await GitRefManager.resolve({
13552 fs: new FileSystem(fs),
13553 gitdir,
13554 ref,
13555 depth,
13556 });
13557 return oid
13558 } catch (err) {
13559 err.caller = 'git.resolveRef';
13560 throw err
13561 }
13562}
13563
13564// @ts-check
13565
13566/**
13567 * Write an entry to the git config files.
13568 *
13569 * *Caveats:*
13570 * - Currently only the local `$GIT_DIR/config` file can be read or written. However support for the global `~/.gitconfig` and system `$(prefix)/etc/gitconfig` will be added in the future.
13571 * - The current parser does not support the more exotic features of the git-config file format such as `[include]` and `[includeIf]`.
13572 *
13573 * @param {Object} args
13574 * @param {FsClient} args.fs - a file system implementation
13575 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
13576 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13577 * @param {string} args.path - The key of the git config entry
13578 * @param {string | boolean | number | void} args.value - A value to store at that path. (Use `undefined` as the value to delete a config entry.)
13579 * @param {boolean} [args.append = false] - If true, will append rather than replace when setting (use with multi-valued config options).
13580 *
13581 * @returns {Promise<void>} Resolves successfully when operation completed
13582 *
13583 * @example
13584 * // Write config value
13585 * await git.setConfig({
13586 * fs,
13587 * dir: '/tutorial',
13588 * path: 'user.name',
13589 * value: 'Mr. Test'
13590 * })
13591 *
13592 * // Print out config file
13593 * let file = await fs.promises.readFile('/tutorial/.git/config', 'utf8')
13594 * console.log(file)
13595 *
13596 * // Delete a config entry
13597 * await git.setConfig({
13598 * fs,
13599 * dir: '/tutorial',
13600 * path: 'user.name',
13601 * value: undefined
13602 * })
13603 *
13604 * // Print out config file
13605 * file = await fs.promises.readFile('/tutorial/.git/config', 'utf8')
13606 * console.log(file)
13607 */
13608async function setConfig({
13609 fs: _fs,
13610 dir,
13611 gitdir = join(dir, '.git'),
13612 path,
13613 value,
13614 append = false,
13615}) {
13616 try {
13617 assertParameter('fs', _fs);
13618 assertParameter('gitdir', gitdir);
13619 assertParameter('path', path);
13620 // assertParameter('value', value) // We actually allow 'undefined' as a value to unset/delete
13621
13622 const fs = new FileSystem(_fs);
13623 const config = await GitConfigManager.get({ fs, gitdir });
13624 if (append) {
13625 await config.append(path, value);
13626 } else {
13627 await config.set(path, value);
13628 }
13629 await GitConfigManager.save({ fs, gitdir, config });
13630 } catch (err) {
13631 err.caller = 'git.setConfig';
13632 throw err
13633 }
13634}
13635
13636// @ts-check
13637
13638/**
13639 * Tell whether a file has been changed
13640 *
13641 * The possible resolve values are:
13642 *
13643 * | status | description |
13644 * | --------------------- | ------------------------------------------------------------------------------------- |
13645 * | `"ignored"` | file ignored by a .gitignore rule |
13646 * | `"unmodified"` | file unchanged from HEAD commit |
13647 * | `"*modified"` | file has modifications, not yet staged |
13648 * | `"*deleted"` | file has been removed, but the removal is not yet staged |
13649 * | `"*added"` | file is untracked, not yet staged |
13650 * | `"absent"` | file not present in HEAD commit, staging area, or working dir |
13651 * | `"modified"` | file has modifications, staged |
13652 * | `"deleted"` | file has been removed, staged |
13653 * | `"added"` | previously untracked file, staged |
13654 * | `"*unmodified"` | working dir and HEAD commit match, but index differs |
13655 * | `"*absent"` | file not present in working dir or HEAD commit, but present in the index |
13656 * | `"*undeleted"` | file was deleted from the index, but is still in the working dir |
13657 * | `"*undeletemodified"` | file was deleted from the index, but is present with modifications in the working dir |
13658 *
13659 * @param {object} args
13660 * @param {FsClient} args.fs - a file system client
13661 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
13662 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13663 * @param {string} args.filepath - The path to the file to query
13664 * @param {object} [args.cache] - a [cache](cache.md) object
13665 *
13666 * @returns {Promise<'ignored'|'unmodified'|'*modified'|'*deleted'|'*added'|'absent'|'modified'|'deleted'|'added'|'*unmodified'|'*absent'|'*undeleted'|'*undeletemodified'>} Resolves successfully with the file's git status
13667 *
13668 * @example
13669 * let status = await git.status({ fs, dir: '/tutorial', filepath: 'README.md' })
13670 * console.log(status)
13671 *
13672 */
13673async function status({
13674 fs: _fs,
13675 dir,
13676 gitdir = join(dir, '.git'),
13677 filepath,
13678 cache = {},
13679}) {
13680 try {
13681 assertParameter('fs', _fs);
13682 assertParameter('gitdir', gitdir);
13683 assertParameter('filepath', filepath);
13684
13685 const fs = new FileSystem(_fs);
13686 const ignored = await GitIgnoreManager.isIgnored({
13687 fs,
13688 gitdir,
13689 dir,
13690 filepath,
13691 });
13692 if (ignored) {
13693 return 'ignored'
13694 }
13695 const headTree = await getHeadTree({ fs, cache, gitdir });
13696 const treeOid = await getOidAtPath({
13697 fs,
13698 cache,
13699 gitdir,
13700 tree: headTree,
13701 path: filepath,
13702 });
13703 const indexEntry = await GitIndexManager.acquire(
13704 { fs, gitdir, cache },
13705 async function(index) {
13706 for (const entry of index) {
13707 if (entry.path === filepath) return entry
13708 }
13709 return null
13710 }
13711 );
13712 const stats = await fs.lstat(join(dir, filepath));
13713
13714 const H = treeOid !== null; // head
13715 const I = indexEntry !== null; // index
13716 const W = stats !== null; // working dir
13717
13718 const getWorkdirOid = async () => {
13719 if (I && !compareStats(indexEntry, stats)) {
13720 return indexEntry.oid
13721 } else {
13722 const object = await fs.read(join(dir, filepath));
13723 const workdirOid = await hashObject$1({
13724 gitdir,
13725 type: 'blob',
13726 object,
13727 });
13728 // If the oid in the index === working dir oid but stats differed update cache
13729 if (I && indexEntry.oid === workdirOid) {
13730 // and as long as our fs.stats aren't bad.
13731 // size of -1 happens over a BrowserFS HTTP Backend that doesn't serve Content-Length headers
13732 // (like the Karma webserver) because BrowserFS HTTP Backend uses HTTP HEAD requests to do fs.stat
13733 if (stats.size !== -1) {
13734 // We don't await this so we can return faster for one-off cases.
13735 GitIndexManager.acquire({ fs, gitdir, cache }, async function(
13736 index
13737 ) {
13738 index.insert({ filepath, stats, oid: workdirOid });
13739 });
13740 }
13741 }
13742 return workdirOid
13743 }
13744 };
13745
13746 if (!H && !W && !I) return 'absent' // ---
13747 if (!H && !W && I) return '*absent' // -A-
13748 if (!H && W && !I) return '*added' // --A
13749 if (!H && W && I) {
13750 const workdirOid = await getWorkdirOid();
13751 // @ts-ignore
13752 return workdirOid === indexEntry.oid ? 'added' : '*added' // -AA : -AB
13753 }
13754 if (H && !W && !I) return 'deleted' // A--
13755 if (H && !W && I) {
13756 // @ts-ignore
13757 return treeOid === indexEntry.oid ? '*deleted' : '*deleted' // AA- : AB-
13758 }
13759 if (H && W && !I) {
13760 const workdirOid = await getWorkdirOid();
13761 return workdirOid === treeOid ? '*undeleted' : '*undeletemodified' // A-A : A-B
13762 }
13763 if (H && W && I) {
13764 const workdirOid = await getWorkdirOid();
13765 if (workdirOid === treeOid) {
13766 // @ts-ignore
13767 return workdirOid === indexEntry.oid ? 'unmodified' : '*unmodified' // AAA : ABA
13768 } else {
13769 // @ts-ignore
13770 return workdirOid === indexEntry.oid ? 'modified' : '*modified' // ABB : AAB
13771 }
13772 }
13773 /*
13774 ---
13775 -A-
13776 --A
13777 -AA
13778 -AB
13779 A--
13780 AA-
13781 AB-
13782 A-A
13783 A-B
13784 AAA
13785 ABA
13786 ABB
13787 AAB
13788 */
13789 } catch (err) {
13790 err.caller = 'git.status';
13791 throw err
13792 }
13793}
13794
13795async function getOidAtPath({ fs, cache, gitdir, tree, path }) {
13796 if (typeof path === 'string') path = path.split('/');
13797 const dirname = path.shift();
13798 for (const entry of tree) {
13799 if (entry.path === dirname) {
13800 if (path.length === 0) {
13801 return entry.oid
13802 }
13803 const { type, object } = await _readObject({
13804 fs,
13805 cache,
13806 gitdir,
13807 oid: entry.oid,
13808 });
13809 if (type === 'tree') {
13810 const tree = GitTree.from(object);
13811 return getOidAtPath({ fs, cache, gitdir, tree, path })
13812 }
13813 if (type === 'blob') {
13814 throw new ObjectTypeError(entry.oid, type, 'blob', path.join('/'))
13815 }
13816 }
13817 }
13818 return null
13819}
13820
13821async function getHeadTree({ fs, cache, gitdir }) {
13822 // Get the tree from the HEAD commit.
13823 let oid;
13824 try {
13825 oid = await GitRefManager.resolve({ fs, gitdir, ref: 'HEAD' });
13826 } catch (e) {
13827 // Handle fresh branches with no commits
13828 if (e instanceof NotFoundError) {
13829 return []
13830 }
13831 }
13832 const { tree } = await _readTree({ fs, cache, gitdir, oid });
13833 return tree
13834}
13835
13836// @ts-check
13837
13838/**
13839 * Efficiently get the status of multiple files at once.
13840 *
13841 * The returned `StatusMatrix` is admittedly not the easiest format to read.
13842 * However it conveys a large amount of information in dense format that should make it easy to create reports about the current state of the repository;
13843 * without having to do multiple, time-consuming isomorphic-git calls.
13844 * My hope is that the speed and flexibility of the function will make up for the learning curve of interpreting the return value.
13845 *
13846 * ```js live
13847 * // get the status of all the files in 'src'
13848 * let status = await git.statusMatrix({
13849 * fs,
13850 * dir: '/tutorial',
13851 * filter: f => f.startsWith('src/')
13852 * })
13853 * console.log(status)
13854 * ```
13855 *
13856 * ```js live
13857 * // get the status of all the JSON and Markdown files
13858 * let status = await git.statusMatrix({
13859 * fs,
13860 * dir: '/tutorial',
13861 * filter: f => f.endsWith('.json') || f.endsWith('.md')
13862 * })
13863 * console.log(status)
13864 * ```
13865 *
13866 * The result is returned as a 2D array.
13867 * The outer array represents the files and/or blobs in the repo, in alphabetical order.
13868 * The inner arrays describe the status of the file:
13869 * the first value is the filepath, and the next three are integers
13870 * representing the HEAD status, WORKDIR status, and STAGE status of the entry.
13871 *
13872 * ```js
13873 * // example StatusMatrix
13874 * [
13875 * ["a.txt", 0, 2, 0], // new, untracked
13876 * ["b.txt", 0, 2, 2], // added, staged
13877 * ["c.txt", 0, 2, 3], // added, staged, with unstaged changes
13878 * ["d.txt", 1, 1, 1], // unmodified
13879 * ["e.txt", 1, 2, 1], // modified, unstaged
13880 * ["f.txt", 1, 2, 2], // modified, staged
13881 * ["g.txt", 1, 2, 3], // modified, staged, with unstaged changes
13882 * ["h.txt", 1, 0, 1], // deleted, unstaged
13883 * ["i.txt", 1, 0, 0], // deleted, staged
13884 * ]
13885 * ```
13886 *
13887 * - The HEAD status is either absent (0) or present (1).
13888 * - The WORKDIR status is either absent (0), identical to HEAD (1), or different from HEAD (2).
13889 * - The STAGE status is either absent (0), identical to HEAD (1), identical to WORKDIR (2), or different from WORKDIR (3).
13890 *
13891 * ```ts
13892 * type Filename = string
13893 * type HeadStatus = 0 | 1
13894 * type WorkdirStatus = 0 | 1 | 2
13895 * type StageStatus = 0 | 1 | 2 | 3
13896 *
13897 * type StatusRow = [Filename, HeadStatus, WorkdirStatus, StageStatus]
13898 *
13899 * type StatusMatrix = StatusRow[]
13900 * ```
13901 *
13902 * > Think of the natural progression of file modifications as being from HEAD (previous) -> WORKDIR (current) -> STAGE (next).
13903 * > Then HEAD is "version 1", WORKDIR is "version 2", and STAGE is "version 3".
13904 * > Then, imagine a "version 0" which is before the file was created.
13905 * > Then the status value in each column corresponds to the oldest version of the file it is identical to.
13906 * > (For a file to be identical to "version 0" means the file is deleted.)
13907 *
13908 * Here are some examples of queries you can answer using the result:
13909 *
13910 * #### Q: What files have been deleted?
13911 * ```js
13912 * const FILE = 0, WORKDIR = 2
13913 *
13914 * const filenames = (await statusMatrix({ dir }))
13915 * .filter(row => row[WORKDIR] === 0)
13916 * .map(row => row[FILE])
13917 * ```
13918 *
13919 * #### Q: What files have unstaged changes?
13920 * ```js
13921 * const FILE = 0, WORKDIR = 2, STAGE = 3
13922 *
13923 * const filenames = (await statusMatrix({ dir }))
13924 * .filter(row => row[WORKDIR] !== row[STAGE])
13925 * .map(row => row[FILE])
13926 * ```
13927 *
13928 * #### Q: What files have been modified since the last commit?
13929 * ```js
13930 * const FILE = 0, HEAD = 1, WORKDIR = 2
13931 *
13932 * const filenames = (await statusMatrix({ dir }))
13933 * .filter(row => row[HEAD] !== row[WORKDIR])
13934 * .map(row => row[FILE])
13935 * ```
13936 *
13937 * #### Q: What files will NOT be changed if I commit right now?
13938 * ```js
13939 * const FILE = 0, HEAD = 1, STAGE = 3
13940 *
13941 * const filenames = (await statusMatrix({ dir }))
13942 * .filter(row => row[HEAD] === row[STAGE])
13943 * .map(row => row[FILE])
13944 * ```
13945 *
13946 * For reference, here are all possible combinations:
13947 *
13948 * | HEAD | WORKDIR | STAGE | `git status --short` equivalent |
13949 * | ---- | ------- | ----- | ------------------------------- |
13950 * | 0 | 0 | 0 | `` |
13951 * | 0 | 0 | 3 | `AD` |
13952 * | 0 | 2 | 0 | `??` |
13953 * | 0 | 2 | 2 | `A ` |
13954 * | 0 | 2 | 3 | `AM` |
13955 * | 1 | 0 | 0 | `D ` |
13956 * | 1 | 0 | 1 | ` D` |
13957 * | 1 | 0 | 3 | `MD` |
13958 * | 1 | 1 | 0 | `D ` + `??` |
13959 * | 1 | 1 | 1 | `` |
13960 * | 1 | 1 | 3 | `MM` |
13961 * | 1 | 2 | 0 | `D ` + `??` |
13962 * | 1 | 2 | 1 | ` M` |
13963 * | 1 | 2 | 2 | `M ` |
13964 * | 1 | 2 | 3 | `MM` |
13965 *
13966 * @param {object} args
13967 * @param {FsClient} args.fs - a file system client
13968 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
13969 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
13970 * @param {string} [args.ref = 'HEAD'] - Optionally specify a different commit to compare against the workdir and stage instead of the HEAD
13971 * @param {string[]} [args.filepaths = ['.']] - Limit the query to the given files and directories
13972 * @param {function(string): boolean} [args.filter] - Filter the results to only those whose filepath matches a function.
13973 * @param {object} [args.cache] - a [cache](cache.md) object
13974 * @param {boolean} [args.ignored = false] - include ignored files in the result
13975 *
13976 * @returns {Promise<Array<StatusRow>>} Resolves with a status matrix, described below.
13977 * @see StatusRow
13978 */
13979async function statusMatrix({
13980 fs: _fs,
13981 dir,
13982 gitdir = join(dir, '.git'),
13983 ref = 'HEAD',
13984 filepaths = ['.'],
13985 filter,
13986 cache = {},
13987 ignored: shouldIgnore = false,
13988}) {
13989 try {
13990 assertParameter('fs', _fs);
13991 assertParameter('gitdir', gitdir);
13992 assertParameter('ref', ref);
13993
13994 const fs = new FileSystem(_fs);
13995 return await _walk({
13996 fs,
13997 cache,
13998 dir,
13999 gitdir,
14000 trees: [TREE({ ref }), WORKDIR(), STAGE()],
14001 map: async function(filepath, [head, workdir, stage]) {
14002 // Ignore ignored files, but only if they are not already tracked.
14003 if (!head && !stage && workdir) {
14004 if (!shouldIgnore) {
14005 const isIgnored = await GitIgnoreManager.isIgnored({
14006 fs,
14007 dir,
14008 filepath,
14009 });
14010 if (isIgnored) {
14011 return null
14012 }
14013 }
14014 }
14015 // match against base paths
14016 if (!filepaths.some(base => worthWalking(filepath, base))) {
14017 return null
14018 }
14019 // Late filter against file names
14020 if (filter) {
14021 if (!filter(filepath)) return
14022 }
14023
14024 const [headType, workdirType, stageType] = await Promise.all([
14025 head && head.type(),
14026 workdir && workdir.type(),
14027 stage && stage.type(),
14028 ]);
14029
14030 const isBlob = [headType, workdirType, stageType].includes('blob');
14031
14032 // For now, bail on directories unless the file is also a blob in another tree
14033 if ((headType === 'tree' || headType === 'special') && !isBlob) return
14034 if (headType === 'commit') return null
14035
14036 if ((workdirType === 'tree' || workdirType === 'special') && !isBlob)
14037 return
14038
14039 if (stageType === 'commit') return null
14040 if ((stageType === 'tree' || stageType === 'special') && !isBlob) return
14041
14042 // Figure out the oids for files, using the staged oid for the working dir oid if the stats match.
14043 const headOid = headType === 'blob' ? await head.oid() : undefined;
14044 const stageOid = stageType === 'blob' ? await stage.oid() : undefined;
14045 let workdirOid;
14046 if (
14047 headType !== 'blob' &&
14048 workdirType === 'blob' &&
14049 stageType !== 'blob'
14050 ) {
14051 // We don't actually NEED the sha. Any sha will do
14052 // TODO: update this logic to handle N trees instead of just 3.
14053 workdirOid = '42';
14054 } else if (workdirType === 'blob') {
14055 workdirOid = await workdir.oid();
14056 }
14057 const entry = [undefined, headOid, workdirOid, stageOid];
14058 const result = entry.map(value => entry.indexOf(value));
14059 result.shift(); // remove leading undefined entry
14060 return [filepath, ...result]
14061 },
14062 })
14063 } catch (err) {
14064 err.caller = 'git.statusMatrix';
14065 throw err
14066 }
14067}
14068
14069// @ts-check
14070
14071/**
14072 * Create a lightweight tag
14073 *
14074 * @param {object} args
14075 * @param {FsClient} args.fs - a file system client
14076 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14077 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14078 * @param {string} args.ref - What to name the tag
14079 * @param {string} [args.object = 'HEAD'] - What oid the tag refers to. (Will resolve to oid if value is a ref.) By default, the commit object which is referred by the current `HEAD` is used.
14080 * @param {boolean} [args.force = false] - Instead of throwing an error if a tag named `ref` already exists, overwrite the existing tag.
14081 *
14082 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
14083 *
14084 * @example
14085 * await git.tag({ fs, dir: '/tutorial', ref: 'test-tag' })
14086 * console.log('done')
14087 *
14088 */
14089async function tag({
14090 fs: _fs,
14091 dir,
14092 gitdir = join(dir, '.git'),
14093 ref,
14094 object,
14095 force = false,
14096}) {
14097 try {
14098 assertParameter('fs', _fs);
14099 assertParameter('gitdir', gitdir);
14100 assertParameter('ref', ref);
14101
14102 const fs = new FileSystem(_fs);
14103
14104 if (ref === undefined) {
14105 throw new MissingParameterError('ref')
14106 }
14107
14108 ref = ref.startsWith('refs/tags/') ? ref : `refs/tags/${ref}`;
14109
14110 // Resolve passed object
14111 const value = await GitRefManager.resolve({
14112 fs,
14113 gitdir,
14114 ref: object || 'HEAD',
14115 });
14116
14117 if (!force && (await GitRefManager.exists({ fs, gitdir, ref }))) {
14118 throw new AlreadyExistsError('tag', ref)
14119 }
14120
14121 await GitRefManager.writeRef({ fs, gitdir, ref, value });
14122 } catch (err) {
14123 err.caller = 'git.tag';
14124 throw err
14125 }
14126}
14127
14128// @ts-check
14129
14130/**
14131 * Register file contents in the working tree or object database to the git index (aka staging area).
14132 *
14133 * @param {object} args
14134 * @param {FsClient} args.fs - a file system client
14135 * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
14136 * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14137 * @param {string} args.filepath - File to act upon.
14138 * @param {string} [args.oid] - OID of the object in the object database to add to the index with the specified filepath.
14139 * @param {number} [args.mode = 100644] - The file mode to add the file to the index.
14140 * @param {boolean} [args.add] - Adds the specified file to the index if it does not yet exist in the index.
14141 * @param {boolean} [args.remove] - Remove the specified file from the index if it does not exist in the workspace anymore.
14142 * @param {boolean} [args.force] - Remove the specified file from the index, even if it still exists in the workspace.
14143 * @param {object} [args.cache] - a [cache](cache.md) object
14144 *
14145 * @returns {Promise<string | void>} Resolves successfully with the SHA-1 object id of the object written or updated in the index, or nothing if the file was removed.
14146 *
14147 * @example
14148 * await git.updateIndex({
14149 * fs,
14150 * dir: '/tutorial',
14151 * filepath: 'readme.md'
14152 * })
14153 *
14154 * @example
14155 * // Manually create a blob in the object database.
14156 * let oid = await git.writeBlob({
14157 * fs,
14158 * dir: '/tutorial',
14159 * blob: new Uint8Array([])
14160 * })
14161 *
14162 * // Write the object in the object database to the index.
14163 * await git.updateIndex({
14164 * fs,
14165 * dir: '/tutorial',
14166 * add: true,
14167 * filepath: 'readme.md',
14168 * oid
14169 * })
14170 */
14171async function updateIndex({
14172 fs: _fs,
14173 dir,
14174 gitdir = join(dir, '.git'),
14175 cache = {},
14176 filepath,
14177 oid,
14178 mode,
14179 add,
14180 remove,
14181 force,
14182}) {
14183 try {
14184 assertParameter('fs', _fs);
14185 assertParameter('gitdir', gitdir);
14186 assertParameter('filepath', filepath);
14187
14188 const fs = new FileSystem(_fs);
14189
14190 if (remove) {
14191 return await GitIndexManager.acquire(
14192 { fs, gitdir, cache },
14193 async function(index) {
14194 let fileStats;
14195
14196 if (!force) {
14197 // Check if the file is still present in the working directory
14198 fileStats = await fs.lstat(join(dir, filepath));
14199
14200 if (fileStats) {
14201 if (fileStats.isDirectory()) {
14202 // Removing directories should not work
14203 throw new InvalidFilepathError('directory')
14204 }
14205
14206 // Do nothing if we don't force and the file still exists in the workdir
14207 return
14208 }
14209 }
14210
14211 // Directories are not allowed, so we make sure the provided filepath exists in the index
14212 if (index.has({ filepath })) {
14213 index.delete({
14214 filepath,
14215 });
14216 }
14217 }
14218 )
14219 }
14220
14221 // Test if it is a file and exists on disk if `remove` is not provided, only of no oid is provided
14222 let fileStats;
14223
14224 if (!oid) {
14225 fileStats = await fs.lstat(join(dir, filepath));
14226
14227 if (!fileStats) {
14228 throw new NotFoundError(
14229 `file at "${filepath}" on disk and "remove" not set`
14230 )
14231 }
14232
14233 if (fileStats.isDirectory()) {
14234 throw new InvalidFilepathError('directory')
14235 }
14236 }
14237
14238 return await GitIndexManager.acquire({ fs, gitdir, cache }, async function(
14239 index
14240 ) {
14241 if (!add && !index.has({ filepath })) {
14242 // If the index does not contain the filepath yet and `add` is not set, we should throw
14243 throw new NotFoundError(
14244 `file at "${filepath}" in index and "add" not set`
14245 )
14246 }
14247
14248 // By default we use 0 for the stats of the index file
14249 let stats = {
14250 ctime: new Date(0),
14251 mtime: new Date(0),
14252 dev: 0,
14253 ino: 0,
14254 mode,
14255 uid: 0,
14256 gid: 0,
14257 size: 0,
14258 };
14259
14260 if (!oid) {
14261 stats = fileStats;
14262
14263 // Write the file to the object database
14264 const object = stats.isSymbolicLink()
14265 ? await fs.readlink(join(dir, filepath))
14266 : await fs.read(join(dir, filepath));
14267
14268 oid = await _writeObject({
14269 fs,
14270 gitdir,
14271 type: 'blob',
14272 format: 'content',
14273 object,
14274 });
14275 }
14276
14277 index.insert({
14278 filepath,
14279 oid: oid,
14280 stats,
14281 });
14282
14283 return oid
14284 })
14285 } catch (err) {
14286 err.caller = 'git.updateIndex';
14287 throw err
14288 }
14289}
14290
14291// @ts-check
14292
14293/**
14294 * Return the version number of isomorphic-git
14295 *
14296 * I don't know why you might need this. I added it just so I could check that I was getting
14297 * the correct version of the library and not a cached version.
14298 *
14299 * @returns {string} the version string taken from package.json at publication time
14300 *
14301 * @example
14302 * console.log(git.version())
14303 *
14304 */
14305function version() {
14306 try {
14307 return pkg.version
14308 } catch (err) {
14309 err.caller = 'git.version';
14310 throw err
14311 }
14312}
14313
14314// @ts-check
14315
14316/**
14317 * @callback WalkerMap
14318 * @param {string} filename
14319 * @param {Array<WalkerEntry | null>} entries
14320 * @returns {Promise<any>}
14321 */
14322
14323/**
14324 * @callback WalkerReduce
14325 * @param {any} parent
14326 * @param {any[]} children
14327 * @returns {Promise<any>}
14328 */
14329
14330/**
14331 * @callback WalkerIterateCallback
14332 * @param {WalkerEntry[]} entries
14333 * @returns {Promise<any[]>}
14334 */
14335
14336/**
14337 * @callback WalkerIterate
14338 * @param {WalkerIterateCallback} walk
14339 * @param {IterableIterator<WalkerEntry[]>} children
14340 * @returns {Promise<any[]>}
14341 */
14342
14343/**
14344 * A powerful recursive tree-walking utility.
14345 *
14346 * The `walk` API simplifies gathering detailed information about a tree or comparing all the filepaths in two or more trees.
14347 * Trees can be git commits, the working directory, or the or git index (staging area).
14348 * As long as a file or directory is present in at least one of the trees, it will be traversed.
14349 * Entries are traversed in alphabetical order.
14350 *
14351 * The arguments to `walk` are the `trees` you want to traverse, and 3 optional transform functions:
14352 * `map`, `reduce`, and `iterate`.
14353 *
14354 * ## `TREE`, `WORKDIR`, and `STAGE`
14355 *
14356 * Tree walkers are represented by three separate functions that can be imported:
14357 *
14358 * ```js
14359 * import { TREE, WORKDIR, STAGE } from 'isomorphic-git'
14360 * ```
14361 *
14362 * These functions return opaque handles called `Walker`s.
14363 * The only thing that `Walker` objects are good for is passing into `walk`.
14364 * Here are the three `Walker`s passed into `walk` by the `statusMatrix` command for example:
14365 *
14366 * ```js
14367 * let ref = 'HEAD'
14368 *
14369 * let trees = [TREE({ ref }), WORKDIR(), STAGE()]
14370 * ```
14371 *
14372 * For the arguments, see the doc pages for [TREE](./TREE.md), [WORKDIR](./WORKDIR.md), and [STAGE](./STAGE.md).
14373 *
14374 * `map`, `reduce`, and `iterate` allow you control the recursive walk by pruning and transforming `WalkerEntry`s into the desired result.
14375 *
14376 * ## WalkerEntry
14377 *
14378 * {@link WalkerEntry typedef}
14379 *
14380 * `map` receives an array of `WalkerEntry[]` as its main argument, one `WalkerEntry` for each `Walker` in the `trees` argument.
14381 * The methods are memoized per `WalkerEntry` so calling them multiple times in a `map` function does not adversely impact performance.
14382 * By only computing these values if needed, you build can build lean, mean, efficient walking machines.
14383 *
14384 * ### WalkerEntry#type()
14385 *
14386 * Returns the kind as a string. This is normally either `tree` or `blob`.
14387 *
14388 * `TREE`, `STAGE`, and `WORKDIR` walkers all return a string.
14389 *
14390 * Possible values:
14391 *
14392 * - `'tree'` directory
14393 * - `'blob'` file
14394 * - `'special'` used by `WORKDIR` to represent irregular files like sockets and FIFOs
14395 * - `'commit'` used by `TREE` to represent submodules
14396 *
14397 * ```js
14398 * await entry.type()
14399 * ```
14400 *
14401 * ### WalkerEntry#mode()
14402 *
14403 * Returns the file mode as a number. Use this to distinguish between regular files, symlinks, and executable files.
14404 *
14405 * `TREE`, `STAGE`, and `WORKDIR` walkers all return a number for all `type`s of entries.
14406 *
14407 * It has been normalized to one of the 4 values that are allowed in git commits:
14408 *
14409 * - `0o40000` directory
14410 * - `0o100644` file
14411 * - `0o100755` file (executable)
14412 * - `0o120000` symlink
14413 *
14414 * Tip: to make modes more readable, you can print them to octal using `.toString(8)`.
14415 *
14416 * ```js
14417 * await entry.mode()
14418 * ```
14419 *
14420 * ### WalkerEntry#oid()
14421 *
14422 * Returns the SHA-1 object id for blobs and trees.
14423 *
14424 * `TREE` walkers return a string for `blob` and `tree` entries.
14425 *
14426 * `STAGE` and `WORKDIR` walkers return a string for `blob` entries and `undefined` for `tree` entries.
14427 *
14428 * ```js
14429 * await entry.oid()
14430 * ```
14431 *
14432 * ### WalkerEntry#content()
14433 *
14434 * Returns the file contents as a Buffer.
14435 *
14436 * `TREE` and `WORKDIR` walkers return a Buffer for `blob` entries and `undefined` for `tree` entries.
14437 *
14438 * `STAGE` walkers always return `undefined` since the file contents are never stored in the stage.
14439 *
14440 * ```js
14441 * await entry.content()
14442 * ```
14443 *
14444 * ### WalkerEntry#stat()
14445 *
14446 * Returns a normalized subset of filesystem Stat data.
14447 *
14448 * `WORKDIR` walkers return a `Stat` for `blob` and `tree` entries.
14449 *
14450 * `STAGE` walkers return a `Stat` for `blob` entries and `undefined` for `tree` entries.
14451 *
14452 * `TREE` walkers return `undefined` for all entry types.
14453 *
14454 * ```js
14455 * await entry.stat()
14456 * ```
14457 *
14458 * {@link Stat typedef}
14459 *
14460 * ## map(string, Array<WalkerEntry|null>) => Promise<any>
14461 *
14462 * {@link WalkerMap typedef}
14463 *
14464 * This is the function that is called once per entry BEFORE visiting the children of that node.
14465 *
14466 * If you return `null` for a `tree` entry, then none of the children of that `tree` entry will be walked.
14467 *
14468 * This is a good place for query logic, such as examining the contents of a file.
14469 * Ultimately, compare all the entries and return any values you are interested in.
14470 * If you do not return a value (or return undefined) that entry will be filtered from the results.
14471 *
14472 * Example 1: Find all the files containing the word 'foo'.
14473 * ```js
14474 * async function map(filepath, [head, workdir]) {
14475 * let content = (await workdir.content()).toString('utf8')
14476 * if (content.contains('foo')) {
14477 * return {
14478 * filepath,
14479 * content
14480 * }
14481 * }
14482 * }
14483 * ```
14484 *
14485 * Example 2: Return the difference between the working directory and the HEAD commit
14486 * ```js
14487 * const map = async (filepath, [head, workdir]) => {
14488 * return {
14489 * filepath,
14490 * oid: await head?.oid(),
14491 * diff: diff(
14492 * (await head?.content())?.toString('utf8') || '',
14493 * (await workdir?.content())?.toString('utf8') || ''
14494 * )
14495 * }
14496 * }
14497 * ```
14498 *
14499 * Example 3:
14500 * ```js
14501 * let path = require('path')
14502 * // Only examine files in the directory `cwd`
14503 * let cwd = 'src/app'
14504 * async function map (filepath, [head, workdir, stage]) {
14505 * if (
14506 * // don't skip the root directory
14507 * head.fullpath !== '.' &&
14508 * // return true for 'src' and 'src/app'
14509 * !cwd.startsWith(filepath) &&
14510 * // return true for 'src/app/*'
14511 * path.dirname(filepath) !== cwd
14512 * ) {
14513 * return null
14514 * } else {
14515 * return filepath
14516 * }
14517 * }
14518 * ```
14519 *
14520 * ## reduce(parent, children)
14521 *
14522 * {@link WalkerReduce typedef}
14523 *
14524 * This is the function that is called once per entry AFTER visiting the children of that node.
14525 *
14526 * Default: `async (parent, children) => parent === undefined ? children.flat() : [parent, children].flat()`
14527 *
14528 * The default implementation of this function returns all directories and children in a giant flat array.
14529 * You can define a different accumulation method though.
14530 *
14531 * Example: Return a hierarchical structure
14532 * ```js
14533 * async function reduce (parent, children) {
14534 * return Object.assign(parent, { children })
14535 * }
14536 * ```
14537 *
14538 * ## iterate(walk, children)
14539 *
14540 * {@link WalkerIterate typedef}
14541 *
14542 * {@link WalkerIterateCallback typedef}
14543 *
14544 * Default: `(walk, children) => Promise.all([...children].map(walk))`
14545 *
14546 * The default implementation recurses all children concurrently using Promise.all.
14547 * However you could use a custom function to traverse children serially or use a global queue to throttle recursion.
14548 *
14549 * @param {object} args
14550 * @param {FsClient} args.fs - a file system client
14551 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14552 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14553 * @param {Walker[]} args.trees - The trees you want to traverse
14554 * @param {WalkerMap} [args.map] - Transform `WalkerEntry`s into a result form
14555 * @param {WalkerReduce} [args.reduce] - Control how mapped entries are combined with their parent result
14556 * @param {WalkerIterate} [args.iterate] - Fine-tune how entries within a tree are iterated over
14557 * @param {object} [args.cache] - a [cache](cache.md) object
14558 *
14559 * @returns {Promise<any>} The finished tree-walking result
14560 */
14561async function walk({
14562 fs,
14563 dir,
14564 gitdir = join(dir, '.git'),
14565 trees,
14566 map,
14567 reduce,
14568 iterate,
14569 cache = {},
14570}) {
14571 try {
14572 assertParameter('fs', fs);
14573 assertParameter('gitdir', gitdir);
14574 assertParameter('trees', trees);
14575
14576 return await _walk({
14577 fs: new FileSystem(fs),
14578 cache,
14579 dir,
14580 gitdir,
14581 trees,
14582 map,
14583 reduce,
14584 iterate,
14585 })
14586 } catch (err) {
14587 err.caller = 'git.walk';
14588 throw err
14589 }
14590}
14591
14592// @ts-check
14593
14594/**
14595 * Write a blob object directly
14596 *
14597 * @param {object} args
14598 * @param {FsClient} args.fs - a file system client
14599 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14600 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14601 * @param {Uint8Array} args.blob - The blob object to write
14602 *
14603 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly written object
14604 *
14605 * @example
14606 * // Manually create a blob.
14607 * let oid = await git.writeBlob({
14608 * fs,
14609 * dir: '/tutorial',
14610 * blob: new Uint8Array([])
14611 * })
14612 *
14613 * console.log('oid', oid) // should be 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
14614 *
14615 */
14616async function writeBlob({ fs, dir, gitdir = join(dir, '.git'), blob }) {
14617 try {
14618 assertParameter('fs', fs);
14619 assertParameter('gitdir', gitdir);
14620 assertParameter('blob', blob);
14621
14622 return await _writeObject({
14623 fs: new FileSystem(fs),
14624 gitdir,
14625 type: 'blob',
14626 object: blob,
14627 format: 'content',
14628 })
14629 } catch (err) {
14630 err.caller = 'git.writeBlob';
14631 throw err
14632 }
14633}
14634
14635// @ts-check
14636
14637/**
14638 * @param {object} args
14639 * @param {import('../models/FileSystem.js').FileSystem} args.fs
14640 * @param {string} args.gitdir
14641 * @param {CommitObject} args.commit
14642 *
14643 * @returns {Promise<string>}
14644 * @see CommitObject
14645 *
14646 */
14647async function _writeCommit({ fs, gitdir, commit }) {
14648 // Convert object to buffer
14649 const object = GitCommit.from(commit).toObject();
14650 const oid = await _writeObject({
14651 fs,
14652 gitdir,
14653 type: 'commit',
14654 object,
14655 format: 'content',
14656 });
14657 return oid
14658}
14659
14660// @ts-check
14661
14662/**
14663 * Write a commit object directly
14664 *
14665 * @param {object} args
14666 * @param {FsClient} args.fs - a file system client
14667 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14668 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14669 * @param {CommitObject} args.commit - The object to write
14670 *
14671 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly written object
14672 * @see CommitObject
14673 *
14674 */
14675async function writeCommit({
14676 fs,
14677 dir,
14678 gitdir = join(dir, '.git'),
14679 commit,
14680}) {
14681 try {
14682 assertParameter('fs', fs);
14683 assertParameter('gitdir', gitdir);
14684 assertParameter('commit', commit);
14685
14686 return await _writeCommit({
14687 fs: new FileSystem(fs),
14688 gitdir,
14689 commit,
14690 })
14691 } catch (err) {
14692 err.caller = 'git.writeCommit';
14693 throw err
14694 }
14695}
14696
14697// @ts-check
14698
14699/**
14700 * Write a git object directly
14701 *
14702 * `format` can have the following values:
14703 *
14704 * | param | description |
14705 * | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14706 * | 'deflated' | Treat `object` as the raw deflate-compressed buffer for an object, meaning can be written to `.git/objects/**` as-is. |
14707 * | 'wrapped' | Treat `object` as the inflated object buffer wrapped in the git object header. This is the raw buffer used when calculating the SHA-1 object id of a git object. |
14708 * | 'content' | Treat `object` as the object buffer without the git header. |
14709 * | 'parsed' | Treat `object` as a parsed representation of the object. |
14710 *
14711 * If `format` is `'parsed'`, then `object` must match one of the schemas for `CommitObject`, `TreeObject`, `TagObject`, or a `string` (for blobs).
14712 *
14713 * {@link CommitObject typedef}
14714 *
14715 * {@link TreeObject typedef}
14716 *
14717 * {@link TagObject typedef}
14718 *
14719 * If `format` is `'content'`, `'wrapped'`, or `'deflated'`, `object` should be a `Uint8Array`.
14720 *
14721 * @deprecated
14722 * > This command is overly complicated.
14723 * >
14724 * > If you know the type of object you are writing, use [`writeBlob`](./writeBlob.md), [`writeCommit`](./writeCommit.md), [`writeTag`](./writeTag.md), or [`writeTree`](./writeTree.md).
14725 *
14726 * @param {object} args
14727 * @param {FsClient} args.fs - a file system client
14728 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14729 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14730 * @param {string | Uint8Array | CommitObject | TreeObject | TagObject} args.object - The object to write.
14731 * @param {'blob'|'tree'|'commit'|'tag'} [args.type] - The kind of object to write.
14732 * @param {'deflated' | 'wrapped' | 'content' | 'parsed'} [args.format = 'parsed'] - What format the object is in. The possible choices are listed below.
14733 * @param {string} [args.oid] - If `format` is `'deflated'` then this param is required. Otherwise it is calculated.
14734 * @param {string} [args.encoding] - If `type` is `'blob'` then `object` will be converted to a Uint8Array using `encoding`.
14735 *
14736 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly written object.
14737 *
14738 * @example
14739 * // Manually create an annotated tag.
14740 * let sha = await git.resolveRef({ fs, dir: '/tutorial', ref: 'HEAD' })
14741 * console.log('commit', sha)
14742 *
14743 * let oid = await git.writeObject({
14744 * fs,
14745 * dir: '/tutorial',
14746 * type: 'tag',
14747 * object: {
14748 * object: sha,
14749 * type: 'commit',
14750 * tag: 'my-tag',
14751 * tagger: {
14752 * name: 'your name',
14753 * email: 'email@example.com',
14754 * timestamp: Math.floor(Date.now()/1000),
14755 * timezoneOffset: new Date().getTimezoneOffset()
14756 * },
14757 * message: 'Optional message'
14758 * }
14759 * })
14760 *
14761 * console.log('tag', oid)
14762 *
14763 */
14764async function writeObject({
14765 fs: _fs,
14766 dir,
14767 gitdir = join(dir, '.git'),
14768 type,
14769 object,
14770 format = 'parsed',
14771 oid,
14772 encoding = undefined,
14773}) {
14774 try {
14775 const fs = new FileSystem(_fs);
14776 // Convert object to buffer
14777 if (format === 'parsed') {
14778 switch (type) {
14779 case 'commit':
14780 object = GitCommit.from(object).toObject();
14781 break
14782 case 'tree':
14783 object = GitTree.from(object).toObject();
14784 break
14785 case 'blob':
14786 object = Buffer.from(object, encoding);
14787 break
14788 case 'tag':
14789 object = GitAnnotatedTag.from(object).toObject();
14790 break
14791 default:
14792 throw new ObjectTypeError(oid || '', type, 'blob|commit|tag|tree')
14793 }
14794 // GitObjectManager does not know how to serialize content, so we tweak that parameter before passing it.
14795 format = 'content';
14796 }
14797 oid = await _writeObject({
14798 fs,
14799 gitdir,
14800 type,
14801 object,
14802 oid,
14803 format,
14804 });
14805 return oid
14806 } catch (err) {
14807 err.caller = 'git.writeObject';
14808 throw err
14809 }
14810}
14811
14812// @ts-check
14813
14814/**
14815 * Write a ref which refers to the specified SHA-1 object id, or a symbolic ref which refers to the specified ref.
14816 *
14817 * @param {object} args
14818 * @param {FsClient} args.fs - a file system client
14819 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14820 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14821 * @param {string} args.ref - The name of the ref to write
14822 * @param {string} args.value - When `symbolic` is false, a ref or an SHA-1 object id. When true, a ref starting with `refs/`.
14823 * @param {boolean} [args.force = false] - Instead of throwing an error if a ref named `ref` already exists, overwrite the existing ref.
14824 * @param {boolean} [args.symbolic = false] - Whether the ref is symbolic or not.
14825 *
14826 * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
14827 *
14828 * @example
14829 * await git.writeRef({
14830 * fs,
14831 * dir: '/tutorial',
14832 * ref: 'refs/heads/another-branch',
14833 * value: 'HEAD'
14834 * })
14835 * await git.writeRef({
14836 * fs,
14837 * dir: '/tutorial',
14838 * ref: 'HEAD',
14839 * value: 'refs/heads/another-branch',
14840 * force: true,
14841 * symbolic: true
14842 * })
14843 * console.log('done')
14844 *
14845 */
14846async function writeRef({
14847 fs: _fs,
14848 dir,
14849 gitdir = join(dir, '.git'),
14850 ref,
14851 value,
14852 force = false,
14853 symbolic = false,
14854}) {
14855 try {
14856 assertParameter('fs', _fs);
14857 assertParameter('gitdir', gitdir);
14858 assertParameter('ref', ref);
14859 assertParameter('value', value);
14860
14861 const fs = new FileSystem(_fs);
14862
14863 if (ref !== cleanGitRef.clean(ref)) {
14864 throw new InvalidRefNameError(ref, cleanGitRef.clean(ref))
14865 }
14866
14867 if (!force && (await GitRefManager.exists({ fs, gitdir, ref }))) {
14868 throw new AlreadyExistsError('ref', ref)
14869 }
14870
14871 if (symbolic) {
14872 await GitRefManager.writeSymbolicRef({
14873 fs,
14874 gitdir,
14875 ref,
14876 value,
14877 });
14878 } else {
14879 value = await GitRefManager.resolve({
14880 fs,
14881 gitdir,
14882 ref: value,
14883 });
14884 await GitRefManager.writeRef({
14885 fs,
14886 gitdir,
14887 ref,
14888 value,
14889 });
14890 }
14891 } catch (err) {
14892 err.caller = 'git.writeRef';
14893 throw err
14894 }
14895}
14896
14897// @ts-check
14898
14899/**
14900 * @param {object} args
14901 * @param {import('../models/FileSystem.js').FileSystem} args.fs
14902 * @param {string} args.gitdir
14903 * @param {TagObject} args.tag
14904 *
14905 * @returns {Promise<string>}
14906 */
14907async function _writeTag({ fs, gitdir, tag }) {
14908 // Convert object to buffer
14909 const object = GitAnnotatedTag.from(tag).toObject();
14910 const oid = await _writeObject({
14911 fs,
14912 gitdir,
14913 type: 'tag',
14914 object,
14915 format: 'content',
14916 });
14917 return oid
14918}
14919
14920// @ts-check
14921
14922/**
14923 * Write an annotated tag object directly
14924 *
14925 * @param {object} args
14926 * @param {FsClient} args.fs - a file system client
14927 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14928 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14929 * @param {TagObject} args.tag - The object to write
14930 *
14931 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly written object
14932 * @see TagObject
14933 *
14934 * @example
14935 * // Manually create an annotated tag.
14936 * let sha = await git.resolveRef({ fs, dir: '/tutorial', ref: 'HEAD' })
14937 * console.log('commit', sha)
14938 *
14939 * let oid = await git.writeTag({
14940 * fs,
14941 * dir: '/tutorial',
14942 * tag: {
14943 * object: sha,
14944 * type: 'commit',
14945 * tag: 'my-tag',
14946 * tagger: {
14947 * name: 'your name',
14948 * email: 'email@example.com',
14949 * timestamp: Math.floor(Date.now()/1000),
14950 * timezoneOffset: new Date().getTimezoneOffset()
14951 * },
14952 * message: 'Optional message'
14953 * }
14954 * })
14955 *
14956 * console.log('tag', oid)
14957 *
14958 */
14959async function writeTag({ fs, dir, gitdir = join(dir, '.git'), tag }) {
14960 try {
14961 assertParameter('fs', fs);
14962 assertParameter('gitdir', gitdir);
14963 assertParameter('tag', tag);
14964
14965 return await _writeTag({
14966 fs: new FileSystem(fs),
14967 gitdir,
14968 tag,
14969 })
14970 } catch (err) {
14971 err.caller = 'git.writeTag';
14972 throw err
14973 }
14974}
14975
14976// @ts-check
14977
14978/**
14979 * Write a tree object directly
14980 *
14981 * @param {object} args
14982 * @param {FsClient} args.fs - a file system client
14983 * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14984 * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14985 * @param {TreeObject} args.tree - The object to write
14986 *
14987 * @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly written object.
14988 * @see TreeObject
14989 * @see TreeEntry
14990 *
14991 */
14992async function writeTree({ fs, dir, gitdir = join(dir, '.git'), tree }) {
14993 try {
14994 assertParameter('fs', fs);
14995 assertParameter('gitdir', gitdir);
14996 assertParameter('tree', tree);
14997
14998 return await _writeTree({
14999 fs: new FileSystem(fs),
15000 gitdir,
15001 tree,
15002 })
15003 } catch (err) {
15004 err.caller = 'git.writeTree';
15005 throw err
15006 }
15007}
15008
15009// default export
15010var index = {
15011 Errors,
15012 STAGE,
15013 TREE,
15014 WORKDIR,
15015 add,
15016 abortMerge,
15017 addNote,
15018 addRemote,
15019 annotatedTag,
15020 branch,
15021 checkout,
15022 clone,
15023 commit,
15024 getConfig,
15025 getConfigAll,
15026 setConfig,
15027 currentBranch,
15028 deleteBranch,
15029 deleteRef,
15030 deleteRemote,
15031 deleteTag,
15032 expandOid,
15033 expandRef,
15034 fastForward,
15035 fetch,
15036 findMergeBase,
15037 findRoot,
15038 getRemoteInfo,
15039 getRemoteInfo2,
15040 hashBlob,
15041 indexPack,
15042 init,
15043 isDescendent,
15044 isIgnored,
15045 listBranches,
15046 listFiles,
15047 listNotes,
15048 listRemotes,
15049 listServerRefs,
15050 listTags,
15051 log,
15052 merge,
15053 packObjects,
15054 pull,
15055 push,
15056 readBlob,
15057 readCommit,
15058 readNote,
15059 readObject,
15060 readTag,
15061 readTree,
15062 remove,
15063 removeNote,
15064 renameBranch,
15065 resetIndex,
15066 updateIndex,
15067 resolveRef,
15068 status,
15069 statusMatrix,
15070 tag,
15071 version,
15072 walk,
15073 writeBlob,
15074 writeCommit,
15075 writeObject,
15076 writeRef,
15077 writeTag,
15078 writeTree,
15079};
15080
15081export default index;
15082export { Errors, STAGE, TREE, WORKDIR, abortMerge, add, addNote, addRemote, annotatedTag, branch, checkout, clone, commit, currentBranch, deleteBranch, deleteRef, deleteRemote, deleteTag, expandOid, expandRef, fastForward, fetch, findMergeBase, findRoot, getConfig, getConfigAll, getRemoteInfo, getRemoteInfo2, hashBlob, indexPack, init, isDescendent, isIgnored, listBranches, listFiles, listNotes, listRemotes, listServerRefs, listTags, log, merge, packObjects, pull, push, readBlob, readCommit, readNote, readObject, readTag, readTree, remove, removeNote, renameBranch, resetIndex, resolveRef, setConfig, status, statusMatrix, tag, updateIndex, version, walk, writeBlob, writeCommit, writeObject, writeRef, writeTag, writeTree };