@antora/content-aggregator 3.2.0-alpha.1 → 3.2.0-alpha.10

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.
@@ -1,32 +1,32 @@
1
1
  'use strict'
2
2
 
3
3
  const computeOrigin = require('./compute-origin')
4
- const { createHash } = require('crypto')
4
+ const { createHash } = require('node: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
- const EventEmitter = require('events')
8
+ const EventEmitter = require('node:events')
10
9
  const expandPath = require('@antora/expand-path-helper')
11
10
  const File = require('./file')
12
11
  const filterRefs = require('./filter-refs')
13
- const fs = require('fs')
12
+ const fs = require('node:fs')
14
13
  const { promises: fsp } = fs
15
14
  const getCacheDir = require('cache-directory')
16
15
  const GitCredentialManagerStore = require('./git-credential-manager-store')
17
16
  const git = require('./git')
18
17
  const { NotFoundError, ObjectTypeError, UnknownTransportError, UrlParseError } = git.Errors
19
- const globStream = require('glob-stream')
18
+ const { globStream } = require('fast-glob')
19
+ const { inspect } = require('node:util')
20
20
  const invariably = require('./invariably')
21
21
  const logger = require('./logger')
22
22
  const { makeMatcherRx, versionMatcherOpts: VERSION_MATCHER_OPTS } = require('./matcher')
23
23
  const MultiProgress = require('multi-progress') // calls require('progress') as a peer dependencies
24
- const ospath = require('path')
24
+ const ospath = require('node:path')
25
25
  const { posix: path } = ospath
26
26
  const posixify = require('./posixify')
27
27
  const removeGitSuffix = require('./remove-git-suffix')
28
28
  const { fs: resolvePathGlobsFs, git: resolvePathGlobsGit } = require('./resolve-path-globs')
29
- const { pipeline, Writable } = require('stream')
29
+ const { pipeline, Writable } = require('node:stream')
30
30
  const forEach = (write) => new Writable({ objectMode: true, write })
31
31
  const userRequire = require('@antora/user-require-helper')
32
32
  const yaml = require('js-yaml')
@@ -87,138 +87,128 @@ const URL_PORT_CLEANER_RX = /^([^/]+):[0-9]+(?=\/)/
87
87
  */
88
88
  function aggregateContent (playbook) {
89
89
  const startDir = playbook.dir || '.'
90
- const { branches, editUrl, tags, sources } = playbook.content
91
- const sourceDefaults = { branches, editUrl, tags }
90
+ const { branches, editUrl, tags, sources, worktrees } = playbook.content
91
+ const sourceDefaults = { branches, editUrl, tags, worktrees }
92
92
  const { cacheDir: requestedCacheDir, fetch, quiet } = playbook.runtime
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
- const progress = !quiet && createProgress(sourcesByUrl.keys(), process.stdout)
103
+ const progress = quiet ? undefined : createProgress(sourcesByUrl.keys(), process.stdout)
101
104
  const refPatternCache = Object.assign(new Map(), { braces: new Map() })
102
- const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir, refPatternCache }
103
- return collectFiles(sourcesByUrl, loadOpts, fetchConcurrency).then(buildAggregate, (err) => {
104
- progress && progress.terminate()
105
+ const fetchConfig = { always: fetch, depth: Math.max(0, gitConfig.fetchDepth ?? 1) }
106
+ const loadOpts = { cacheDir, fetch: fetchConfig, gitPlugins, progress, startDir, refPatternCache }
107
+ return collectFiles(sourcesByUrl, loadOpts, concurrency).then(buildAggregate, (err) => {
108
+ progress?.terminate()
105
109
  throw err
106
110
  })
107
111
  })
108
112
  }
109
113
 
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
114
+ async function collectFiles (sourcesByUrl, loadOpts, concurrency, fetchedUrls = []) {
115
+ const loadTasks = [...sourcesByUrl.entries()].map(([url, sources]) => {
116
+ const loadOptsForUrl = Object.assign({}, loadOpts)
117
+ if (loadOpts.fetch.always && fetchedUrls.length && ~fetchedUrls.indexOf(url)) loadOptsForUrl.fetch.always = false
118
+ if (tagsSpecified(sources)) loadOptsForUrl.fetch.tags = true
119
+ return loadRepository.bind(null, url, loadOptsForUrl, { url, sources })
120
+ })
121
+ return gracefulPromiseAllWithLimit(loadTasks, concurrency.fetch).then(([results, rejections]) => {
122
+ if (rejections.length) {
123
+ if (concurrency.fetch > 1 && results.length > 1 && rejections.every(({ recoverable }) => recoverable)) {
124
+ if (loadOpts.progress) loadOpts.progress.terminate() // reset cursor position and allow it be reused
125
+ const msg0 = 'An unexpected error occurred while fetching content sources concurrently.'
126
+ const msg1 = 'Retrying with git.fetch_concurrency value of 1.'
127
+ logger.warn(rejections[0], msg0 + ' ' + msg1)
128
+ const fulfilledUrls = results.filter((it) => it?.repo.url).map((it) => it.url)
129
+ return collectFiles(sourcesByUrl, loadOpts, Object.assign(concurrency, { fetch: 1 }), fulfilledUrls)
130
+ }
131
+ throw rejections[0]
143
132
  }
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
- )
133
+ return Promise.all(
134
+ results.map(({ repo, authStatus, sources }) =>
135
+ selectStartPathsForRepository(repo, sources).then((startPaths) =>
136
+ collectFilesFromStartPaths.bind(null, startPaths, repo, authStatus)
137
+ )
138
+ )
139
+ ).then((collectTasks) => promiseAllWithLimit(collectTasks, concurrency.read))
140
+ })
153
141
  }
154
142
 
155
143
  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)
144
+ const entries = Object.assign(new Map(), { accum: [] })
145
+ for (const batchesForOrigin of componentVersionBuckets) {
146
+ for (const batch of batchesForOrigin) {
147
+ let key, entry
148
+ if ((entry = entries.get((key = batch.version + '@' + batch.name)))) {
162
149
  const { files, origins } = batch
163
150
  ;(batch.files = entry.files).push(...files)
164
151
  ;(batch.origins = entry.origins).push(origins[0])
165
152
  Object.assign(entry, batch)
166
- return accum
167
- }, new Map())
168
- .values(),
169
- ]
153
+ } else {
154
+ entries.set(key, batch).accum.push(batch)
155
+ }
156
+ }
157
+ }
158
+ return entries.accum
170
159
  }
171
160
 
172
- async function loadRepository (url, opts) {
161
+ async function loadRepository (url, opts, result = {}) {
173
162
  let authStatus, dir, repo
174
163
  const cache = { [REF_PATTERN_CACHE_KEY]: opts.refPatternCache }
175
164
  if (~url.indexOf(':') && GIT_URI_DETECTOR_RX.test(url)) {
176
165
  let credentials, displayUrl
177
166
  ;({ displayUrl, url, credentials } = extractCredentials(url))
178
- const { cacheDir, fetch, fetchTags, gitPlugins, progress } = opts
167
+ const { cacheDir, fetch, gitPlugins, progress } = opts
179
168
  dir = ospath.join(cacheDir, generateCloneFolderName(displayUrl))
180
169
  // NOTE the presence of the url property on the repo object implies the repository is remote
181
170
  repo = { cache, dir, fs, gitdir: dir, noCheckout: true, url }
171
+ const { credentialManager } = gitPlugins
182
172
  const validStateFile = ospath.join(dir, VALID_STATE_FILENAME)
183
173
  try {
184
174
  await fsp.access(validStateFile)
185
- if (fetch) {
175
+ if (fetch.always) {
186
176
  await fsp.unlink(validStateFile)
187
- const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetchTags, 'fetch')
177
+ const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetch, 'fetch')
188
178
  await git
189
179
  .fetch(fetchOpts)
190
180
  .then(() => {
191
- authStatus = identifyAuthStatus(gitPlugins.credentialManager, credentials, url)
181
+ authStatus = identifyAuthStatus(credentialManager, credentials, url)
192
182
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
193
183
  })
194
184
  .catch((fetchErr) => {
195
- if (fetchOpts.onProgress) fetchOpts.onProgress.finish(fetchErr)
185
+ fetchOpts.onProgress?.finish(fetchErr)
196
186
  if (HTTP_ERROR_CODE_RX.test(fetchErr.code) && fetchErr.data.statusCode === 401) fetchErr.rethrow = true
197
187
  throw fetchErr
198
188
  })
199
189
  .then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
200
- .then(() => fetchOpts.onProgress && fetchOpts.onProgress.finish())
190
+ .then(() => fetchOpts.onProgress?.finish())
201
191
  } else {
202
192
  authStatus = await git.getConfig(Object.assign({ path: 'remote.origin.private' }, repo))
203
193
  }
204
194
  } catch (gitErr) {
205
195
  await fsp['rm' in fsp ? 'rm' : 'rmdir'](dir, { recursive: true, force: true })
206
196
  if (gitErr.rethrow) throw transformGitCloneError(gitErr, displayUrl)
207
- const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetchTags, 'clone')
197
+ const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetch, 'clone')
208
198
  await git
209
199
  .clone(fetchOpts)
210
200
  .then(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo)))
211
201
  .then(() => {
212
- authStatus = identifyAuthStatus(gitPlugins.credentialManager, credentials, url)
202
+ authStatus = identifyAuthStatus(credentialManager, credentials, url)
213
203
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
214
204
  })
215
205
  .catch((cloneErr) => {
216
- // FIXME triggering the error handler here causes assertion problems in the test suite
217
- //if (fetchOpts.onProgress) fetchOpts.onProgress.finish(cloneErr)
218
- throw transformGitCloneError(cloneErr, displayUrl)
206
+ fetchOpts.onProgress?.finish(cloneErr)
207
+ const authRequested = credentialManager.status({ url }) === 'requested'
208
+ throw transformGitCloneError(cloneErr, displayUrl, authRequested)
219
209
  })
220
210
  .then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
221
- .then(() => fetchOpts.onProgress && fetchOpts.onProgress.finish())
211
+ .then(() => fetchOpts.onProgress?.finish())
222
212
  }
223
213
  } else if (await isDirectory((dir = expandPath(url, { dot: opts.startDir })))) {
224
214
  const dotgit = ospath.join(dir, '.git')
@@ -239,7 +229,7 @@ async function loadRepository (url, opts) {
239
229
  } else {
240
230
  throw new Error(`Local content source does not exist: ${dir}${url !== dir ? ' (url: ' + url + ')' : ''}`)
241
231
  }
242
- return { repo, authStatus }
232
+ return Object.assign(result, { repo, authStatus })
243
233
  }
244
234
 
245
235
  function extractCredentials (url) {
@@ -254,34 +244,61 @@ function extractCredentials (url) {
254
244
  // NOTE if only username is present, assume it's an oauth token and set password to empty string
255
245
  const credentials = username ? { username, password: password || '' } : {}
256
246
  return { displayUrl, url, credentials }
257
- } else if (url.startsWith('git@')) {
258
- return { displayUrl: url, url: 'https://' + url.substr(4).replace(':', '/') }
259
- } else {
260
- return { displayUrl: url, url }
261
247
  }
248
+ if (url.startsWith('git@')) return { displayUrl: url, url: 'https://' + url.substr(4).replace(':', '/') }
249
+ return { displayUrl: url, url }
262
250
  }
263
251
 
264
- async function collectFilesFromSource (source, repo, remoteName, authStatus) {
265
- const originUrl = repo.url || (await resolveRemoteUrl(repo, remoteName))
266
- return selectReferences(source, repo, remoteName).then((refs) => {
267
- if (!refs.length) {
268
- const { url, branches, tags, startPath, startPaths } = source
252
+ async function selectStartPathsForRepository (repo, sources) {
253
+ const startPaths = []
254
+ const originUrls = {}
255
+ for (const source of sources) {
256
+ const { version, editUrl } = source
257
+ let remoteName, originUrl
258
+ if (repo.url) {
259
+ remoteName = 'origin' // NOTE if repository is managed (has url property), we can assume remote name is origin
260
+ originUrl = repo.url
261
+ } else {
262
+ remoteName = source.remote || 'origin'
263
+ originUrl =
264
+ remoteName in originUrls
265
+ ? originUrls[remoteName]
266
+ : (originUrls[remoteName] = await resolveRemoteUrl(repo, remoteName))
267
+ if (!originUrl) {
268
+ remoteName = undefined
269
+ if ((originUrl = posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir).indexOf(' ')) {
270
+ originUrl = originUrl.replace(SPACE_RX, '%20')
271
+ }
272
+ }
273
+ }
274
+ const refs = await selectReferences(source, repo, remoteName)
275
+ if (refs.length) {
276
+ for (const ref of refs) {
277
+ for (const startPath of await selectStartPaths(source, repo, ref)) {
278
+ startPaths.push({ startPath, ref, originUrl, editUrl, version })
279
+ }
280
+ }
281
+ } else {
282
+ const { url, branches, tags } = source
269
283
  const startPathInfo =
270
- 'startPaths' in source ? { 'start paths': startPaths || undefined } : { 'start path': startPath || undefined }
284
+ 'startPaths' in source
285
+ ? { 'start paths': source.startPaths || undefined }
286
+ : { 'start path': source.startPath || undefined }
271
287
  const sourceInfo = yaml.dump({ url, branches, tags, ...startPathInfo }, { flowLevel: 1 }).trimRight()
272
288
  logger.info(`No matching references found for content source entry (${sourceInfo.replace(NEWLINE_RX, ' | ')})`)
273
- return []
274
289
  }
275
- return Promise.all(refs.map((it) => collectFilesFromReference(source, repo, remoteName, authStatus, it, originUrl)))
276
- })
290
+ }
291
+ return startPaths
277
292
  }
278
293
 
279
294
  // QUESTION should we resolve HEAD to a ref eagerly to avoid having to do a match on it?
280
295
  async function selectReferences (source, repo, remote) {
281
296
  let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns } = source
282
- const isBare = repo.noCheckout
297
+ const managed = 'url' in repo
298
+ const isBare = managed || repo.noCheckout
283
299
  const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
284
- const noWorktree = repo.url ? undefined : false
300
+ const noWorktree = managed ? undefined : false
301
+ const isLinkedWorktree = repo.worktree?.name
285
302
  const refs = new Map()
286
303
  if (
287
304
  tagPatterns &&
@@ -297,46 +314,63 @@ async function selectReferences (source, repo, remote) {
297
314
  }
298
315
  }
299
316
  }
300
- if (!branchPatterns) return [...refs.values()]
301
- branchPatterns = Array.isArray(branchPatterns)
302
- ? branchPatterns.map((pattern) => String(pattern))
303
- : splitRefPatterns(String(branchPatterns))
304
- if (!branchPatterns.length) return [...refs.values()]
305
- const worktreeName = repo.worktreeName // possibly switch to worktree property ({ name, dir}) in future
306
- if (worktreeName) branchPatterns = branchPatterns.map((it) => (it === 'HEAD' ? 'HEAD@' + worktreeName : it))
307
- if (worktreePatterns) {
317
+ if (
318
+ !branchPatterns ||
319
+ !(branchPatterns = Array.isArray(branchPatterns)
320
+ ? branchPatterns.map((pattern) => String(pattern))
321
+ : splitRefPatterns(String(branchPatterns))).length
322
+ ) {
323
+ return [...refs.values()]
324
+ }
325
+ let useWorktree = false
326
+ if (!managed && (useWorktree = {})) {
308
327
  if (worktreePatterns === '.') {
309
- worktreePatterns = ['.']
328
+ isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
329
+ worktreePatterns = []
330
+ } else if (!worktreePatterns) {
331
+ worktreePatterns = []
310
332
  } else if (worktreePatterns === true) {
311
- worktreePatterns = ['.', '*']
333
+ if (!isBare) useWorktree.main = true
334
+ // NOTE if we don't start at a linked worktree, linked worktree cannot be current worktree
335
+ if (isLinkedWorktree) useWorktree.linked = isLinkedWorktree
336
+ worktreePatterns = ['*']
337
+ } else if (worktreePatterns === '/.') {
338
+ if (!isBare) useWorktree.main = true
339
+ worktreePatterns = []
312
340
  } else {
313
- worktreePatterns = Array.isArray(worktreePatterns)
314
- ? worktreePatterns.map((pattern) => String(pattern))
315
- : splitRefPatterns(String(worktreePatterns))
316
- if (worktreeName) worktreePatterns = worktreePatterns.map((it) => (it === '@' ? worktreeName : it))
341
+ worktreePatterns = (
342
+ Array.isArray(worktreePatterns)
343
+ ? worktreePatterns.map((pattern) => String(pattern))
344
+ : splitRefPatterns(String(worktreePatterns))
345
+ ).reduce((accum, it) => {
346
+ if (it === '/.') return (isBare || (useWorktree.main = true)) && accum
347
+ if (it === '.') {
348
+ isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
349
+ } else {
350
+ accum.push(it)
351
+ }
352
+ return accum
353
+ }, [])
317
354
  }
318
- } else {
319
- worktreePatterns = worktreePatterns === undefined ? [worktreeName || '.'] : []
355
+ if (!(useWorktree.main || useWorktree.linked)) useWorktree = false
320
356
  }
321
- if (branchPatterns.length === 1 && (branchPatterns[0] === 'HEAD' || branchPatterns[0] === '.')) {
322
- const currentBranch = await getCurrentBranchName(repo, remote)
323
- if (currentBranch) {
324
- branchPatterns = [currentBranch]
325
- } else if (isBare) {
326
- return [...refs.values()]
327
- } else {
328
- // NOTE current branch is undefined when HEAD is detached
329
- const head = worktreePatterns[0] === '.' ? repo.dir : noWorktree
330
- refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
331
- return [...refs.values()]
332
- }
333
- } else {
357
+ let currentBranch
358
+ if (!isLinkedWorktree) {
334
359
  let headBranchIdx
335
- // NOTE we can assume at least two entries if HEAD or . are present
336
- if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
337
- const currentBranch = await getCurrentBranchName(repo, remote)
338
- if (currentBranch) {
339
- // NOTE ignore if current branch is already in list
360
+ if (branchPatterns.length === 1 && (branchPatterns[0] === 'HEAD' || branchPatterns[0] === '.')) {
361
+ if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
362
+ branchPatterns = [currentBranch]
363
+ } else if (isBare) {
364
+ return [...refs.values()]
365
+ } else {
366
+ // NOTE current branch is undefined when HEAD is detached
367
+ const head = useWorktree.main ? repo.dir : noWorktree
368
+ refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
369
+ return [...refs.values()]
370
+ }
371
+ } else if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
372
+ // NOTE we can assume at least two entries if HEAD or . are present
373
+ if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
340
374
  if (~branchPatterns.indexOf(currentBranch)) {
341
375
  branchPatterns.splice(headBranchIdx, 1)
342
376
  } else {
@@ -345,11 +379,7 @@ async function selectReferences (source, repo, remote) {
345
379
  } else if (isBare) {
346
380
  branchPatterns.splice(headBranchIdx, 1)
347
381
  } else {
348
- let head = noWorktree
349
- if (worktreePatterns[0] === '.') {
350
- worktreePatterns = worktreePatterns.slice(1)
351
- head = repo.dir
352
- }
382
+ const head = useWorktree.main ? repo.dir : noWorktree
353
383
  // NOTE current branch is undefined when HEAD is detached
354
384
  refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
355
385
  branchPatterns.splice(headBranchIdx, 1)
@@ -357,40 +387,51 @@ async function selectReferences (source, repo, remote) {
357
387
  }
358
388
  }
359
389
  // NOTE isomorphic-git includes HEAD in list of remote branches (see https://isomorphic-git.org/docs/listBranches)
360
- const remoteBranches = (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
390
+ const remoteBranches = remote
391
+ ? (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
392
+ : []
361
393
  if (remoteBranches.length) {
362
394
  for (const shortname of filterRefs(remoteBranches, branchPatterns, patternCache)) {
363
395
  const fullname = 'remotes/' + remote + '/' + shortname
364
396
  refs.set(shortname, { shortname, fullname, type: 'branch', remote, head: noWorktree })
365
397
  }
366
398
  }
367
- // NOTE only consider local branches if repo has a worktree or there are no remote tracking branches
368
- if (!isBare) {
369
- const localBranches = await git.listBranches(repo)
370
- if (localBranches.length) {
371
- const worktrees = await findWorktrees(repo, worktreePatterns)
372
- let onMatch
373
- if ((worktreePatterns.join('') || '.') !== '.') {
374
- const symbolicNames = new Map()
375
- worktrees.forEach(({ name, symbolicName = 'HEAD@' + name }, shortname) => {
376
- localBranches.push(symbolicName)
377
- symbolicNames.set(symbolicName, shortname)
378
- })
379
- onMatch = (candidate, { pattern }) => {
380
- const shortname = symbolicNames.get(candidate)
381
- return shortname ? (pattern.startsWith('HEAD@') ? shortname : undefined) : candidate
399
+ if (!managed) {
400
+ const localBranches = await git.listBranches(repo).then((branches) => {
401
+ if (branches.length || isBare) return branches
402
+ if (currentBranch != null) return [currentBranch]
403
+ return getCurrentBranchName(repo).then((branch) => (branch ? [branch] : []))
404
+ })
405
+ const worktrees = await findWorktrees(repo, worktreePatterns, useWorktree)
406
+ let onMatch
407
+ if ((useWorktree || worktreePatterns.length) && worktrees.size) {
408
+ const headNames = new Map()
409
+ worktrees.forEach(({ name, symbolicNames }, shortname) => {
410
+ if (name) {
411
+ const headName = 'HEAD@' + name
412
+ localBranches.push(headName)
413
+ headNames.set(headName, shortname)
382
414
  }
383
- }
384
- for (const shortname of filterRefs(localBranches, branchPatterns, patternCache, onMatch)) {
385
- const head = (worktrees.get(shortname) || { head: noWorktree }).head
386
- refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
415
+ if (symbolicNames) {
416
+ for (const symbolicName of symbolicNames) {
417
+ const symbolicHeadName = symbolicName === 'HEAD' ? symbolicName : 'HEAD@' + symbolicName
418
+ localBranches.push(symbolicHeadName)
419
+ headNames.set(symbolicHeadName, shortname)
420
+ }
421
+ }
422
+ })
423
+ onMatch = (candidate, { pattern }) => {
424
+ const shortname = headNames.get(candidate)
425
+ if (!shortname) return candidate
426
+ if (pattern === 'HEAD' || pattern.startsWith('HEAD@')) return shortname
387
427
  }
388
428
  }
389
- } else if (!remoteBranches.length) {
390
- const localBranches = await git.listBranches(repo)
391
429
  if (localBranches.length) {
392
- for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
393
- refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head: noWorktree })
430
+ const preferRemote = isBare && remoteBranches.length > 0
431
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache, onMatch)) {
432
+ if (preferRemote && refs.has(shortname)) continue
433
+ const head = (worktrees.get(shortname) || { head: false }).head
434
+ refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
394
435
  }
395
436
  }
396
437
  }
@@ -398,22 +439,21 @@ async function selectReferences (source, repo, remote) {
398
439
  }
399
440
 
400
441
  /**
401
- * Returns the current branch name unless the HEAD is detached.
442
+ * Returns the current branch name or undefined if the HEAD is detached.
402
443
  */
403
444
  function getCurrentBranchName (repo, remote) {
404
445
  return (
405
- repo.noCheckout && remote
446
+ remote && repo.noCheckout
406
447
  ? git
407
- .resolveRef(Object.assign({ ref: 'refs/remotes/' + remote + '/HEAD', depth: 2 }, repo))
408
- .catch(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 2 }, repo)))
448
+ .resolveRef(Object.assign({ ref: 'refs/remotes/' + remote + '/HEAD', depth: 2 }, repo))
449
+ .catch(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 2 }, repo)))
409
450
  : git.resolveRef(Object.assign({ ref: 'HEAD', depth: 2 }, repo))
410
451
  ).then((ref) => (ref.startsWith('refs/') ? ref.replace(SHORTEN_REF_RX, '') : undefined))
411
452
  }
412
453
 
413
- async function collectFilesFromReference (source, repo, remoteName, authStatus, ref, originUrl) {
454
+ async function selectStartPaths (source, repo, ref) {
414
455
  const url = repo.url
415
456
  const displayUrl = url || repo.dir
416
- const { version, editUrl } = source
417
457
  const worktreePath = ref.head
418
458
  if (!worktreePath) {
419
459
  ref.oid = await git.resolveRef(
@@ -433,25 +473,31 @@ async function collectFilesFromReference (source, repo, remoteName, authStatus,
433
473
  const flag = worktreePath ? ' <worktree>' : ref.remote && worktreePath === false ? ` <remotes/${ref.remote}>` : ''
434
474
  throw new Error(`no start paths found in ${where} (${ref.type}: ${ref.shortname}${flag})`)
435
475
  }
436
- return Promise.all(
437
- startPaths.map((startPath) =>
438
- collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
439
- )
440
- )
476
+ return startPaths
477
+ }
478
+ return [cleanStartPath(coerceToString(source.startPath))]
479
+ }
480
+
481
+ async function collectFilesFromStartPaths (startPaths, repo, authStatus) {
482
+ const buckets = []
483
+ for (const { startPath, ref, originUrl, editUrl, version } of startPaths) {
484
+ buckets.push(await collectFilesFromStartPath(startPath, repo, authStatus, ref, originUrl, editUrl, version))
441
485
  }
442
- const startPath = cleanStartPath(coerceToString(source.startPath))
443
- return collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
486
+ repo.cache = undefined
487
+ return buckets
444
488
  }
445
489
 
446
- function collectFilesFromStartPath (startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version) {
490
+ function collectFilesFromStartPath (startPath, repo, authStatus, ref, originUrl, editUrl, version) {
491
+ const worktreePath = ref.head
447
492
  const origin = computeOrigin(originUrl, authStatus, repo.gitdir, ref, startPath, worktreePath, editUrl)
448
493
  return (worktreePath ? readFilesFromWorktree(origin) : readFilesFromGitTree(repo, ref.oid, startPath))
449
- .then((files) =>
450
- Object.assign(deepClone((origin.descriptor = loadComponentDescriptor(files, ref, version))), {
451
- files: files.map((file) => assignFileProperties(file, origin)),
452
- origins: [origin],
453
- })
454
- )
494
+ .then((files) => {
495
+ const batch = deepClone((origin.descriptor = loadComponentDescriptor(files, ref, version)))
496
+ if ('nav' in batch && Array.isArray(batch.nav)) batch.nav.origin = origin
497
+ batch.files = files.map((file) => assignFileProperties(file, origin))
498
+ batch.origins = [origin]
499
+ return batch
500
+ })
455
501
  .catch((err) => {
456
502
  const where = worktreePath || (worktreePath === false ? repo.gitdir : repo.url || repo.dir)
457
503
  const flag = worktreePath ? ' <worktree>' : ref.remote && worktreePath === false ? ` <remotes/${ref.remote}>` : ''
@@ -476,19 +522,18 @@ function readFilesFromWorktree (origin) {
476
522
  }
477
523
 
478
524
  function srcFs (cwd, origin) {
479
- return new Promise((resolve, reject, cache = Object.create(null), files = [], relpathStart = cwd.length + 1) =>
525
+ return new Promise((resolve, reject, files = []) =>
480
526
  pipeline(
481
- globStream(CONTENT_SRC_GLOB, Object.assign({ cache, cwd }, CONTENT_SRC_OPTS)),
482
- forEach(({ path: abspathPosix }, _, done) => {
483
- if ((cache[abspathPosix] || {}).constructor === Array) return done() // detects some directories
484
- const abspath = posixify ? ospath.normalize(abspathPosix) : abspathPosix
485
- const relpath = abspath.substr(relpathStart)
486
- symlinkAwareStat(abspath).then(
487
- (stat) => {
488
- if (stat.isDirectory()) return done() // detects directories that slipped through cache check
527
+ globStream(CONTENT_SRC_GLOB, Object.assign({ cwd }, CONTENT_SRC_OPTS)),
528
+ forEach(({ path: relpath, dirent }, _, done) => {
529
+ if (dirent.isDirectory()) return done()
530
+ const relpathPosix = relpath
531
+ const abspath = posixify ? ospath.join(cwd, (relpath = ospath.normalize(relpath))) : cwd + '/' + relpath
532
+ fsp.stat(abspath).then(
533
+ (stat) =>
489
534
  fsp.readFile(abspath).then(
490
535
  (contents) => {
491
- files.push(new File({ path: posixify ? posixify(relpath) : relpath, contents, stat, src: { abspath } }))
536
+ files.push(new File({ path: relpathPosix, contents, stat, src: { abspath } }))
492
537
  done()
493
538
  },
494
539
  (readErr) => {
@@ -498,22 +543,28 @@ function srcFs (cwd, origin) {
498
543
  : logger.error(logObject, readErr.message.replace(`'${abspath}'`, relpath))
499
544
  done()
500
545
  }
501
- )
502
- },
546
+ ),
503
547
  (statErr) => {
504
548
  const logObject = { file: { abspath, origin } }
505
- if (statErr.symlink) {
506
- logger.error(
507
- logObject,
508
- (statErr.code === 'ELOOP' ? 'ELOOP: symbolic link cycle, ' : 'ENOENT: broken symbolic link, ') +
509
- `${relpath} -> ${statErr.symlink}`
510
- )
511
- } else if (statErr.code === 'ENOENT') {
512
- logger.warn(logObject, `ENOENT: file or directory disappeared, ${statErr.syscall} ${relpath}`)
549
+ if (dirent.isSymbolicLink()) {
550
+ fsp
551
+ .readlink(abspath)
552
+ .then(
553
+ (symlink) =>
554
+ (statErr.code === 'ELOOP' ? 'ELOOP: symbolic link cycle, ' : 'ENOENT: broken symbolic link, ') +
555
+ `${relpath} -> ${symlink}`,
556
+ () => statErr.message.replace(`'${abspath}'`, relpath)
557
+ )
558
+ .then((message) => {
559
+ logger.error(logObject, message)
560
+ done()
561
+ })
513
562
  } else {
514
- logger.error(logObject, statErr.message.replace(`'${abspath}'`, relpath))
563
+ statErr.code === 'ENOENT'
564
+ ? logger.warn(logObject, `ENOENT: file or directory disappeared, ${statErr.syscall} ${relpath}`)
565
+ : logger.error(logObject, statErr.message.replace(`'${abspath}'`, relpath))
566
+ done()
515
567
  }
516
- done()
517
568
  }
518
569
  )
519
570
  }),
@@ -523,26 +574,25 @@ function srcFs (cwd, origin) {
523
574
  }
524
575
 
525
576
  function readFilesFromGitTree (repo, oid, startPath) {
526
- return git
527
- .readTree(Object.assign({ oid }, repo))
528
- .then((root) =>
529
- getGitTreeAtStartPath(repo, oid, startPath).then((start) =>
530
- srcGitTree(repo, Object.assign(root, { dirname: '' }), start)
531
- )
532
- )
577
+ return git.readTree(Object.assign({ oid }, repo)).then((root) => {
578
+ Object.assign(root, { dirname: '' })
579
+ return startPath
580
+ ? getGitTreeAtStartPath(repo, oid, startPath).then((start) => {
581
+ Object.assign(start, { dirname: startPath })
582
+ return srcGitTree(repo, root, start)
583
+ })
584
+ : srcGitTree(repo, root)
585
+ })
533
586
  }
534
587
 
535
588
  function getGitTreeAtStartPath (repo, oid, startPath) {
536
- return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).then(
537
- (result) => Object.assign(result, { dirname: startPath }),
538
- (err) => {
539
- const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
540
- throw new Error(`the start path '${startPath}' ${m}`)
541
- }
542
- )
589
+ return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).catch((err) => {
590
+ const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
591
+ throw new Error(`the start path '${startPath}' ${m}`)
592
+ })
543
593
  }
544
594
 
545
- function srcGitTree (repo, root, start) {
595
+ function srcGitTree (repo, root, start = root) {
546
596
  return new Promise((resolve, reject) => {
547
597
  const files = []
548
598
  createGitTreeWalker(repo, root, filterGitEntry, gitEntryToFile)
@@ -585,7 +635,8 @@ function visitGitTree (emitter, repo, root, filter, convert, parent, dirname = '
585
635
  (target) => {
586
636
  if (target.type === 'tree') {
587
637
  return visitGitTree(emitter, repo, root, filter, convert, target, vfilePath, target.following)
588
- } else if (target.type === 'blob' && filterVerdict === true && (mode = FILE_MODES[target.mode])) {
638
+ }
639
+ if (target.type === 'blob' && filterVerdict === true && (mode = FILE_MODES[target.mode])) {
589
640
  return convert(Object.assign({ mode, oid: target.oid, path: vfilePath }, repo)).then((result) =>
590
641
  emitter.emit('entry', result)
591
642
  )
@@ -657,11 +708,11 @@ function readGitObjectAtPath (repo, root, parent, pathSegments, following) {
657
708
  if (entry.path === firstPathSegment) {
658
709
  return entry.type === 'tree'
659
710
  ? git.readTree(Object.assign({ oid: entry.oid }, repo)).then((subtree) => {
660
- Object.assign(subtree, { dirname: path.join(parent.dirname, entry.path) })
661
- return (pathSegments = pathSegments.slice(1)).length
662
- ? readGitObjectAtPath(repo, root, subtree, pathSegments, following)
663
- : Object.assign(subtree, { type: 'tree', following }) // Q: should this create copy?
664
- })
711
+ Object.assign(subtree, { dirname: path.join(parent.dirname, entry.path) })
712
+ return (pathSegments = pathSegments.slice(1)).length
713
+ ? readGitObjectAtPath(repo, root, subtree, pathSegments, following)
714
+ : Object.assign(subtree, { type: 'tree', following }) // Q: should this create copy?
715
+ })
665
716
  : entry.mode === SYMLINK_FILE_MODE
666
717
  ? readGitSymlink(repo, root, parent, entry, following)
667
718
  : Promise.resolve(entry)
@@ -684,8 +735,13 @@ function filterGitEntry (entry) {
684
735
 
685
736
  function gitEntryToFile (entry) {
686
737
  return git.readBlob(entry).then(({ blob: contents }) => {
687
- contents = Buffer.from(contents.buffer)
688
- const stat = Object.assign(new fs.Stats(), { mode: entry.mode, mtime: undefined, size: contents.byteLength })
738
+ const stat = {
739
+ mode: entry.mode,
740
+ size: (contents = Buffer.from(contents.buffer)).byteLength,
741
+ isDirectory: invariably.false,
742
+ isFile: invariably.true,
743
+ isSymbolicLink: invariably.false,
744
+ }
689
745
  return new File({ path: entry.path, contents, stat })
690
746
  })
691
747
  }
@@ -697,7 +753,7 @@ function loadComponentDescriptor (files, ref, version) {
697
753
  files.splice(descriptorFileIdx, 1)
698
754
  let data
699
755
  try {
700
- data = yaml.load(descriptorFile.contents.toString(), { schema: yaml.CORE_SCHEMA })
756
+ data = Object(yaml.load(descriptorFile.contents.toString(), { schema: yaml.CORE_SCHEMA }))
701
757
  } catch (err) {
702
758
  throw Object.assign(err, { message: `${COMPONENT_DESC_FILENAME} has invalid syntax; ${err.message}` })
703
759
  }
@@ -710,18 +766,18 @@ function loadComponentDescriptor (files, ref, version) {
710
766
  if (!version) {
711
767
  if (version === undefined) throw new Error(`${COMPONENT_DESC_FILENAME} is missing a version`)
712
768
  if (version === false) throw new Error(`${COMPONENT_DESC_FILENAME} has an invalid version`)
713
- version = '' + (typeof version === 'number' ? version : '')
769
+ version = typeof version === 'number' ? '' + version : ''
714
770
  } else if (version === true) {
715
771
  version = ref.shortname.replace(PATH_SEPARATOR_RX, '-')
716
772
  } else if (version.constructor === Object) {
717
773
  const refname = ref.shortname
718
774
  let matched
719
775
  if (refname in version) {
720
- matched = version[refname]
776
+ matched = '' + (version[refname] ?? '')
721
777
  } else if (
722
778
  !Object.entries(version).some(([pattern, replacement]) => {
723
- const result = refname.replace(makeMatcherRx(pattern, VERSION_MATCHER_OPTS), '\0' + replacement)
724
- if (result === refname) return false
779
+ const result = refname.replace(makeMatcherRx(pattern, VERSION_MATCHER_OPTS), '\0' + (replacement ?? ''))
780
+ if (result === refname) return false // no match
725
781
  matched = result.substr(1)
726
782
  return true
727
783
  })
@@ -736,7 +792,7 @@ function loadComponentDescriptor (files, ref, version) {
736
792
  throw new Error(`version in ${COMPONENT_DESC_FILENAME} cannot have path segments: ${version}`)
737
793
  }
738
794
  data.version = version
739
- return camelCaseKeys(data, ['asciidoc'])
795
+ return camelCaseKeys(data, ['asciidoc', 'ext'])
740
796
  }
741
797
 
742
798
  function assignFileProperties (file, origin) {
@@ -753,18 +809,20 @@ function assignFileProperties (file, origin) {
753
809
  return file
754
810
  }
755
811
 
756
- function buildFetchOptions (repo, progress, displayUrl, credentialsFromUrl, gitPlugins, fetchTags, operation) {
812
+ function buildFetchOptions (repo, progress, displayUrl, credentialsFromUrl, gitPlugins, fetch, operation) {
757
813
  const { credentialManager, http, urlRouter } = gitPlugins
814
+ const corsProxy = false
815
+ const depth = fetch.depth || undefined
758
816
  const onAuth = resolveCredentials.bind(credentialManager, new Map().set(undefined, credentialsFromUrl))
759
817
  const onAuthFailure = onAuth
760
818
  const onAuthSuccess = (url) => credentialManager.approved({ url })
761
- const opts = Object.assign({ corsProxy: false, depth: 1, http, onAuth, onAuthFailure, onAuthSuccess }, repo)
819
+ const opts = Object.assign({ corsProxy, depth, http, onAuth, onAuthFailure, onAuthSuccess }, repo)
762
820
  if (urlRouter) opts.url = urlRouter.ensureGitSuffix(opts.url)
763
821
  if (progress) opts.onProgress = createProgressListener(progress, displayUrl, operation)
764
822
  if (operation === 'fetch') {
765
823
  opts.prune = true
766
- if (fetchTags) opts.tags = opts.pruneTags = true
767
- } else if (!fetchTags) {
824
+ if (fetch.tags) opts.tags = opts.pruneTags = true
825
+ } else if (!fetch.tags) {
768
826
  opts.noTags = true
769
827
  }
770
828
  return opts
@@ -829,7 +887,6 @@ function onGitProgress ({ phase, loaded, total }) {
829
887
 
830
888
  function onGitComplete (err) {
831
889
  if (err) {
832
- // TODO could use progressBar.interrupt() to replace bar with message instead
833
890
  this.chars.incomplete = '?'
834
891
  this.update(0)
835
892
  // NOTE force progress bar to update regardless of throttle setting
@@ -863,9 +920,8 @@ function identifyAuthStatus (credentialManager, credentials, url) {
863
920
  const status = credentialManager.status({ url })
864
921
  if (credentials) {
865
922
  return typeof status === 'string' && status.startsWith('requested,') ? 'auth-required' : 'auth-embedded'
866
- } else if (status != null) {
867
- return 'auth-required'
868
923
  }
924
+ if (status != null) return 'auth-required'
869
925
  }
870
926
 
871
927
  /**
@@ -874,7 +930,7 @@ function identifyAuthStatus (credentialManager, credentials, url) {
874
930
  * The purpose of this function is generate a safe, unique folder name for the cloned
875
931
  * repository that gets stored in the cache directory.
876
932
  *
877
- * The generated folder name follows the pattern: <basename>-<sha1>-<version>.git
933
+ * The generated folder name follows the pattern: <basename>-<sha1-of-normalized-url>.git
878
934
  *
879
935
  * @param {String} url - The repository URL to convert.
880
936
  * @returns {String} The generated folder name.
@@ -890,21 +946,18 @@ function generateCloneFolderName (url) {
890
946
  *
891
947
  * @param {Repository} repo - The repository on which to operate.
892
948
  * @param {String} remoteName - The name of the remote to resolve.
893
- * @returns {String} The URL of the specified remote, if defined, or the file URI to the local repository.
949
+ * @returns {String} The URL of the specified remote, if defined
894
950
  */
895
951
  function resolveRemoteUrl (repo, remoteName) {
896
952
  return git.getConfig(Object.assign({ path: 'remote.' + remoteName + '.url' }, repo)).then((url) => {
897
- if (url) {
898
- if (url.startsWith('https://') || url.startsWith('http://')) {
899
- return ~url.indexOf('@') ? url.replace(URL_AUTH_CLEANER_RX, '$1') : url
900
- } else if (url.startsWith('git@')) {
901
- return 'https://' + url.substr(4).replace(':', '/')
902
- } else if (url.startsWith('ssh://')) {
903
- return 'https://' + url.substr(url.indexOf('@') + 1 || 6).replace(URL_PORT_CLEANER_RX, '$1')
904
- }
953
+ if (!url) return
954
+ if (url.startsWith('https://') || url.startsWith('http://')) {
955
+ return ~url.indexOf('@') ? url.replace(URL_AUTH_CLEANER_RX, '$1') : url
956
+ }
957
+ if (url.startsWith('git@')) return 'https://' + url.substr(4).replace(':', '/')
958
+ if (url.startsWith('ssh://')) {
959
+ return 'https://' + url.substr(url.indexOf('@') + 1 || 6).replace(URL_PORT_CLEANER_RX, '$1')
905
960
  }
906
- url = posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir
907
- return ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url
908
961
  })
909
962
  }
910
963
 
@@ -918,20 +971,6 @@ function isDirectory (url) {
918
971
  return fsp.stat(url).then((stat) => stat.isDirectory(), invariably.false)
919
972
  }
920
973
 
921
- function symlinkAwareStat (path_) {
922
- return fsp.lstat(path_).then((lstat) => {
923
- if (!lstat.isSymbolicLink()) return lstat
924
- return fsp.stat(path_).catch((statErr) =>
925
- fsp
926
- .readlink(path_)
927
- .catch(invariably.void)
928
- .then((symlink) => {
929
- throw Object.assign(statErr, { symlink })
930
- })
931
- )
932
- })
933
- }
934
-
935
974
  function tagsSpecified (sources) {
936
975
  return sources.some(({ tags }) => tags && (Array.isArray(tags) ? tags.length : true))
937
976
  }
@@ -946,7 +985,7 @@ function loadGitPlugins (gitConfig, networkConfig, startDir) {
946
985
  if (typeof credentialManager.configure === 'function') {
947
986
  credentialManager.configure({ config: gitConfig.credentials, startDir })
948
987
  }
949
- if (typeof credentialManager.status !== 'function') Object.assign(credentialManager, { status () {} })
988
+ if (typeof credentialManager.status !== 'function') Object.assign(credentialManager, { status: invariably.void })
950
989
  } else {
951
990
  credentialManager = new GitCredentialManagerStore().configure({ config: gitConfig.credentials, startDir })
952
991
  }
@@ -979,8 +1018,8 @@ function ensureCacheDir (preferredCacheDir, startDir) {
979
1018
  )
980
1019
  }
981
1020
 
982
- function transformGitCloneError (err, displayUrl) {
983
- let wrappedMsg, trimMessage
1021
+ function transformGitCloneError (err, displayUrl, authRequested) {
1022
+ let wrappedMsg, recoverable, trimMessage
984
1023
  if (HTTP_ERROR_CODE_RX.test(err.code)) {
985
1024
  switch (err.data.statusCode) {
986
1025
  case 401:
@@ -989,30 +1028,31 @@ function transformGitCloneError (err, displayUrl) {
989
1028
  : 'Content repository not found or requires credentials'
990
1029
  break
991
1030
  case 404:
992
- wrappedMsg = 'Content repository not found'
1031
+ wrappedMsg = authRequested
1032
+ ? 'Content repository not found or credentials were rejected'
1033
+ : 'Content repository not found'
993
1034
  break
994
1035
  default:
995
1036
  wrappedMsg = err.message
996
- trimMessage = true
1037
+ recoverable = trimMessage = true
997
1038
  }
998
1039
  } else if (err instanceof UrlParseError || err instanceof UnknownTransportError) {
999
1040
  wrappedMsg = 'Content source uses an unsupported transport protocol'
1000
1041
  } else if (err.code === 'ENOTFOUND') {
1001
1042
  wrappedMsg = `Content repository host could not be resolved: ${err.hostname}`
1002
1043
  } else {
1003
- wrappedMsg = `${err.name}: ${err.message}`
1004
- trimMessage = true
1005
- }
1006
- if (trimMessage) {
1007
- wrappedMsg = ~(wrappedMsg = wrappedMsg.trimRight()).indexOf('. ') ? wrappedMsg : wrappedMsg.replace(/\.$/, '')
1044
+ wrappedMsg = err.message || String(err)
1045
+ recoverable = trimMessage = true
1008
1046
  }
1047
+ if (trimMessage && !~(wrappedMsg = wrappedMsg.trimEnd()).indexOf('. ')) wrappedMsg = wrappedMsg.replace(/\.$/, '')
1009
1048
  const errWrapper = new Error(`${wrappedMsg} (url: ${displayUrl})`)
1010
- errWrapper.stack += `\nCaused by: ${err.stack || 'unknown'}`
1049
+ errWrapper.stack += `\nCaused by: ${err.stack ? inspect(err).replace(/^Error \[(.+?)\](?=: )/, '$1') : err}`
1050
+ if (recoverable) Object.defineProperty(errWrapper, 'recoverable', { value: true })
1011
1051
  return errWrapper
1012
1052
  }
1013
1053
 
1014
1054
  function splitRefPatterns (str) {
1015
- return ~str.indexOf('{') ? str.split(VENTILATED_CSV_RX) : str.split(CSV_RX)
1055
+ return str.split(~str.indexOf('{') ? VENTILATED_CSV_RX : CSV_RX)
1016
1056
  }
1017
1057
 
1018
1058
  function camelCaseKeys (o, stopPaths = [], p = '') {
@@ -1035,54 +1075,93 @@ function coerceToString (value) {
1035
1075
  return value == null ? '' : String(value)
1036
1076
  }
1037
1077
 
1038
- async function resolveRepositoryFromWorktree (repo) {
1078
+ function resolveRepositoryFromWorktree (repo) {
1039
1079
  return fsp
1040
1080
  .readFile(repo.gitdir, 'utf8')
1041
- .then((contents) => contents.trimRight().substr(8))
1042
- .then((worktreeGitdir) => {
1043
- const worktreeName = ospath.basename(worktreeGitdir)
1044
- return fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8').then(
1081
+ .then((contents) => contents.substr(8).trimEnd())
1082
+ .then((worktreeGitdir) =>
1083
+ fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8').then(
1045
1084
  (contents) => {
1046
- const gitdir = ospath.join(worktreeGitdir, contents.trimRight())
1047
- return ospath.basename(gitdir) === '.git'
1048
- ? Object.assign(repo, { dir: ospath.dirname(gitdir), gitdir, worktreeName })
1049
- : Object.assign(repo, { dir: gitdir, gitdir, worktreeName })
1085
+ const gitdir = ospath.join(worktreeGitdir, contents.trimEnd())
1086
+ const dir = ospath.basename(gitdir) === '.git' ? ospath.dirname(gitdir) : gitdir
1087
+ const name = ospath.basename(worktreeGitdir)
1088
+ return Object.assign(repo, { dir, gitdir, worktree: { gitdir: worktreeGitdir, name } })
1050
1089
  },
1051
1090
  () => repo
1052
1091
  )
1053
- })
1092
+ )
1054
1093
  }
1055
1094
 
1056
- function findWorktrees (repo, patterns) {
1057
- if (!patterns.length) return new Map()
1058
- const mainWorktree =
1059
- patterns[0] === '.' && (patterns = patterns.slice(1))
1060
- ? getCurrentBranchName(repo).then((branch) => (branch ? [branch, { head: repo.dir, name: '.' }] : undefined))
1061
- : Promise.resolve()
1062
- const worktreesDir = patterns.length ? ospath.join(repo.dir, '.git', 'worktrees') : undefined
1095
+ function findWorktrees (repo, patterns, useWorktree) {
1096
+ const useLinkedWorktree = !!useWorktree.linked
1097
+ const mainWorktree = useWorktree.main
1098
+ ? getCurrentBranchName(repo).then((branch) => {
1099
+ if (!branch) return
1100
+ return [branch, { head: repo.dir, name: undefined, symbolicNames: useLinkedWorktree ? ['/.'] : ['/.', '.'] }]
1101
+ })
1102
+ : Promise.resolve()
1103
+ if (!(useLinkedWorktree || patterns.length)) return mainWorktree.then((entry) => new Map(entry && [entry]))
1104
+ const worktreesDir = ospath.join(repo.dir, repo.dir === repo.gitdir ? '' : '.git', 'worktrees')
1063
1105
  const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
1064
- return (
1065
- worktreesDir
1066
- ? fsp
1106
+ const scanWorktrees = patterns.length
1107
+ ? fsp
1067
1108
  .readdir(worktreesDir)
1068
1109
  .then((worktreeNames) => filterRefs(worktreeNames, patterns, patternCache), invariably.emptyArray)
1069
- .then((worktreeNames) =>
1070
- Promise.all(
1071
- worktreeNames.map((worktreeName) => {
1072
- const gitdir = ospath.resolve(worktreesDir, worktreeName)
1073
- // NOTE branch name defaults to worktree name if HEAD is detached
1074
- return git
1075
- .currentBranch(Object.assign({}, repo, { gitdir }))
1076
- .then((branch = worktreeName) =>
1077
- fsp
1078
- .readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
1079
- .then((contents) => [branch, { head: ospath.dirname(contents.trimRight()), name: worktreeName }])
1080
- )
1081
- })
1110
+ .then((worktreeNames) => {
1111
+ if (useLinkedWorktree && !~worktreeNames.indexOf(useWorktree.linked)) worktreeNames.push(useWorktree.linked)
1112
+ return worktreeNames
1113
+ })
1114
+ : Promise.resolve(useLinkedWorktree ? [useWorktree.linked] : [])
1115
+ return scanWorktrees
1116
+ .then((worktreeNames) =>
1117
+ Promise.all(
1118
+ worktreeNames.map((name) => {
1119
+ const symbolicNames = useLinkedWorktree && name === useWorktree.linked ? ['.', 'HEAD'] : undefined
1120
+ const gitdir = ospath.resolve(worktreesDir, name)
1121
+ // NOTE branch name defaults to worktree name if HEAD is detached
1122
+ return getCurrentBranchName(Object.assign({}, repo, { gitdir })).then((branch = name) =>
1123
+ fsp
1124
+ .readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
1125
+ .then((contents) => [branch, { head: ospath.dirname(contents.trimEnd()), name, symbolicNames }])
1082
1126
  )
1083
- )
1084
- : Promise.resolve()
1085
- ).then((entries = []) => mainWorktree.then((entry) => new Map(entry ? entries.push(entry) && entries : entries)))
1127
+ })
1128
+ )
1129
+ )
1130
+ .then((entries) => new Map(entries))
1131
+ .then((worktrees) => mainWorktree.then((result) => (result ? worktrees.set(result[0], result[1]) : worktrees)))
1132
+ }
1133
+
1134
+ async function gracefulPromiseAllWithLimit (tasks, limit = Infinity) {
1135
+ const rejections = []
1136
+ const recordRejection = (err) => rejections.push(err) && undefined
1137
+ const started = []
1138
+ if (tasks.length <= limit) {
1139
+ for (const task of tasks) started.push(task().catch(recordRejection))
1140
+ } else {
1141
+ const pending = []
1142
+ for (const task of tasks) {
1143
+ const current = task()
1144
+ .catch(recordRejection)
1145
+ .finally(() => pending.splice(pending.indexOf(current), 1))
1146
+ started.push(current)
1147
+ if (pending.push(current) < limit) continue
1148
+ await Promise.race(pending)
1149
+ if (rejections.length) break
1150
+ }
1151
+ }
1152
+ return Promise.all(started).then((results) => [results, rejections])
1153
+ }
1154
+
1155
+ async function promiseAllWithLimit (tasks, limit = Infinity) {
1156
+ if (tasks.length <= limit) return Promise.all(tasks.map((task) => task()))
1157
+ const started = []
1158
+ const pending = []
1159
+ for (const task of tasks) {
1160
+ const current = task().finally(() => pending.splice(pending.indexOf(current), 1))
1161
+ started.push(current)
1162
+ if (pending.push(current) >= limit) await Promise.race(pending)
1163
+ }
1164
+ return Promise.all(started)
1086
1165
  }
1087
1166
 
1088
1167
  module.exports = aggregateContent