@antora/content-aggregator 3.1.5 → 3.1.7

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.
@@ -5,7 +5,6 @@ const { createHash } = require('crypto')
5
5
  const createGitHttpPlugin = require('./git-plugin-http')
6
6
  const decodeUint8Array = require('./decode-uint8-array')
7
7
  const deepClone = require('./deep-clone')
8
- const deepFlatten = require('./deep-flatten')
9
8
  const EventEmitter = require('events')
10
9
  const expandPath = require('@antora/expand-path-helper')
11
10
  const File = require('./file')
@@ -17,6 +16,7 @@ const GitCredentialManagerStore = require('./git-credential-manager-store')
17
16
  const git = require('./git')
18
17
  const { NotFoundError, ObjectTypeError, UnknownTransportError, UrlParseError } = git.Errors
19
18
  const globStream = require('glob-stream')
19
+ const { inspect } = require('util')
20
20
  const invariably = require('./invariably')
21
21
  const logger = require('./logger')
22
22
  const { makeMatcherRx, versionMatcherOpts: VERSION_MATCHER_OPTS } = require('./matcher')
@@ -93,83 +93,71 @@ function aggregateContent (playbook) {
93
93
  return ensureCacheDir(requestedCacheDir, startDir).then((cacheDir) => {
94
94
  const gitConfig = Object.assign({ ensureGitSuffix: true }, playbook.git)
95
95
  const gitPlugins = loadGitPlugins(gitConfig, playbook.network || {}, startDir)
96
- const fetchConcurrency = Math.max(gitConfig.fetchConcurrency || Infinity, 1)
96
+ const concurrency = {
97
+ fetch: Math.max(gitConfig.fetchConcurrency || Infinity, 1),
98
+ read: Math.max(gitConfig.readConcurrency || Infinity, 1),
99
+ }
97
100
  const sourcesByUrl = sources.reduce((accum, source) => {
98
101
  return accum.set(source.url, [...(accum.get(source.url) || []), Object.assign({}, sourceDefaults, source)])
99
102
  }, new Map())
100
103
  const progress = !quiet && createProgress(sourcesByUrl.keys(), process.stdout)
101
104
  const refPatternCache = Object.assign(new Map(), { braces: new Map() })
102
105
  const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir, refPatternCache }
103
- return collectFiles(sourcesByUrl, loadOpts, fetchConcurrency).then(buildAggregate, (err) => {
106
+ return collectFiles(sourcesByUrl, loadOpts, concurrency).then(buildAggregate, (err) => {
104
107
  progress && progress.terminate()
105
108
  throw err
106
109
  })
107
110
  })
108
111
  }
109
112
 
110
- async function collectFiles (sourcesByUrl, loadOpts, concurrency) {
111
- const tasks = [...sourcesByUrl.entries()].map(([url, sources]) => [
112
- () => loadRepository(url, Object.assign({ fetchTags: tagsSpecified(sources) }, loadOpts)),
113
- ({ repo, authStatus }) =>
114
- Promise.all(
115
- sources.map((source) => {
116
- // NOTE if repository is managed (has a url property), we can assume the remote name is origin
117
- // TODO if the repo has no remotes, then remoteName should be undefined
118
- const remoteName = repo.url ? 'origin' : source.remote || 'origin'
119
- return collectFilesFromSource(source, repo, remoteName, authStatus)
120
- })
121
- ),
122
- ])
123
- let rejection, started
124
- const startedContinuations = []
125
- const recordRejection = (err) => {
126
- rejection = err
127
- }
128
- const runTask = (primary, continuation, idx) =>
129
- primary().then((value) => {
130
- if (!rejection) startedContinuations[idx] = continuation(value).catch(recordRejection)
131
- }, recordRejection)
132
- if (tasks.length > concurrency) {
133
- started = []
134
- const pending = []
135
- for (const [primary, continuation] of tasks) {
136
- const current = runTask(primary, continuation, started.length).finally(() =>
137
- pending.splice(pending.indexOf(current), 1)
138
- )
139
- started.push(current)
140
- if (pending.push(current) < concurrency) continue
141
- await Promise.race(pending)
142
- if (rejection) break
113
+ async function collectFiles (sourcesByUrl, loadOpts, concurrency, fetchedUrls) {
114
+ const loadTasks = [...sourcesByUrl.entries()].map(([url, sources]) => {
115
+ const loadOptsForUrl = Object.assign({}, loadOpts)
116
+ if (loadOpts.fetch && fetchedUrls && fetchedUrls.length && fetchedUrls.includes(url)) loadOptsForUrl.fetch = false
117
+ if (tagsSpecified(sources)) loadOptsForUrl.fetchTags = true
118
+ return loadRepository.bind(null, url, loadOptsForUrl, { url, sources })
119
+ })
120
+ return gracefulPromiseAllWithLimit(loadTasks, concurrency.fetch).then(([results, rejections]) => {
121
+ if (rejections.length) {
122
+ if (concurrency.fetch > 1 && results.length > 1 && rejections.every(({ recoverable }) => recoverable)) {
123
+ if (loadOpts.progress) loadOpts.progress.terminate() // reset cursor position and allow it be reused
124
+ const msg0 = 'An unexpected error occurred while fetching content sources concurrently.'
125
+ const msg1 = 'Retrying with git.fetch_concurrency value of 1.'
126
+ logger.warn(rejections[0], msg0 + ' ' + msg1)
127
+ const fulfilledUrls = results.map((it) => it && it.repo.url && it.url).filter((it) => it)
128
+ return collectFiles(sourcesByUrl, loadOpts, Object.assign(concurrency, { fetch: 1 }), fulfilledUrls)
129
+ }
130
+ throw rejections[0]
143
131
  }
144
- } else {
145
- started = tasks.map(([primary, continuation], idx) => runTask(primary, continuation, idx))
146
- }
147
- return Promise.all(started).then(() =>
148
- Promise.all(startedContinuations).then((result) => {
149
- if (rejection) throw rejection
150
- return result
151
- })
152
- )
132
+ return Promise.all(
133
+ results.map(({ repo, authStatus, sources }) =>
134
+ selectStartPathsForRepository(repo, authStatus, sources).then((startPaths) =>
135
+ collectFilesFromStartPaths.bind(null, startPaths, repo, authStatus)
136
+ )
137
+ )
138
+ ).then((collectTasks) => promiseAllWithLimit(collectTasks, concurrency.read))
139
+ })
153
140
  }
154
141
 
155
142
  function buildAggregate (componentVersionBuckets) {
156
- return [
157
- ...deepFlatten(componentVersionBuckets)
158
- .reduce((accum, batch) => {
159
- const key = batch.version + '@' + batch.name
160
- const entry = accum.get(key)
161
- if (!entry) return accum.set(key, batch)
143
+ const entries = Object.assign(new Map(), { accum: [] })
144
+ for (const batchesForOrigin of componentVersionBuckets) {
145
+ for (const batch of batchesForOrigin) {
146
+ let key, entry
147
+ if ((entry = entries.get((key = batch.version + '@' + batch.name)))) {
162
148
  const { files, origins } = batch
163
149
  ;(batch.files = entry.files).push(...files)
164
150
  ;(batch.origins = entry.origins).push(origins[0])
165
151
  Object.assign(entry, batch)
166
- return accum
167
- }, new Map())
168
- .values(),
169
- ]
152
+ } else {
153
+ entries.set(key, batch).accum.push(batch)
154
+ }
155
+ }
156
+ }
157
+ return entries.accum
170
158
  }
171
159
 
172
- async function loadRepository (url, opts) {
160
+ async function loadRepository (url, opts, result = {}) {
173
161
  let authStatus, dir, repo
174
162
  const cache = { [REF_PATTERN_CACHE_KEY]: opts.refPatternCache }
175
163
  if (~url.indexOf(':') && GIT_URI_DETECTOR_RX.test(url)) {
@@ -179,6 +167,7 @@ async function loadRepository (url, opts) {
179
167
  dir = ospath.join(cacheDir, generateCloneFolderName(displayUrl))
180
168
  // NOTE the presence of the url property on the repo object implies the repository is remote
181
169
  repo = { cache, dir, fs, gitdir: dir, noCheckout: true, url }
170
+ const { credentialManager } = gitPlugins
182
171
  const validStateFile = ospath.join(dir, VALID_STATE_FILENAME)
183
172
  try {
184
173
  await fsp.access(validStateFile)
@@ -188,8 +177,7 @@ async function loadRepository (url, opts) {
188
177
  await git
189
178
  .fetch(fetchOpts)
190
179
  .then(() => {
191
- const credentialManager = gitPlugins.credentialManager
192
- authStatus = credentials ? 'auth-embedded' : credentialManager.status({ url }) ? 'auth-required' : undefined
180
+ authStatus = identifyAuthStatus(credentialManager, credentials, url)
193
181
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
194
182
  })
195
183
  .catch((fetchErr) => {
@@ -210,14 +198,13 @@ async function loadRepository (url, opts) {
210
198
  .clone(fetchOpts)
211
199
  .then(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo)))
212
200
  .then(() => {
213
- const credentialManager = gitPlugins.credentialManager
214
- authStatus = credentials ? 'auth-embedded' : credentialManager.status({ url }) ? 'auth-required' : undefined
201
+ authStatus = identifyAuthStatus(credentialManager, credentials, url)
215
202
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
216
203
  })
217
204
  .catch((cloneErr) => {
218
- // FIXME triggering the error handler here causes assertion problems in the test suite
219
- //if (fetchOpts.onProgress) fetchOpts.onProgress.finish(cloneErr)
220
- throw transformGitCloneError(cloneErr, displayUrl)
205
+ if (fetchOpts.onProgress) fetchOpts.onProgress.finish(cloneErr)
206
+ const authRequested = credentialManager.status({ url }) === 'requested'
207
+ throw transformGitCloneError(cloneErr, displayUrl, authRequested)
221
208
  })
222
209
  .then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
223
210
  .then(() => fetchOpts.onProgress && fetchOpts.onProgress.finish())
@@ -234,7 +221,7 @@ async function loadRepository (url, opts) {
234
221
  } else {
235
222
  throw new Error(`Local content source does not exist: ${dir}${url !== dir ? ' (url: ' + url + ')' : ''}`)
236
223
  }
237
- return { repo, authStatus }
224
+ return Object.assign(result, { repo, authStatus })
238
225
  }
239
226
 
240
227
  function extractCredentials (url) {
@@ -256,19 +243,33 @@ function extractCredentials (url) {
256
243
  }
257
244
  }
258
245
 
259
- async function collectFilesFromSource (source, repo, remoteName, authStatus) {
260
- const originUrl = repo.url || (await resolveRemoteUrl(repo, remoteName))
261
- return selectReferences(source, repo, remoteName).then((refs) => {
262
- if (!refs.length) {
263
- const { url, branches, tags, startPath, startPaths } = source
246
+ async function selectStartPathsForRepository (repo, authStatus, sources) {
247
+ const startPaths = []
248
+ const originUrls = {}
249
+ for (const source of sources) {
250
+ const { version, editUrl } = source
251
+ // NOTE if repository is managed (has a url property), we can assume the remote name is origin
252
+ // TODO if the repo has no remotes, then remoteName should be undefined
253
+ const remoteName = repo.url ? 'origin' : source.remote || 'origin'
254
+ const originUrl = repo.url || (originUrls[remoteName] ||= await resolveRemoteUrl(repo, remoteName))
255
+ const refs = await selectReferences(source, repo, remoteName)
256
+ if (refs.length) {
257
+ for (const ref of refs) {
258
+ for (const startPath of await selectStartPaths(source, repo, remoteName, ref)) {
259
+ startPaths.push({ startPath, ref, originUrl, editUrl, version })
260
+ }
261
+ }
262
+ } else {
263
+ const { url, branches, tags } = source
264
264
  const startPathInfo =
265
- 'startPaths' in source ? { 'start paths': startPaths || undefined } : { 'start path': startPath || undefined }
265
+ 'startPaths' in source
266
+ ? { 'start paths': source.startPaths || undefined }
267
+ : { 'start path': source.startPath || undefined }
266
268
  const sourceInfo = yaml.dump({ url, branches, tags, ...startPathInfo }, { flowLevel: 1 }).trimRight()
267
269
  logger.info(`No matching references found for content source entry (${sourceInfo.replace(NEWLINE_RX, ' | ')})`)
268
- return []
269
270
  }
270
- return Promise.all(refs.map((it) => collectFilesFromReference(source, repo, remoteName, authStatus, it, originUrl)))
271
- })
271
+ }
272
+ return startPaths
272
273
  }
273
274
 
274
275
  // QUESTION should we resolve HEAD to a ref eagerly to avoid having to do a match on it?
@@ -397,10 +398,9 @@ function getCurrentBranchName (repo, remote) {
397
398
  return refPromise.then((ref) => (ref.startsWith('refs/') ? ref.replace(SHORTEN_REF_RX, '') : undefined))
398
399
  }
399
400
 
400
- async function collectFilesFromReference (source, repo, remoteName, authStatus, ref, originUrl) {
401
+ async function selectStartPaths (source, repo, remoteName, ref) {
401
402
  const url = repo.url
402
403
  const displayUrl = url || repo.dir
403
- const { version, editUrl } = source
404
404
  const worktreePath = ref.head
405
405
  if (!worktreePath) {
406
406
  ref.oid = await git.resolveRef(
@@ -420,17 +420,22 @@ async function collectFilesFromReference (source, repo, remoteName, authStatus,
420
420
  const flag = worktreePath ? ' <worktree>' : ref.remote && worktreePath === false ? ` <remotes/${ref.remote}>` : ''
421
421
  throw new Error(`no start paths found in ${where} (${ref.type}: ${ref.shortname}${flag})`)
422
422
  }
423
- return Promise.all(
424
- startPaths.map((startPath) =>
425
- collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
426
- )
427
- )
423
+ return startPaths
424
+ }
425
+ return [cleanStartPath(coerceToString(source.startPath))]
426
+ }
427
+
428
+ async function collectFilesFromStartPaths (startPaths, repo, authStatus) {
429
+ const buckets = []
430
+ for (const { startPath, ref, originUrl, editUrl, version } of startPaths) {
431
+ buckets.push(await collectFilesFromStartPath(startPath, repo, authStatus, ref, originUrl, editUrl, version))
428
432
  }
429
- const startPath = cleanStartPath(coerceToString(source.startPath))
430
- return collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
433
+ repo.cache = undefined
434
+ return buckets
431
435
  }
432
436
 
433
- function collectFilesFromStartPath (startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version) {
437
+ function collectFilesFromStartPath (startPath, repo, authStatus, ref, originUrl, editUrl, version) {
438
+ const worktreePath = ref.head
434
439
  const origin = computeOrigin(originUrl, authStatus, repo.gitdir, ref, startPath, worktreePath, editUrl)
435
440
  return (worktreePath ? readFilesFromWorktree(origin) : readFilesFromGitTree(repo, ref.oid, startPath))
436
441
  .then((files) =>
@@ -510,26 +515,25 @@ function srcFs (cwd, origin) {
510
515
  }
511
516
 
512
517
  function readFilesFromGitTree (repo, oid, startPath) {
513
- return git
514
- .readTree(Object.assign({ oid }, repo))
515
- .then((root) =>
516
- getGitTreeAtStartPath(repo, oid, startPath).then((start) =>
517
- srcGitTree(repo, Object.assign(root, { dirname: '' }), start)
518
- )
519
- )
518
+ return git.readTree(Object.assign({ oid }, repo)).then((root) => {
519
+ Object.assign(root, { dirname: '' })
520
+ return startPath
521
+ ? getGitTreeAtStartPath(repo, oid, startPath).then((start) => {
522
+ Object.assign(start, { dirname: startPath })
523
+ return srcGitTree(repo, root, start)
524
+ })
525
+ : srcGitTree(repo, root)
526
+ })
520
527
  }
521
528
 
522
529
  function getGitTreeAtStartPath (repo, oid, startPath) {
523
- return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).then(
524
- (result) => Object.assign(result, { dirname: startPath }),
525
- (err) => {
526
- const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
527
- throw new Error(`the start path '${startPath}' ${m}`)
528
- }
529
- )
530
+ return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).catch((err) => {
531
+ const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
532
+ throw new Error(`the start path '${startPath}' ${m}`)
533
+ })
530
534
  }
531
535
 
532
- function srcGitTree (repo, root, start) {
536
+ function srcGitTree (repo, root, start = root) {
533
537
  return new Promise((resolve, reject) => {
534
538
  const files = []
535
539
  createGitTreeWalker(repo, root, filterGitEntry, gitEntryToFile)
@@ -816,7 +820,6 @@ function onGitProgress ({ phase, loaded, total }) {
816
820
 
817
821
  function onGitComplete (err) {
818
822
  if (err) {
819
- // TODO could use progressBar.interrupt() to replace bar with message instead
820
823
  this.chars.incomplete = '?'
821
824
  this.update(0)
822
825
  // NOTE force progress bar to update regardless of throttle setting
@@ -846,6 +849,10 @@ function resolveCredentials (credentialsFromUrlHolder, url, auth) {
846
849
  )
847
850
  }
848
851
 
852
+ function identifyAuthStatus (credentialManager, credentials, url) {
853
+ return credentials ? 'auth-embedded' : credentialManager.status({ url }) ? 'auth-required' : undefined
854
+ }
855
+
849
856
  /**
850
857
  * Generates a safe, unique folder name for a git URL.
851
858
  *
@@ -957,8 +964,8 @@ function ensureCacheDir (preferredCacheDir, startDir) {
957
964
  )
958
965
  }
959
966
 
960
- function transformGitCloneError (err, displayUrl) {
961
- let wrappedMsg, trimMessage
967
+ function transformGitCloneError (err, displayUrl, authRequested) {
968
+ let wrappedMsg, recoverable, trimMessage
962
969
  if (HTTP_ERROR_CODE_RX.test(err.code)) {
963
970
  switch (err.data.statusCode) {
964
971
  case 401:
@@ -967,25 +974,26 @@ function transformGitCloneError (err, displayUrl) {
967
974
  : 'Content repository not found or requires credentials'
968
975
  break
969
976
  case 404:
970
- wrappedMsg = 'Content repository not found'
977
+ wrappedMsg = authRequested
978
+ ? 'Content repository not found or credentials were rejected'
979
+ : 'Content repository not found'
971
980
  break
972
981
  default:
973
982
  wrappedMsg = err.message
974
- trimMessage = true
983
+ recoverable = trimMessage = true
975
984
  }
976
985
  } else if (err instanceof UrlParseError || err instanceof UnknownTransportError) {
977
986
  wrappedMsg = 'Content source uses an unsupported transport protocol'
978
987
  } else if (err.code === 'ENOTFOUND') {
979
988
  wrappedMsg = `Content repository host could not be resolved: ${err.hostname}`
980
989
  } else {
981
- wrappedMsg = `${err.name}: ${err.message}`
982
- trimMessage = true
983
- }
984
- if (trimMessage) {
985
- wrappedMsg = ~(wrappedMsg = wrappedMsg.trimRight()).indexOf('. ') ? wrappedMsg : wrappedMsg.replace(/\.$/, '')
990
+ wrappedMsg = err.message || String(err)
991
+ recoverable = trimMessage = true
986
992
  }
993
+ if (trimMessage && !~(wrappedMsg = wrappedMsg.trimEnd()).indexOf('. ')) wrappedMsg = wrappedMsg.replace(/\.$/, '')
987
994
  const errWrapper = new Error(`${wrappedMsg} (url: ${displayUrl})`)
988
- errWrapper.stack += `\nCaused by: ${err.stack || 'unknown'}`
995
+ errWrapper.stack += `\nCaused by: ${err.stack ? inspect(err).replace(/^Error \[(.+?)\](?=: )/, '$1') : err}`
996
+ if (recoverable) Object.defineProperty(errWrapper, 'recoverable', { value: true })
989
997
  return errWrapper
990
998
  }
991
999
 
@@ -1034,7 +1042,7 @@ function findWorktrees (repo, patterns) {
1034
1042
  .then((branch = worktreeName) =>
1035
1043
  fsp
1036
1044
  .readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
1037
- .then((contents) => ({ branch, dir: ospath.dirname(contents.trimRight()) }))
1045
+ .then((contents) => ({ branch, dir: ospath.dirname(contents.trimEnd()) }))
1038
1046
  )
1039
1047
  })
1040
1048
  ).then((entries) => entries.reduce((accum, it) => accum.set(it.branch, it.dir), new Map()))
@@ -1048,4 +1056,37 @@ function findWorktrees (repo, patterns) {
1048
1056
  )
1049
1057
  }
1050
1058
 
1059
+ async function gracefulPromiseAllWithLimit (tasks, limit = Infinity) {
1060
+ const rejections = []
1061
+ const recordRejection = (err) => rejections.push(err) && undefined
1062
+ const started = []
1063
+ if (tasks.length <= limit) {
1064
+ for (const task of tasks) started.push(task().catch(recordRejection))
1065
+ } else {
1066
+ const pending = []
1067
+ for (const task of tasks) {
1068
+ const current = task()
1069
+ .catch(recordRejection)
1070
+ .finally(() => pending.splice(pending.indexOf(current), 1))
1071
+ started.push(current)
1072
+ if (pending.push(current) < limit) continue
1073
+ await Promise.race(pending)
1074
+ if (rejections.length) break
1075
+ }
1076
+ }
1077
+ return Promise.all(started).then((results) => [results, rejections])
1078
+ }
1079
+
1080
+ async function promiseAllWithLimit (tasks, limit = Infinity) {
1081
+ if (tasks.length <= limit) return Promise.all(tasks.map((task) => task()))
1082
+ const started = []
1083
+ const pending = []
1084
+ for (const task of tasks) {
1085
+ const current = task().finally(() => pending.splice(pending.indexOf(current), 1))
1086
+ started.push(current)
1087
+ if (pending.push(current) >= limit) await Promise.race(pending)
1088
+ }
1089
+ return Promise.all(started)
1090
+ }
1091
+
1051
1092
  module.exports = aggregateContent
@@ -49,11 +49,12 @@ module.exports = ({ headers: extraHeaders, httpProxy, httpsProxy, noProxy } = {}
49
49
  }
50
50
  return {
51
51
  async request ({ url, method, headers, body }) {
52
- headers = mergeHeaders(headers, extraHeaders)
52
+ headers = Object.assign(mergeHeaders(headers, extraHeaders), { connection: 'close' })
53
53
  body = await mergeBuffers(body)
54
- return new Promise((resolve, reject) =>
55
- get({ url, method, headers, body }, (err, res) => (err ? reject(err) : resolve(distillResponse(res))))
56
- )
54
+ return new Promise((resolve, reject) => {
55
+ const opts = { url, method, headers, body, timeout: 0, keepAlive: false }
56
+ return get(opts, (err, res) => (err ? reject(err) : resolve(distillResponse(res))))
57
+ })
57
58
  },
58
59
  }
59
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-aggregator",
3
- "version": "3.1.5",
3
+ "version": "3.1.7",
4
4
  "description": "Fetches and aggregates content from distributed sources for use in an Antora documentation pipeline.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -29,13 +29,13 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@antora/expand-path-helper": "~2.0",
32
- "@antora/logger": "3.1.5",
32
+ "@antora/logger": "3.1.7",
33
33
  "@antora/user-require-helper": "~2.0",
34
34
  "braces": "~3.0",
35
35
  "cache-directory": "~2.0",
36
36
  "glob-stream": "~7.0",
37
37
  "hpagent": "~1.2",
38
- "isomorphic-git": "~1.21",
38
+ "isomorphic-git": "~1.25",
39
39
  "js-yaml": "~4.1",
40
40
  "multi-progress": "~4.0",
41
41
  "picomatch": "~2.3",