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/README.md +6 -2
- package/browser-tests.json +6 -3
- package/index.cjs +820 -27
- package/index.d.ts +95 -0
- package/index.js +819 -28
- package/index.umd.min.d.ts +95 -0
- package/index.umd.min.js +2 -2
- package/index.umd.min.js.map +1 -1
- package/package.json +1 -1
- package/size_report.html +1 -1
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.
|
|
7604
|
-
agent: 'git/isomorphic-git@1.
|
|
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;
|