isomorphic-git 1.27.3 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs CHANGED
@@ -311,6 +311,11 @@ var diff3Merge = _interopDefault(require('diff3'));
311
311
  */
312
312
 
313
313
  /**
314
+ * @typedef {'push' | 'pop' | 'apply' | 'drop' | 'list' | 'clear'} StashOp the type of stash ops
315
+ */
316
+
317
+ /**
318
+ * @typedef {'equal' | 'modify' | 'add' | 'remove' | 'unknown'} StashChangeType - when compare WORDIR to HEAD, 'remove' could mean 'untracked'
314
319
  * @typedef {Object} ClientRef
315
320
  * @property {string} ref The name of the ref
316
321
  * @property {string} oid The SHA-1 object id the ref points to
@@ -7600,8 +7605,8 @@ function filterCapabilities(server, client) {
7600
7605
 
7601
7606
  const pkg = {
7602
7607
  name: 'isomorphic-git',
7603
- version: '1.27.3',
7604
- agent: 'git/isomorphic-git@1.27.3',
7608
+ version: '1.29.0',
7609
+ agent: 'git/isomorphic-git@1.29.0',
7605
7610
  };
7606
7611
 
7607
7612
  class FIFO {
@@ -11065,6 +11070,40 @@ async function listNotes({
11065
11070
 
11066
11071
  // @ts-check
11067
11072
 
11073
+ /**
11074
+ * List refs
11075
+ *
11076
+ * @param {object} args
11077
+ * @param {FsClient} args.fs - a file system client
11078
+ * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
11079
+ * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
11080
+ * @param {string} [args.filepath] - [required] The refs path to list
11081
+ *
11082
+ * @returns {Promise<Array<string>>} Resolves successfully with an array of ref names below the supplied `filepath`
11083
+ *
11084
+ * @example
11085
+ * let refs = await git.listRefs({ fs, dir: '/tutorial', filepath: 'refs/heads' })
11086
+ * console.log(refs)
11087
+ *
11088
+ */
11089
+ async function listRefs({
11090
+ fs,
11091
+ dir,
11092
+ gitdir = pathBrowserify.join(dir, '.git'),
11093
+ filepath,
11094
+ }) {
11095
+ try {
11096
+ assertParameter('fs', fs);
11097
+ assertParameter('gitdir', gitdir);
11098
+ return GitRefManager.listRefs({ fs: new FileSystem(fs), gitdir, filepath })
11099
+ } catch (err) {
11100
+ err.caller = 'git.listRefs';
11101
+ throw err
11102
+ }
11103
+ }
11104
+
11105
+ // @ts-check
11106
+
11068
11107
  /**
11069
11108
  * @param {object} args
11070
11109
  * @param {import('../models/FileSystem.js').FileSystem} args.fs
@@ -13889,6 +13928,781 @@ async function setConfig({
13889
13928
 
13890
13929
  // @ts-check
13891
13930
 
13931
+ /**
13932
+ * @param {object} args
13933
+ * @param {import('../models/FileSystem.js').FileSystem} args.fs
13934
+ * @param {string} args.gitdir
13935
+ * @param {CommitObject} args.commit
13936
+ *
13937
+ * @returns {Promise<string>}
13938
+ * @see CommitObject
13939
+ *
13940
+ */
13941
+ async function _writeCommit({ fs, gitdir, commit }) {
13942
+ // Convert object to buffer
13943
+ const object = GitCommit.from(commit).toObject();
13944
+ const oid = await _writeObject({
13945
+ fs,
13946
+ gitdir,
13947
+ type: 'commit',
13948
+ object,
13949
+ format: 'content',
13950
+ });
13951
+ return oid
13952
+ }
13953
+
13954
+ class GitRefStash {
13955
+ // constructor removed
13956
+
13957
+ static get timezoneOffsetForRefLogEntry() {
13958
+ const offsetMinutes = new Date().getTimezoneOffset();
13959
+ const offsetHours = Math.abs(Math.floor(offsetMinutes / 60));
13960
+ const offsetMinutesFormatted = Math.abs(offsetMinutes % 60)
13961
+ .toString()
13962
+ .padStart(2, '0');
13963
+ const sign = offsetMinutes > 0 ? '-' : '+';
13964
+ return `${sign}${offsetHours
13965
+ .toString()
13966
+ .padStart(2, '0')}${offsetMinutesFormatted}`
13967
+ }
13968
+
13969
+ static createStashReflogEntry(author, stashCommit, message) {
13970
+ const nameNoSpace = author.name.replace(/\s/g, '');
13971
+ const z40 = '0000000000000000000000000000000000000000'; // hard code for now, works with `git stash list`
13972
+ const timestamp = Math.floor(Date.now() / 1000);
13973
+ const timezoneOffset = GitRefStash.timezoneOffsetForRefLogEntry;
13974
+ return `${z40} ${stashCommit} ${nameNoSpace} ${author.email} ${timestamp} ${timezoneOffset}\t${message}\n`
13975
+ }
13976
+
13977
+ static getStashReflogEntry(reflogString, parsed = false) {
13978
+ const reflogLines = reflogString.split('\n');
13979
+ const entries = reflogLines
13980
+ .filter(l => l)
13981
+ .reverse()
13982
+ .map((line, idx) =>
13983
+ parsed ? `stash@{${idx}}: ${line.split('\t')[1]}` : line
13984
+ );
13985
+ return entries
13986
+ }
13987
+ }
13988
+
13989
+ const _TreeMap = {
13990
+ stage: STAGE,
13991
+ workdir: WORKDIR,
13992
+ };
13993
+
13994
+ let lock$3;
13995
+ async function acquireLock$1(ref, callback) {
13996
+ if (lock$3 === undefined) lock$3 = new AsyncLock();
13997
+ return lock$3.acquire(ref, callback)
13998
+ }
13999
+
14000
+ // make sure filepath, blob type and blob object (from loose objects) plus oid are in sync and valid
14001
+ async function checkAndWriteBlob(fs, gitdir, dir, filepath, oid = null) {
14002
+ const currentFilepath = pathBrowserify.join(dir, filepath);
14003
+ const stats = await fs.lstat(currentFilepath);
14004
+ if (!stats) throw new NotFoundError(currentFilepath)
14005
+ if (stats.isDirectory())
14006
+ throw new InternalError(
14007
+ `${currentFilepath}: file expected, but found directory`
14008
+ )
14009
+
14010
+ // Look for it in the loose object directory.
14011
+ const objContent = oid
14012
+ ? await readObjectLoose({ fs, gitdir, oid })
14013
+ : undefined;
14014
+ let retOid = objContent ? oid : undefined;
14015
+ if (!objContent) {
14016
+ await acquireLock$1({ fs, gitdir, currentFilepath }, async () => {
14017
+ const object = stats.isSymbolicLink()
14018
+ ? await fs.readlink(currentFilepath).then(posixifyPathBuffer)
14019
+ : await fs.read(currentFilepath);
14020
+
14021
+ if (object === null) throw new NotFoundError(currentFilepath)
14022
+
14023
+ retOid = await _writeObject({ fs, gitdir, type: 'blob', object });
14024
+ });
14025
+ }
14026
+
14027
+ return retOid
14028
+ }
14029
+
14030
+ async function processTreeEntries({ fs, dir, gitdir, entries }) {
14031
+ // make sure each tree entry has valid oid
14032
+ async function processTreeEntry(entry) {
14033
+ if (entry.type === 'tree') {
14034
+ if (!entry.oid) {
14035
+ // Process children entries if the current entry is a tree
14036
+ const children = await Promise.all(entry.children.map(processTreeEntry));
14037
+ // Write the tree with the processed children
14038
+ entry.oid = await _writeTree({
14039
+ fs,
14040
+ gitdir,
14041
+ tree: children,
14042
+ });
14043
+ entry.mode = 0o40000; // directory
14044
+ }
14045
+ } else if (entry.type === 'blob') {
14046
+ entry.oid = await checkAndWriteBlob(
14047
+ fs,
14048
+ gitdir,
14049
+ dir,
14050
+ entry.path,
14051
+ entry.oid
14052
+ );
14053
+ entry.mode = 0o100644; // file
14054
+ }
14055
+
14056
+ // remove path from entry.path
14057
+ entry.path = entry.path.split('/').pop();
14058
+ return entry
14059
+ }
14060
+
14061
+ return Promise.all(entries.map(processTreeEntry))
14062
+ }
14063
+
14064
+ async function writeTreeChanges({
14065
+ fs,
14066
+ dir,
14067
+ gitdir,
14068
+ treePair, // [TREE({ ref: 'HEAD' }), 'STAGE'] would be the equivalent of `git write-tree`
14069
+ }) {
14070
+ const isStage = treePair[1] === 'stage';
14071
+ const trees = treePair.map(t => (typeof t === 'string' ? _TreeMap[t]() : t));
14072
+
14073
+ const changedEntries = [];
14074
+ // transform WalkerEntry objects into the desired format
14075
+ const map = async (filepath, [head, stage]) => {
14076
+ if (
14077
+ filepath === '.' ||
14078
+ (await GitIgnoreManager.isIgnored({ fs, dir, gitdir, filepath }))
14079
+ ) {
14080
+ return
14081
+ }
14082
+
14083
+ if (stage) {
14084
+ if (
14085
+ !head ||
14086
+ ((await head.oid()) !== (await stage.oid()) &&
14087
+ (await stage.oid()) !== undefined)
14088
+ ) {
14089
+ changedEntries.push([head, stage]);
14090
+ }
14091
+ return {
14092
+ mode: await stage.mode(),
14093
+ path: filepath,
14094
+ oid: await stage.oid(),
14095
+ type: await stage.type(),
14096
+ }
14097
+ }
14098
+ };
14099
+
14100
+ // combine mapped entries with their parent results
14101
+ const reduce = async (parent, children) => {
14102
+ children = children.filter(Boolean); // Remove undefined entries
14103
+ if (!parent) {
14104
+ return children.length > 0 ? children : undefined
14105
+ } else {
14106
+ parent.children = children;
14107
+ return parent
14108
+ }
14109
+ };
14110
+
14111
+ // if parent is skipped, skip the children
14112
+ const iterate = async (walk, children) => {
14113
+ const filtered = [];
14114
+ for (const child of children) {
14115
+ const [head, stage] = child;
14116
+ if (isStage) {
14117
+ if (stage) {
14118
+ // for deleted file in work dir, it also needs to be added on stage
14119
+ if (await fs.exists(`${dir}/${stage.toString()}`)) {
14120
+ filtered.push(child);
14121
+ } else {
14122
+ changedEntries.push([null, stage]); // record the change (deletion) while stop the iteration
14123
+ }
14124
+ }
14125
+ } else if (head) {
14126
+ // for deleted file in workdir, "stage" (workdir in our case) will be undefined
14127
+ if (!stage) {
14128
+ changedEntries.push([head, null]); // record the change (deletion) while stop the iteration
14129
+ } else {
14130
+ filtered.push(child); // workdir, tracked only
14131
+ }
14132
+ }
14133
+ }
14134
+ return filtered.length ? Promise.all(filtered.map(walk)) : []
14135
+ };
14136
+
14137
+ const entries = await _walk({
14138
+ fs,
14139
+ cache: {},
14140
+ dir,
14141
+ gitdir,
14142
+ trees,
14143
+ map,
14144
+ reduce,
14145
+ iterate,
14146
+ });
14147
+
14148
+ if (changedEntries.length === 0 || entries.length === 0) {
14149
+ return null // no changes found to stash
14150
+ }
14151
+
14152
+ const processedEntries = await processTreeEntries({
14153
+ fs,
14154
+ dir,
14155
+ gitdir,
14156
+ entries,
14157
+ });
14158
+
14159
+ const treeEntries = processedEntries.filter(Boolean).map(entry => ({
14160
+ mode: entry.mode,
14161
+ path: entry.path,
14162
+ oid: entry.oid,
14163
+ type: entry.type,
14164
+ }));
14165
+
14166
+ return _writeTree({ fs, gitdir, tree: treeEntries })
14167
+ }
14168
+
14169
+ async function applyTreeChanges({
14170
+ fs,
14171
+ dir,
14172
+ gitdir,
14173
+ stashCommit,
14174
+ parentCommit,
14175
+ wasStaged,
14176
+ }) {
14177
+ const dirRemoved = [];
14178
+ const stageUpdated = [];
14179
+
14180
+ // analyze the changes
14181
+ const ops = await _walk({
14182
+ fs,
14183
+ cache: {},
14184
+ dir,
14185
+ gitdir,
14186
+ trees: [TREE({ ref: parentCommit }), TREE({ ref: stashCommit })],
14187
+ map: async (filepath, [parent, stash]) => {
14188
+ if (
14189
+ filepath === '.' ||
14190
+ (await GitIgnoreManager.isIgnored({ fs, dir, gitdir, filepath }))
14191
+ ) {
14192
+ return
14193
+ }
14194
+ const type = stash ? await stash.type() : await parent.type();
14195
+ if (type !== 'tree' && type !== 'blob') {
14196
+ return
14197
+ }
14198
+
14199
+ // deleted tree or blob
14200
+ if (!stash && parent) {
14201
+ const method = type === 'tree' ? 'rmdir' : 'rm';
14202
+ if (type === 'tree') dirRemoved.push(filepath);
14203
+ if (type === 'blob' && wasStaged)
14204
+ stageUpdated.push({ filepath, oid: await parent.oid() }); // stats is undefined, will stage the deletion with index.insert
14205
+ return { method, filepath }
14206
+ }
14207
+
14208
+ const oid = await stash.oid();
14209
+ if (!parent || (await parent.oid()) !== oid) {
14210
+ // only apply changes if changed from the parent commit or doesn't exist in the parent commit
14211
+ if (type === 'tree') {
14212
+ return { method: 'mkdir', filepath }
14213
+ } else {
14214
+ if (wasStaged)
14215
+ stageUpdated.push({
14216
+ filepath,
14217
+ oid,
14218
+ stats: await fs.lstat(pathBrowserify.join(dir, filepath)),
14219
+ });
14220
+ return {
14221
+ method: 'write',
14222
+ filepath,
14223
+ oid,
14224
+ }
14225
+ }
14226
+ }
14227
+ },
14228
+ });
14229
+
14230
+ // apply the changes to work dir
14231
+ await acquireLock$1({ fs, gitdir, dirRemoved, ops }, async () => {
14232
+ for (const op of ops) {
14233
+ const currentFilepath = pathBrowserify.join(dir, op.filepath);
14234
+ switch (op.method) {
14235
+ case 'rmdir':
14236
+ await fs.rmdir(currentFilepath);
14237
+ break
14238
+ case 'mkdir':
14239
+ await fs.mkdir(currentFilepath);
14240
+ break
14241
+ case 'rm':
14242
+ await fs.rm(currentFilepath);
14243
+ break
14244
+ case 'write':
14245
+ // only writes if file is not in the removedDirs
14246
+ if (
14247
+ !dirRemoved.some(removedDir =>
14248
+ currentFilepath.startsWith(removedDir)
14249
+ )
14250
+ ) {
14251
+ const { object } = await _readObject({
14252
+ fs,
14253
+ cache: {},
14254
+ gitdir,
14255
+ oid: op.oid,
14256
+ });
14257
+ // just like checkout, since mode only applicable to create, not update, delete first
14258
+ if (await fs.exists(currentFilepath)) {
14259
+ await fs.rm(currentFilepath);
14260
+ }
14261
+ await fs.write(currentFilepath, object); // only handles regular files for now
14262
+ }
14263
+ break
14264
+ }
14265
+ }
14266
+ });
14267
+
14268
+ // update the stage
14269
+ await GitIndexManager.acquire({ fs, gitdir, cache: {} }, async index => {
14270
+ stageUpdated.forEach(({ filepath, stats, oid }) => {
14271
+ index.insert({ filepath, stats, oid });
14272
+ });
14273
+ });
14274
+ }
14275
+
14276
+ class GitStashManager {
14277
+ constructor({ fs, dir, gitdir = pathBrowserify.join(dir, '.git') }) {
14278
+ Object.assign(this, {
14279
+ fs,
14280
+ dir,
14281
+ gitdir,
14282
+ _author: null,
14283
+ });
14284
+ }
14285
+
14286
+ static get refStash() {
14287
+ return 'refs/stash'
14288
+ }
14289
+
14290
+ static get refLogsStash() {
14291
+ return 'logs/refs/stash'
14292
+ }
14293
+
14294
+ get refStashPath() {
14295
+ return pathBrowserify.join(this.gitdir, GitStashManager.refStash)
14296
+ }
14297
+
14298
+ get refLogsStashPath() {
14299
+ return pathBrowserify.join(this.gitdir, GitStashManager.refLogsStash)
14300
+ }
14301
+
14302
+ async getAuthor() {
14303
+ if (!this._author) {
14304
+ this._author = await normalizeAuthorObject({
14305
+ fs: this.fs,
14306
+ gitdir: this.gitdir,
14307
+ author: {},
14308
+ });
14309
+ if (!this._author) throw new MissingNameError('author')
14310
+ }
14311
+ return this._author
14312
+ }
14313
+
14314
+ async getStashSHA(refIdx, stashEntries) {
14315
+ if (!(await this.fs.exists(this.refStashPath))) {
14316
+ return null
14317
+ }
14318
+
14319
+ const entries =
14320
+ stashEntries || (await this.readStashReflogs({ parsed: false }));
14321
+ return entries[refIdx].split(' ')[1]
14322
+ }
14323
+
14324
+ async writeStashCommit({ message, tree, parent }) {
14325
+ return _writeCommit({
14326
+ fs: this.fs,
14327
+ gitdir: this.gitdir,
14328
+ commit: {
14329
+ message,
14330
+ tree,
14331
+ parent,
14332
+ author: await this.getAuthor(),
14333
+ committer: await this.getAuthor(),
14334
+ },
14335
+ })
14336
+ }
14337
+
14338
+ async readStashCommit(refIdx) {
14339
+ const stashEntries = await this.readStashReflogs({ parsed: false });
14340
+ if (refIdx !== 0) {
14341
+ // non-default case, throw exceptions if not valid
14342
+ if (refIdx < 0 || refIdx > stashEntries.length - 1) {
14343
+ throw new InvalidRefNameError(
14344
+ `stash@${refIdx}`,
14345
+ 'number that is in range of [0, num of stash pushed]'
14346
+ )
14347
+ }
14348
+ }
14349
+
14350
+ const stashSHA = await this.getStashSHA(refIdx, stashEntries);
14351
+ if (!stashSHA) {
14352
+ return {} // no stash found
14353
+ }
14354
+
14355
+ // get the stash commit object
14356
+ return _readCommit({
14357
+ fs: this.fs,
14358
+ cache: {},
14359
+ gitdir: this.gitdir,
14360
+ oid: stashSHA,
14361
+ })
14362
+ }
14363
+
14364
+ async writeStashRef(stashCommit) {
14365
+ return GitRefManager.writeRef({
14366
+ fs: this.fs,
14367
+ gitdir: this.gitdir,
14368
+ ref: GitStashManager.refStash,
14369
+ value: stashCommit,
14370
+ })
14371
+ }
14372
+
14373
+ async writeStashReflogEntry({ stashCommit, message }) {
14374
+ const author = await this.getAuthor();
14375
+ const entry = GitRefStash.createStashReflogEntry(
14376
+ author,
14377
+ stashCommit,
14378
+ message
14379
+ );
14380
+ const filepath = this.refLogsStashPath;
14381
+
14382
+ await acquireLock$1({ filepath, entry }, async () => {
14383
+ const appendTo = (await this.fs.exists(filepath))
14384
+ ? await this.fs.read(filepath, 'utf8')
14385
+ : '';
14386
+ await this.fs.write(filepath, appendTo + entry, 'utf8');
14387
+ });
14388
+ }
14389
+
14390
+ async readStashReflogs({ parsed = false }) {
14391
+ if (!(await this.fs.exists(this.refLogsStashPath))) {
14392
+ return []
14393
+ }
14394
+
14395
+ const reflogBuffer = await this.fs.read(this.refLogsStashPath);
14396
+ const reflogString = reflogBuffer.toString();
14397
+
14398
+ return GitRefStash.getStashReflogEntry(reflogString, parsed)
14399
+ }
14400
+ }
14401
+
14402
+ // @ts-check
14403
+
14404
+ async function _stashPush({ fs, dir, gitdir, message = '' }) {
14405
+ const stashMgr = new GitStashManager({ fs, dir, gitdir });
14406
+
14407
+ await stashMgr.getAuthor(); // ensure there is an author
14408
+ const branch = await _currentBranch({
14409
+ fs,
14410
+ gitdir,
14411
+ fullname: false,
14412
+ });
14413
+
14414
+ // prepare the stash commit: first parent is the current branch HEAD
14415
+ const headCommit = await GitRefManager.resolve({
14416
+ fs,
14417
+ gitdir,
14418
+ ref: 'HEAD',
14419
+ });
14420
+
14421
+ const headCommitObj = await readCommit({ fs, dir, gitdir, oid: headCommit });
14422
+ const headMsg = headCommitObj.commit.message;
14423
+
14424
+ const stashCommitParents = [headCommit];
14425
+ let stashCommitTree = null;
14426
+ let workDirCompareBase = TREE({ ref: 'HEAD' });
14427
+
14428
+ const indexTree = await writeTreeChanges({
14429
+ fs,
14430
+ dir,
14431
+ gitdir,
14432
+ treePair: [TREE({ ref: 'HEAD' }), 'stage'],
14433
+ });
14434
+ if (indexTree) {
14435
+ // this indexTree will be the tree of the stash commit
14436
+ // create a commit from the index tree, which has one parent, the current branch HEAD
14437
+ const stashCommitOne = await stashMgr.writeStashCommit({
14438
+ message: `stash-Index: WIP on ${branch} - ${new Date().toISOString()}`,
14439
+ tree: indexTree, // stashCommitTree
14440
+ parent: stashCommitParents,
14441
+ });
14442
+ stashCommitParents.push(stashCommitOne);
14443
+ stashCommitTree = indexTree;
14444
+ workDirCompareBase = STAGE();
14445
+ }
14446
+
14447
+ const workingTree = await writeTreeChanges({
14448
+ fs,
14449
+ dir,
14450
+ gitdir,
14451
+ treePair: [workDirCompareBase, 'workdir'],
14452
+ });
14453
+ if (workingTree) {
14454
+ // create a commit from the working directory tree, which has one parent, either the one we just had, or the headCommit
14455
+ const workingHeadCommit = await stashMgr.writeStashCommit({
14456
+ message: `stash-WorkDir: WIP on ${branch} - ${new Date().toISOString()}`,
14457
+ tree: workingTree,
14458
+ parent: [stashCommitParents[stashCommitParents.length - 1]],
14459
+ });
14460
+
14461
+ stashCommitParents.push(workingHeadCommit);
14462
+ stashCommitTree = workingTree;
14463
+ }
14464
+
14465
+ if (!stashCommitTree || (!indexTree && !workingTree)) {
14466
+ throw new NotFoundError('changes, nothing to stash')
14467
+ }
14468
+
14469
+ // create another commit from the tree, which has three parents: HEAD and the commit we just made:
14470
+ const stashMsg =
14471
+ (message.trim() || `WIP on ${branch}`) +
14472
+ `: ${headCommit.substring(0, 7)} ${headMsg}`;
14473
+
14474
+ const stashCommit = await stashMgr.writeStashCommit({
14475
+ message: stashMsg,
14476
+ tree: stashCommitTree,
14477
+ parent: stashCommitParents,
14478
+ });
14479
+
14480
+ // next, write this commit into .git/refs/stash:
14481
+ await stashMgr.writeStashRef(stashCommit);
14482
+
14483
+ // write the stash commit to the logs
14484
+ await stashMgr.writeStashReflogEntry({
14485
+ stashCommit,
14486
+ message: stashMsg,
14487
+ });
14488
+
14489
+ // finally, go back to a clean working directory
14490
+ await checkout({
14491
+ fs,
14492
+ dir,
14493
+ gitdir,
14494
+ ref: branch,
14495
+ track: false,
14496
+ force: true, // force checkout to discard changes
14497
+ });
14498
+
14499
+ return stashCommit
14500
+ }
14501
+
14502
+ async function _stashApply({ fs, dir, gitdir, refIdx = 0 }) {
14503
+ const stashMgr = new GitStashManager({ fs, dir, gitdir });
14504
+
14505
+ // get the stash commit object
14506
+ const stashCommit = await stashMgr.readStashCommit(refIdx);
14507
+ const { parent: stashParents = null } = stashCommit.commit
14508
+ ? stashCommit.commit
14509
+ : {};
14510
+ if (!stashParents || !Array.isArray(stashParents)) {
14511
+ return // no stash found
14512
+ }
14513
+
14514
+ // compare the stash commit tree with its parent commit
14515
+ for (let i = 0; i < stashParents.length - 1; i++) {
14516
+ const applyingCommit = await _readCommit({
14517
+ fs,
14518
+ cache: {},
14519
+ gitdir,
14520
+ oid: stashParents[i + 1],
14521
+ });
14522
+ const wasStaged = applyingCommit.commit.message.startsWith('stash-Index');
14523
+
14524
+ await applyTreeChanges({
14525
+ fs,
14526
+ dir,
14527
+ gitdir,
14528
+ stashCommit: stashParents[i + 1],
14529
+ parentCommit: stashParents[i],
14530
+ wasStaged,
14531
+ });
14532
+ }
14533
+ }
14534
+
14535
+ async function _stashDrop({ fs, dir, gitdir, refIdx = 0 }) {
14536
+ const stashMgr = new GitStashManager({ fs, dir, gitdir });
14537
+ const stashCommit = await stashMgr.readStashCommit(refIdx);
14538
+ if (!stashCommit.commit) {
14539
+ return // no stash found
14540
+ }
14541
+ // remove stash ref first
14542
+ const stashRefPath = stashMgr.refStashPath;
14543
+ await acquireLock$1(stashRefPath, async () => {
14544
+ if (await fs.exists(stashRefPath)) {
14545
+ await fs.rm(stashRefPath);
14546
+ }
14547
+ });
14548
+
14549
+ // read from stash reflog and list the stash commits
14550
+ const reflogEntries = await stashMgr.readStashReflogs({ parsed: false });
14551
+ if (!reflogEntries.length) {
14552
+ return // no stash reflog entry
14553
+ }
14554
+
14555
+ // remove the specified stash reflog entry from reflogEntries, then update the stash reflog
14556
+ reflogEntries.splice(refIdx, 1);
14557
+
14558
+ const stashReflogPath = stashMgr.refLogsStashPath;
14559
+ await acquireLock$1({ reflogEntries, stashReflogPath, stashMgr }, async () => {
14560
+ if (reflogEntries.length) {
14561
+ await fs.write(stashReflogPath, reflogEntries.join('\n'), 'utf8');
14562
+ const lastStashCommit = reflogEntries[reflogEntries.length - 1].split(
14563
+ ' '
14564
+ )[1];
14565
+ await stashMgr.writeStashRef(lastStashCommit);
14566
+ } else {
14567
+ // remove the stash reflog file if no entry left
14568
+ await fs.rm(stashReflogPath);
14569
+ }
14570
+ });
14571
+ }
14572
+
14573
+ async function _stashList({ fs, dir, gitdir }) {
14574
+ const stashMgr = new GitStashManager({ fs, dir, gitdir });
14575
+ return stashMgr.readStashReflogs({ parsed: true })
14576
+ }
14577
+
14578
+ async function _stashClear({ fs, dir, gitdir }) {
14579
+ const stashMgr = new GitStashManager({ fs, dir, gitdir });
14580
+ const stashRefPath = [stashMgr.refStashPath, stashMgr.refLogsStashPath];
14581
+
14582
+ await acquireLock$1(stashRefPath, async () => {
14583
+ await Promise.all(
14584
+ stashRefPath.map(async path => {
14585
+ if (await fs.exists(path)) {
14586
+ return fs.rm(path)
14587
+ }
14588
+ })
14589
+ );
14590
+ });
14591
+ }
14592
+
14593
+ async function _stashPop({ fs, dir, gitdir, refIdx = 0 }) {
14594
+ await _stashApply({ fs, dir, gitdir, refIdx });
14595
+ await _stashDrop({ fs, dir, gitdir, refIdx });
14596
+ }
14597
+
14598
+ // @ts-check
14599
+
14600
+ /**
14601
+ * stash api, supports {'push' | 'pop' | 'apply' | 'drop' | 'list' | 'clear'} StashOp
14602
+ * _note_,
14603
+ * - all stash operations are done on tracked files only with loose objects, no packed objects
14604
+ * - when op === 'push', both working directory and index (staged) changes will be stashed, tracked files only
14605
+ * - when op === 'push', message is optional, and only applicable when op === 'push'
14606
+ * - when op === 'apply | pop', the stashed changes will overwrite the working directory, no abort when conflicts
14607
+ *
14608
+ * @param {object} args
14609
+ * @param {FsClient} args.fs - [required] a file system client
14610
+ * @param {string} [args.dir] - [required] The [working tree](dir-vs-gitdir.md) directory path
14611
+ * @param {string} [args.gitdir=join(dir,'.git')] - [optional] The [git directory](dir-vs-gitdir.md) path
14612
+ * @param {'push' | 'pop' | 'apply' | 'drop' | 'list' | 'clear'} [args.op = 'push'] - [optional] name of stash operation, default to 'push'
14613
+ * @param {string} [args.message = ''] - [optional] message to be used for the stash entry, only applicable when op === 'push'
14614
+ * @param {number} [args.refIdx = 0] - [optional - Number] stash ref index of entry, only applicable when op === ['apply' | 'drop' | 'pop'], refIdx >= 0 and < num of stash pushed
14615
+ * @returns {Promise<string | void>} Resolves successfully when stash operations are complete
14616
+ *
14617
+ * @example
14618
+ * // stash changes in the working directory and index
14619
+ * let dir = '/tutorial'
14620
+ * await fs.promises.writeFile(`${dir}/a.txt`, 'original content - a')
14621
+ * await fs.promises.writeFile(`${dir}/b.js`, 'original content - b')
14622
+ * await git.add({ fs, dir, filepath: [`a.txt`,`b.txt`] })
14623
+ * let sha = await git.commit({
14624
+ * fs,
14625
+ * dir,
14626
+ * author: {
14627
+ * name: 'Mr. Stash',
14628
+ * email: 'mstasher@stash.com',
14629
+ * },
14630
+ * message: 'add a.txt and b.txt to test stash'
14631
+ * })
14632
+ * console.log(sha)
14633
+ *
14634
+ * await fs.promises.writeFile(`${dir}/a.txt`, 'stashed chang- a')
14635
+ * await git.add({ fs, dir, filepath: `${dir}/a.txt` })
14636
+ * await fs.promises.writeFile(`${dir}/b.js`, 'work dir change. not stashed - b')
14637
+ *
14638
+ * await git.stash({ fs, dir }) // default gitdir and op
14639
+ *
14640
+ * console.log(await git.status({ fs, dir, filepath: 'a.txt' })) // 'unmodified'
14641
+ * console.log(await git.status({ fs, dir, filepath: 'b.txt' })) // 'unmodified'
14642
+ *
14643
+ * const refLog = await git.stash({ fs, dir, op: 'list' })
14644
+ * console.log(refLog) // [{stash{#} message}]
14645
+ *
14646
+ * await git.stash({ fs, dir, op: 'apply' }) // apply the stash
14647
+ *
14648
+ * console.log(await git.status({ fs, dir, filepath: 'a.txt' })) // 'modified'
14649
+ * console.log(await git.status({ fs, dir, filepath: 'b.txt' })) // '*modified'
14650
+ */
14651
+
14652
+ async function stash({
14653
+ fs,
14654
+ dir,
14655
+ gitdir = pathBrowserify.join(dir, '.git'),
14656
+ op = 'push',
14657
+ message = '',
14658
+ refIdx = 0,
14659
+ }) {
14660
+ assertParameter('fs', fs);
14661
+ assertParameter('dir', dir);
14662
+ assertParameter('gitdir', gitdir);
14663
+ assertParameter('op', op);
14664
+
14665
+ const stashMap = {
14666
+ push: _stashPush,
14667
+ apply: _stashApply,
14668
+ drop: _stashDrop,
14669
+ list: _stashList,
14670
+ clear: _stashClear,
14671
+ pop: _stashPop,
14672
+ };
14673
+
14674
+ const opsNeedRefIdx = ['apply', 'drop', 'pop'];
14675
+
14676
+ try {
14677
+ const _fs = new FileSystem(fs);
14678
+ const folders = ['refs', 'logs', 'logs/refs'];
14679
+ folders
14680
+ .map(f => pathBrowserify.join(gitdir, f))
14681
+ .forEach(async folder => {
14682
+ if (!(await _fs.exists(folder))) {
14683
+ await _fs.mkdir(folder);
14684
+ }
14685
+ });
14686
+
14687
+ const opFunc = stashMap[op];
14688
+ if (opFunc) {
14689
+ if (opsNeedRefIdx.includes(op) && refIdx < 0) {
14690
+ throw new InvalidRefNameError(
14691
+ `stash@${refIdx}`,
14692
+ 'number that is in range of [0, num of stash pushed]'
14693
+ )
14694
+ }
14695
+ return await opFunc({ fs: _fs, dir, gitdir, message, refIdx })
14696
+ }
14697
+ throw new Error(`To be implemented: ${op}`)
14698
+ } catch (err) {
14699
+ err.caller = 'git.stash';
14700
+ throw err
14701
+ }
14702
+ }
14703
+
14704
+ // @ts-check
14705
+
13892
14706
  /**
13893
14707
  * Tell whether a file has been changed
13894
14708
  *
@@ -14890,31 +15704,6 @@ async function writeBlob({ fs, dir, gitdir = pathBrowserify.join(dir, '.git'), b
14890
15704
 
14891
15705
  // @ts-check
14892
15706
 
14893
- /**
14894
- * @param {object} args
14895
- * @param {import('../models/FileSystem.js').FileSystem} args.fs
14896
- * @param {string} args.gitdir
14897
- * @param {CommitObject} args.commit
14898
- *
14899
- * @returns {Promise<string>}
14900
- * @see CommitObject
14901
- *
14902
- */
14903
- async function _writeCommit({ fs, gitdir, commit }) {
14904
- // Convert object to buffer
14905
- const object = GitCommit.from(commit).toObject();
14906
- const oid = await _writeObject({
14907
- fs,
14908
- gitdir,
14909
- type: 'commit',
14910
- object,
14911
- format: 'content',
14912
- });
14913
- return oid
14914
- }
14915
-
14916
- // @ts-check
14917
-
14918
15707
  /**
14919
15708
  * Write a commit object directly
14920
15709
  *
@@ -15301,6 +16090,7 @@ var index = {
15301
16090
  listBranches,
15302
16091
  listFiles,
15303
16092
  listNotes,
16093
+ listRefs,
15304
16094
  listRemotes,
15305
16095
  listServerRefs,
15306
16096
  listTags,
@@ -15332,6 +16122,7 @@ var index = {
15332
16122
  writeRef,
15333
16123
  writeTag,
15334
16124
  writeTree,
16125
+ stash,
15335
16126
  };
15336
16127
 
15337
16128
  exports.Errors = Errors;
@@ -15371,6 +16162,7 @@ exports.isIgnored = isIgnored;
15371
16162
  exports.listBranches = listBranches;
15372
16163
  exports.listFiles = listFiles;
15373
16164
  exports.listNotes = listNotes;
16165
+ exports.listRefs = listRefs;
15374
16166
  exports.listRemotes = listRemotes;
15375
16167
  exports.listServerRefs = listServerRefs;
15376
16168
  exports.listTags = listTags;
@@ -15391,6 +16183,7 @@ exports.renameBranch = renameBranch;
15391
16183
  exports.resetIndex = resetIndex;
15392
16184
  exports.resolveRef = resolveRef;
15393
16185
  exports.setConfig = setConfig;
16186
+ exports.stash = stash;
15394
16187
  exports.status = status;
15395
16188
  exports.statusMatrix = statusMatrix;
15396
16189
  exports.tag = tag;