@antora/content-aggregator 3.0.0-alpha.6 → 3.0.0-beta.1

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,19 +1,24 @@
1
1
  'use strict'
2
2
 
3
+ if (!Promise.allSettled) require('./promise-all-settled-polyfill')
4
+
3
5
  const camelCaseKeys = require('camelcase-keys')
4
6
  const { createHash } = require('crypto')
7
+ const createHttpPlugin = require('./git-plugin-http')
8
+ const decodeUint8Array = require('./decode-uint8-array')
5
9
  const EventEmitter = require('events')
6
10
  const expandPath = require('@antora/expand-path-helper')
7
11
  const File = require('./file')
12
+ const filterRefs = require('./filter-refs')
8
13
  const flattenDeep = require('./flatten-deep')
9
14
  const fs = require('fs')
10
15
  const { promises: fsp } = fs
11
16
  const getCacheDir = require('cache-directory')
12
17
  const GitCredentialManagerStore = require('./git-credential-manager-store')
13
- const git = require('isomorphic-git')
14
- const invariably = { false: () => false, void: () => {}, emptyArray: () => [] }
18
+ const git = require('./git')
19
+ const { NotFoundError, ObjectTypeError, UnknownTransportError, UrlParseError } = git.Errors
20
+ const invariably = { true: () => true, false: () => false, void: () => undefined, emptyArray: () => [] }
15
21
  const { makeRe: makePicomatchRx } = require('picomatch')
16
- const matcher = require('matcher')
17
22
  const MultiProgress = require('multi-progress')
18
23
  const ospath = require('path')
19
24
  const { posix: path } = ospath
@@ -21,18 +26,21 @@ const posixify = ospath.sep === '\\' ? (p) => p.replace(/\\/g, '/') : undefined
21
26
  const { fs: resolvePathGlobsFs, git: resolvePathGlobsGit } = require('./resolve-path-globs')
22
27
  const { Transform } = require('stream')
23
28
  const map = (transform, flush = undefined) => new Transform({ objectMode: true, transform, flush })
29
+ const userRequire = require('@antora/user-require-helper')
24
30
  const vfs = require('vinyl-fs')
25
31
  const yaml = require('js-yaml')
26
32
 
27
33
  const {
28
34
  COMPONENT_DESC_FILENAME,
29
35
  CONTENT_CACHE_FOLDER,
30
- CONTENT_GLOB,
36
+ CONTENT_SRC_GLOB,
37
+ CONTENT_SRC_OPTS,
31
38
  FILE_MODES,
32
39
  GIT_CORE,
33
40
  GIT_OPERATION_LABEL_LENGTH,
34
41
  GIT_PROGRESS_PHASES,
35
42
  PICOMATCH_VERSION_OPTS,
43
+ REF_PATTERN_CACHE_KEY,
36
44
  SYMLINK_FILE_MODE,
37
45
  VALID_STATE_FILENAME,
38
46
  } = require('./constants')
@@ -43,12 +51,16 @@ const VENTILATED_CSV_RX = /\s*,\s+/
43
51
  const EDIT_URL_TEMPLATE_VAR_RX = /\{(web_url|ref(?:hash|name)|path)\}/g
44
52
  const GIT_SUFFIX_RX = /(?:(?:(?:\.git)?\/)?\.git|\/)$/
45
53
  const GIT_URI_DETECTOR_RX = /:(?:\/\/|[^/\\])/
54
+ const HEADS_DIR_RX = /^heads\//
46
55
  const HOSTED_GIT_REPO_RX = /^(?:https?:\/\/|.+@)(git(?:hub|lab)\.com|bitbucket\.org|pagure\.io)[/:](.+?)(?:\.git)?$/
56
+ const HTTP_ERROR_CODE_RX = new RegExp('^' + git.Errors.HttpError.code + '$', 'i')
57
+ const PATH_SEPARATOR_RX = /[/]/g
47
58
  const SHORTEN_REF_RX = /^refs\/(?:heads|remotes\/[^/]+|tags)\//
48
59
  const SPACE_RX = / /g
49
60
  const SUPERFLUOUS_SEPARATORS_RX = /^\/+|\/+$|\/+(?=\/)/g
50
61
  const URL_AUTH_CLEANER_RX = /^(https?:\/\/)[^/@]*@/
51
62
  const URL_AUTH_EXTRACTOR_RX = /^(https?:\/\/)(?:([^/:@]+)?(?::([^/@]+)?)?@)?(.*)/
63
+ const URL_PORT_CLEANER_RX = /^([^/]+):[0-9]+(?=\/)/
52
64
 
53
65
  /**
54
66
  * Aggregates files from the specified content sources so they can be loaded
@@ -68,10 +80,8 @@ const URL_AUTH_EXTRACTOR_RX = /^(https?:\/\/)(?:([^/:@]+)?(?::([^/@]+)?)?@)?(.*)
68
80
  * @param {String} [playbook.runtime.cacheDir=undefined] - The base cache directory.
69
81
  * @param {Boolean} [playbook.runtime.fetch=undefined] - Whether to fetch
70
82
  * updates from managed git repositories.
71
- * @param {Boolean} [playbook.runtime.silent=false] - Whether to be silent
72
- * (suppresses progress bars and warnings).
73
- * @param {Boolean} [playbook.runtime.quiet=false] - Whether to be quiet
74
- * (suppresses progress bars).
83
+ * @param {Boolean} [playbook.runtime.quiet=false] - Whether to be suppress progress
84
+ * bars that show progress of clone and fetch operations.
75
85
  * @param {Array} playbook.git - The git configuration object for Antora.
76
86
  * @param {Boolean} [playbook.git.ensureGitSuffix=true] - Whether the .git
77
87
  * suffix is automatically appended to each repository URL, if missing.
@@ -82,116 +92,143 @@ const URL_AUTH_EXTRACTOR_RX = /^(https?:\/\/)(?:([^/:@]+)?(?::([^/@]+)?)?@)?(.*)
82
92
  function aggregateContent (playbook) {
83
93
  const startDir = playbook.dir || '.'
84
94
  const { branches, editUrl, tags, sources } = playbook.content
85
- const sourcesByUrl = sources.reduce(
86
- (accum, source) => accum.set(source.url, [...(accum.get(source.url) || []), source]),
87
- new Map()
88
- )
89
- const { cacheDir, fetch, silent, quiet } = playbook.runtime
90
- const progress = !quiet && !silent && createProgress(sourcesByUrl.keys(), process.stdout)
91
- const { ensureGitSuffix, credentials } = Object.assign({ ensureGitSuffix: true }, playbook.git)
92
- const credentialManager = registerGitPlugins(credentials, playbook.network || {}, startDir).get('credentialManager')
93
- return ensureCacheDir(cacheDir, startDir)
94
- .then((resolvedCacheDir) =>
95
+ const sourceDefaults = { branches, editUrl, tags }
96
+ const { cacheDir: requestedCacheDir, fetch, quiet } = playbook.runtime
97
+ return ensureCacheDir(requestedCacheDir, startDir).then((cacheDir) => {
98
+ const gitConfig = Object.assign({ ensureGitSuffix: true }, playbook.git)
99
+ const gitPlugins = loadGitPlugins(gitConfig, playbook.network || {}, startDir)
100
+ const fetchConcurrency = Math.max(gitConfig.fetchConcurrency || Infinity, 1)
101
+ const sourcesByUrl = sources.reduce((accum, source) => {
102
+ return accum.set(source.url, [...(accum.get(source.url) || []), Object.assign({}, sourceDefaults, source)])
103
+ }, new Map())
104
+ const progress = !quiet && createProgress(sourcesByUrl.keys(), process.stdout)
105
+ const refPatternCache = Object.assign(new Map(), { braces: new Map() })
106
+ const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir, refPatternCache }
107
+ return collectFiles(sourcesByUrl, loadOpts, fetchConcurrency).then(buildAggregate, (err) => {
108
+ progress && progress.terminate()
109
+ throw err
110
+ })
111
+ })
112
+ }
113
+
114
+ async function collectFiles (sourcesByUrl, loadOpts, concurrency) {
115
+ const tasks = [...sourcesByUrl.entries()].map(([url, sources]) => [
116
+ () => loadRepository(url, Object.assign({ fetchTags: tagsSpecified(sources) }, loadOpts)),
117
+ ({ repo, authStatus }) =>
95
118
  Promise.all(
96
- [...sourcesByUrl.entries()].map(([url, sources]) =>
97
- loadRepository(url, {
98
- cacheDir: resolvedCacheDir,
99
- credentialManager,
100
- fetchTags: tagsSpecified(sources, tags),
101
- progress,
102
- fetch,
103
- startDir,
104
- ensureGitSuffix,
105
- }).then(({ repo, authStatus }) =>
106
- Promise.all(
107
- sources.map((source) => {
108
- source = Object.assign({ branches, editUrl, tags }, source)
109
- // NOTE if repository is managed (has a url), we can assume the remote name is origin
110
- // TODO if the repo has no remotes, then remoteName should be undefined
111
- const remoteName = repo.url ? 'origin' : source.remote || 'origin'
112
- return collectFilesFromSource(source, repo, remoteName, authStatus)
113
- })
114
- )
115
- )
116
- )
117
- )
118
- .then(buildAggregate)
119
- .catch((err) => {
120
- progress && progress.terminate()
121
- throw err
119
+ sources.map((source) => {
120
+ // NOTE if repository is managed (has a url property), we can assume the remote name is origin
121
+ // TODO if the repo has no remotes, then remoteName should be undefined
122
+ const remoteName = repo.url ? 'origin' : source.remote || 'origin'
123
+ return collectFilesFromSource(source, repo, remoteName, authStatus)
122
124
  })
123
- )
124
- .finally(unregisterGitPlugins)
125
+ ),
126
+ ])
127
+ let rejected, started
128
+ const startedContinuations = []
129
+ const recordRejection = (err) => {
130
+ throw (rejected = true) && err
131
+ }
132
+ const runTask = (primary, continuation, idx) =>
133
+ primary().then((value) => {
134
+ if (!rejected) startedContinuations[idx] = continuation(value).catch(recordRejection)
135
+ }, recordRejection)
136
+ if (tasks.length > concurrency) {
137
+ started = []
138
+ const pending = []
139
+ for (const [primary, continuation] of tasks) {
140
+ const current = runTask(primary, continuation, started.length).finally(() =>
141
+ pending.splice(pending.indexOf(current), 1)
142
+ )
143
+ started.push(current)
144
+ if (pending.push(current) < concurrency) continue
145
+ if (await Promise.race(pending).then(invariably.true, invariably.false)) continue
146
+ break
147
+ }
148
+ } else {
149
+ started = tasks.map(([primary, continuation], idx) => runTask(primary, continuation, idx))
150
+ }
151
+ return Promise.allSettled(started).then((outcomes) =>
152
+ Promise.allSettled(startedContinuations).then((continuationOutcomes) => {
153
+ const rejection = outcomes.push(...continuationOutcomes) && outcomes.find(({ status }) => status === 'rejected')
154
+ if (rejection) throw rejection.reason
155
+ return continuationOutcomes.map(({ value }) => value)
156
+ })
157
+ )
125
158
  }
126
159
 
127
160
  function buildAggregate (componentVersionBuckets) {
128
- const aggregateMap = flattenDeep(componentVersionBuckets).reduce((accum, batch) => {
129
- const key = batch.version + '@' + batch.name
130
- const entry = accum.get(key)
131
- return accum.set(key, entry ? Object.assign(entry, batch, { files: [...entry.files, ...batch.files] }) : batch)
132
- }, new Map())
133
- return [...aggregateMap.values()]
161
+ return [
162
+ ...flattenDeep(componentVersionBuckets)
163
+ .reduce((accum, batch) => {
164
+ const key = batch.version + '@' + batch.name
165
+ const entry = accum.get(key)
166
+ if (!entry) return accum.set(key, batch)
167
+ const files = batch.files
168
+ ;(batch.files = entry.files).push(...files)
169
+ Object.assign(entry, batch)
170
+ return accum
171
+ }, new Map())
172
+ .values(),
173
+ ]
134
174
  }
135
175
 
136
176
  async function loadRepository (url, opts) {
137
- let dir
138
- let repo
139
- let authStatus
177
+ let authStatus, dir, repo
178
+ const cache = { [REF_PATTERN_CACHE_KEY]: opts.refPatternCache }
140
179
  if (~url.indexOf(':') && GIT_URI_DETECTOR_RX.test(url)) {
141
- let displayUrl
142
- let credentials
180
+ let credentials, displayUrl
143
181
  ;({ displayUrl, url, credentials } = extractCredentials(url))
144
- dir = ospath.join(opts.cacheDir, generateCloneFolderName(displayUrl))
182
+ const { cacheDir, fetch, fetchTags, gitPlugins, progress } = opts
183
+ dir = ospath.join(cacheDir, generateCloneFolderName(displayUrl))
145
184
  // NOTE the presence of the url property on the repo object implies the repository is remote
146
- repo = { core: GIT_CORE, dir, gitdir: dir, url, noGitSuffix: !opts.ensureGitSuffix, noCheckout: true }
147
- const credentialManager = opts.credentialManager
148
- const validStateFile = ospath.join(repo.gitdir, VALID_STATE_FILENAME)
185
+ repo = { cache, dir, fs, gitdir: dir, noCheckout: true, url }
186
+ const validStateFile = ospath.join(dir, VALID_STATE_FILENAME)
149
187
  try {
150
188
  await fsp.access(validStateFile)
151
- if (opts.fetch) {
189
+ if (fetch) {
152
190
  await fsp.unlink(validStateFile)
153
- const fetchOpts = getFetchOptions(repo, opts.progress, displayUrl, credentials, opts.fetchTags, 'fetch')
191
+ const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetchTags, 'fetch')
154
192
  await git
155
193
  .fetch(fetchOpts)
156
194
  .then(() => {
195
+ const credentialManager = gitPlugins.credentialManager
157
196
  authStatus = credentials ? 'auth-embedded' : credentialManager.status({ url }) ? 'auth-required' : undefined
158
- return git.config(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
197
+ return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
159
198
  })
160
199
  .catch((fetchErr) => {
161
- fetchOpts.emitter && fetchOpts.emitter.emit('error', fetchErr)
162
- if (fetchErr.code === git.E.HTTPError && fetchErr.data.statusCode === 401) fetchErr.rethrow = true
200
+ if (fetchOpts.onProgress) fetchOpts.onProgress.finish(fetchErr)
201
+ if (HTTP_ERROR_CODE_RX.test(fetchErr.code) && fetchErr.data.statusCode === 401) fetchErr.rethrow = true
163
202
  throw fetchErr
164
203
  })
165
204
  .then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
166
- .then(() => fetchOpts.emitter && fetchOpts.emitter.emit('complete'))
205
+ .then(() => fetchOpts.onProgress && fetchOpts.onProgress.finish())
167
206
  } else {
168
- // NOTE use cached value from previous fetch
169
- authStatus = await git.config(Object.assign({ path: 'remote.origin.private' }, repo))
207
+ authStatus = await git.getConfig(Object.assign({ path: 'remote.origin.private' }, repo))
170
208
  }
171
209
  } catch (gitErr) {
172
- await rmdir(dir)
210
+ await fsp['rm' in fsp ? 'rm' : 'rmdir'](dir, { recursive: true, force: true })
173
211
  if (gitErr.rethrow) throw transformGitCloneError(gitErr, displayUrl)
174
- const fetchOpts = getFetchOptions(repo, opts.progress, displayUrl, credentials, opts.fetchTags, 'clone')
212
+ const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetchTags, 'clone')
175
213
  await git
176
214
  .clone(fetchOpts)
177
215
  .then(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo)))
178
216
  .then(() => {
217
+ const credentialManager = gitPlugins.credentialManager
179
218
  authStatus = credentials ? 'auth-embedded' : credentialManager.status({ url }) ? 'auth-required' : undefined
180
- return git.config(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
219
+ return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
181
220
  })
182
- .catch(async (cloneErr) => {
183
- await rmdir(dir)
221
+ .catch((cloneErr) => {
184
222
  // FIXME triggering the error handler here causes assertion problems in the test suite
185
- //fetchOpts.emitter && fetchOpts.emitter.emit('error', cloneErr)
223
+ //if (fetchOpts.onProgress) fetchOpts.onProgress.finish(cloneErr)
186
224
  throw transformGitCloneError(cloneErr, displayUrl)
187
225
  })
188
226
  .then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
189
- .then(() => fetchOpts.emitter && fetchOpts.emitter.emit('complete'))
227
+ .then(() => fetchOpts.onProgress && fetchOpts.onProgress.finish())
190
228
  }
191
- } else if (await isDirectory((dir = expandPath(url, '~+', opts.startDir)))) {
192
- repo = (await isDirectory(ospath.join(dir, '.git')))
193
- ? { core: GIT_CORE, dir }
194
- : { core: GIT_CORE, dir, gitdir: dir, noCheckout: true }
229
+ } else if (await isDirectory((dir = expandPath(url, { dot: opts.startDir })))) {
230
+ const gitdir = ospath.join(dir, '.git')
231
+ repo = (await isDirectory(gitdir)) ? { cache, dir, fs, gitdir } : { cache, dir, fs, gitdir: dir, noCheckout: true }
195
232
  try {
196
233
  await git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo))
197
234
  } catch {
@@ -214,8 +251,8 @@ function extractCredentials (url) {
214
251
  // BitBucket: x-token-auth:<token>@
215
252
  const [, scheme, username, password, rest] = url.match(URL_AUTH_EXTRACTOR_RX)
216
253
  const displayUrl = (url = scheme + rest)
217
- // NOTE if only username is present, assume it's an oauth token
218
- const credentials = username ? (password == null ? { token: username } : { username, password }) : {}
254
+ // NOTE if only username is present, assume it's an oauth token and set password to empty string
255
+ const credentials = username ? { username, password: password || '' } : {}
219
256
  return { displayUrl, url, credentials }
220
257
  } else if (url.startsWith('git@')) {
221
258
  return { displayUrl: url, url: 'https://' + url.substr(4).replace(':', '/') }
@@ -235,16 +272,18 @@ async function collectFilesFromSource (source, repo, remoteName, authStatus) {
235
272
  async function selectReferences (source, repo, remote) {
236
273
  let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns = '.' } = source
237
274
  const isBare = repo.noCheckout
275
+ const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
276
+ const noWorktree = repo.url ? undefined : null
238
277
  const refs = new Map()
239
278
  if (tagPatterns) {
240
279
  tagPatterns = Array.isArray(tagPatterns)
241
280
  ? tagPatterns.map((pattern) => String(pattern))
242
- : String(tagPatterns).split(CSV_RX)
281
+ : splitRefPatterns(String(tagPatterns))
243
282
  if (tagPatterns.length) {
244
283
  const tags = await git.listTags(repo)
245
- for (const shortname of tags.length ? matcher(tags, tagPatterns) : tags) {
284
+ for (const shortname of tags.length ? filterRefs(tags, tagPatterns, patternCache) : tags) {
246
285
  // NOTE tags are stored using symbol keys to distinguish them from branches
247
- refs.set(Symbol(shortname), { shortname, fullname: 'tags/' + shortname, type: 'tag' })
286
+ refs.set(Symbol(shortname), { shortname, fullname: 'tags/' + shortname, type: 'tag', head: noWorktree })
248
287
  }
249
288
  }
250
289
  }
@@ -257,7 +296,7 @@ async function selectReferences (source, repo, remote) {
257
296
  } else {
258
297
  worktreePatterns = Array.isArray(worktreePatterns)
259
298
  ? worktreePatterns.map((pattern) => String(pattern))
260
- : String(worktreePatterns).split(CSV_RX)
299
+ : splitRefPatterns(String(worktreePatterns))
261
300
  }
262
301
  }
263
302
  const branchPatternsString = String(branchPatterns)
@@ -268,16 +307,15 @@ async function selectReferences (source, repo, remote) {
268
307
  } else {
269
308
  if (!isBare) {
270
309
  // NOTE current branch is undefined when HEAD is detached
271
- const ref = { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true }
272
- if (worktreePatterns[0] === '.') ref.head = repo.dir
273
- refs.set('HEAD', ref)
310
+ const head = worktreePatterns[0] === '.' ? repo.dir : noWorktree
311
+ refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
274
312
  }
275
313
  return [...refs.values()]
276
314
  }
277
315
  } else if (
278
316
  (branchPatterns = Array.isArray(branchPatterns)
279
317
  ? branchPatterns.map((pattern) => String(pattern))
280
- : branchPatternsString.split(CSV_RX)).length
318
+ : splitRefPatterns(branchPatternsString)).length
281
319
  ) {
282
320
  let headBranchIdx
283
321
  // NOTE we can assume at least two entries if HEAD or . are present
@@ -292,10 +330,13 @@ async function selectReferences (source, repo, remote) {
292
330
  }
293
331
  } else {
294
332
  if (!isBare) {
333
+ let head = noWorktree
334
+ if (worktreePatterns[0] === '.') {
335
+ worktreePatterns = worktreePatterns.slice(1)
336
+ head = repo.dir
337
+ }
295
338
  // NOTE current branch is undefined when HEAD is detached
296
- const ref = { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true }
297
- if (worktreePatterns[0] === '.' && (worktreePatterns = worktreePatterns.slice(1))) ref.head = repo.dir
298
- refs.set('HEAD', ref)
339
+ refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
299
340
  }
300
341
  branchPatterns.splice(headBranchIdx, 1)
301
342
  }
@@ -306,8 +347,9 @@ async function selectReferences (source, repo, remote) {
306
347
  // NOTE isomorphic-git includes HEAD in list of remote branches (see https://isomorphic-git.org/docs/listBranches)
307
348
  const remoteBranches = (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
308
349
  if (remoteBranches.length) {
309
- for (const shortname of matcher(remoteBranches, branchPatterns)) {
310
- refs.set(shortname, { shortname, fullname: path.join('remotes', remote, shortname), type: 'branch', remote })
350
+ for (const shortname of filterRefs(remoteBranches, branchPatterns, patternCache)) {
351
+ const fullname = 'remotes/' + remote + '/' + shortname
352
+ refs.set(shortname, { shortname, fullname, type: 'branch', remote, head: noWorktree })
311
353
  }
312
354
  }
313
355
  // NOTE only consider local branches if repo has a worktree or there are no remote tracking branches
@@ -315,17 +357,17 @@ async function selectReferences (source, repo, remote) {
315
357
  const localBranches = await git.listBranches(repo)
316
358
  if (localBranches.length) {
317
359
  const worktrees = await findWorktrees(repo, worktreePatterns)
318
- for (const shortname of matcher(localBranches, branchPatterns)) {
319
- const ref = { shortname, fullname: 'heads/' + shortname, type: 'branch' }
320
- if (worktrees.has(shortname)) ref.head = worktrees.get(shortname)
321
- refs.set(shortname, ref)
360
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
361
+ const head = worktrees.get(shortname) || noWorktree
362
+ refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
322
363
  }
323
364
  }
324
365
  } else if (!remoteBranches.length) {
325
- // QUESTION should local branches be used if the only remote branch is HEAD?
326
366
  const localBranches = await git.listBranches(repo)
327
- for (const shortname of localBranches.length ? matcher(localBranches, branchPatterns) : localBranches) {
328
- refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch' })
367
+ if (localBranches.length) {
368
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
369
+ refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head: noWorktree })
370
+ }
329
371
  }
330
372
  }
331
373
  }
@@ -366,7 +408,7 @@ async function collectFilesFromReference (source, repo, remoteName, authStatus,
366
408
  ? resolvePathGlobsFs(worktreePath, startPaths)
367
409
  : resolvePathGlobsGit(repo, ref.oid, startPaths))
368
410
  if (!startPaths.length) {
369
- const refInfo = `ref: ${ref.fullname.replace(/^heads\//, '')}${worktreePath ? ' <worktree>' : ''}`
411
+ const refInfo = `ref: ${ref.fullname.replace(HEADS_DIR_RX, '')}${worktreePath ? ' <worktree>' : ''}`
370
412
  throw new Error(`no start paths found in ${displayUrl} (${refInfo})`)
371
413
  }
372
414
  return Promise.all(
@@ -386,12 +428,12 @@ function collectFilesFromStartPath (startPath, repo, authStatus, ref, worktreePa
386
428
  )
387
429
  .then((files) => {
388
430
  const componentVersionBucket = loadComponentDescriptor(files, ref, version)
389
- const origin = computeOrigin(originUrl, authStatus, ref, startPath, worktreePath, editUrl)
431
+ const origin = computeOrigin(originUrl, authStatus, repo.gitdir, ref, startPath, worktreePath, editUrl)
390
432
  componentVersionBucket.files = files.map((file) => assignFileProperties(file, origin))
391
433
  return componentVersionBucket
392
434
  })
393
435
  .catch((err) => {
394
- const refInfo = `ref: ${ref.fullname.replace(/^heads\//, '')}${worktreePath ? ' <worktree>' : ''}`
436
+ const refInfo = `ref: ${ref.fullname.replace(HEADS_DIR_RX, '')}${worktreePath ? ' <worktree>' : ''}`
395
437
  const pathInfo = !startPath || err.message.startsWith('the start path ') ? '' : ' | path: ' + startPath
396
438
  throw Object.assign(err, { message: `${err.message} in ${repo.url || repo.dir} (${refInfo}${pathInfo})` })
397
439
  })
@@ -399,28 +441,32 @@ function collectFilesFromStartPath (startPath, repo, authStatus, ref, worktreePa
399
441
 
400
442
  function readFilesFromWorktree (worktreePath, startPath) {
401
443
  const cwd = ospath.join(worktreePath, startPath)
402
- return fsp
403
- .stat(cwd)
404
- .catch(() => {
405
- throw new Error(`the start path '${startPath}' does not exist`)
406
- })
407
- .then((stat) => {
444
+ return fsp.stat(cwd).then(
445
+ (stat) => {
408
446
  if (!stat.isDirectory()) throw new Error(`the start path '${startPath}' is not a directory`)
409
447
  return new Promise((resolve, reject) =>
410
448
  vfs
411
- .src(CONTENT_GLOB, { cwd, follow: true, removeBOM: false })
449
+ .src(CONTENT_SRC_GLOB, Object.assign({ cwd }, CONTENT_SRC_OPTS))
412
450
  .on('error', (err) => {
413
- if (err.code === 'ENOENT') {
414
- err.message = `Broken symbolic link detected at ${ospath.relative(cwd, err.path)}`
451
+ if (err.code === 'ENOENT' && err.syscall === 'stat') {
452
+ try {
453
+ if (fs.lstatSync(err.path).isSymbolicLink()) {
454
+ err.message = `Broken symbolic link detected at ${ospath.relative(cwd, err.path)}`
455
+ }
456
+ } catch {}
415
457
  } else if (err.code === 'ELOOP') {
416
458
  err.message = `Symbolic link cycle detected at ${ospath.relative(cwd, err.path)}`
417
459
  }
418
460
  reject(err)
419
461
  })
420
462
  .pipe(relativizeFiles())
421
- .pipe(collectFiles(resolve))
463
+ .pipe(collectDataFromStream(resolve))
422
464
  )
423
- })
465
+ },
466
+ () => {
467
+ throw new Error(`the start path '${startPath}' does not exist`)
468
+ }
469
+ )
424
470
  }
425
471
 
426
472
  /**
@@ -449,11 +495,11 @@ function relativizeFiles () {
449
495
  })
450
496
  }
451
497
 
452
- function collectFiles (done) {
498
+ function collectDataFromStream (done) {
453
499
  const accum = []
454
500
  return map(
455
- (file, enc, next) => {
456
- accum.push(file)
501
+ (obj, _, next) => {
502
+ accum.push(obj)
457
503
  next()
458
504
  },
459
505
  () => done(accum)
@@ -471,14 +517,13 @@ function readFilesFromGitTree (repo, oid, startPath) {
471
517
  }
472
518
 
473
519
  function getGitTreeAtStartPath (repo, oid, startPath) {
474
- return git
475
- .readTree(Object.assign({ oid, filepath: startPath }, repo))
476
- .catch(({ code }) => {
477
- throw new Error(
478
- `the start path '${startPath}' ${code === git.E.ResolveTreeError ? 'is not a directory' : 'does not exist'}`
479
- )
480
- })
481
- .then((result) => Object.assign(result, { dirname: startPath }))
520
+ return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).then(
521
+ (result) => Object.assign(result, { dirname: startPath }),
522
+ (err) => {
523
+ const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
524
+ throw new Error(`the start path '${startPath}' ${m}`)
525
+ }
526
+ )
482
527
  }
483
528
 
484
529
  function srcGitTree (repo, root, start) {
@@ -497,9 +542,11 @@ function createGitTreeWalker (repo, root, filter) {
497
542
  walk (start) {
498
543
  return (
499
544
  visitGitTree(this, repo, root, filter, start)
500
- .then(() => this.emit('end'))
501
545
  // NOTE if error is thrown, promises already being resolved won't halt
502
- .catch((err) => this.emit('error', err))
546
+ .then(
547
+ () => this.emit('end'),
548
+ (err) => this.emit('error', err)
549
+ )
503
550
  )
504
551
  },
505
552
  })
@@ -522,23 +569,24 @@ function visitGitTree (emitter, repo, root, filter, parent, dirname = '', follow
522
569
  let mode
523
570
  if (entry.mode === SYMLINK_FILE_MODE) {
524
571
  reads.push(
525
- readGitSymlink(repo, root, parent, entry, following)
526
- .catch((err) => {
527
- // NOTE this error could be caught after promie chain has already been rejected
528
- if (err.code === git.E.TreeOrBlobNotFoundError) {
529
- err.message = `Broken symbolic link detected at ${vfilePath}`
530
- } else if (err.code === 'SymbolicLinkCycleError') {
531
- err.message = `Symbolic link cycle detected at ${vfilePath}`
532
- }
533
- throw err
534
- })
535
- .then((target) => {
572
+ readGitSymlink(repo, root, parent, entry, following).then(
573
+ (target) => {
536
574
  if (target.type === 'tree') {
537
575
  return visitGitTree(emitter, repo, root, filter, target, vfilePath, new Set(following).add(entry.oid))
538
576
  } else if (target.type === 'blob' && filterVerdict === true && (mode = FILE_MODES[target.mode])) {
539
577
  emitter.emit('entry', Object.assign({ mode, oid: target.oid, path: vfilePath }, repo))
540
578
  }
541
- })
579
+ },
580
+ (err) => {
581
+ // NOTE this error could be caught after promise chain has already been rejected
582
+ if (err instanceof NotFoundError) {
583
+ err.message = `Broken symbolic link detected at ${vfilePath}`
584
+ } else if (err.code === 'SymbolicLinkCycleError') {
585
+ err.message = `Symbolic link cycle detected at ${vfilePath}`
586
+ }
587
+ throw err
588
+ }
589
+ )
542
590
  )
543
591
  } else if ((mode = FILE_MODES[entry.mode])) {
544
592
  emitter.emit('entry', Object.assign({ mode, oid: entry.oid, path: vfilePath }, repo))
@@ -552,11 +600,11 @@ function visitGitTree (emitter, repo, root, filter, parent, dirname = '', follow
552
600
  function readGitSymlink (repo, root, parent, { oid }, following) {
553
601
  if (following.size !== (following = new Set(following).add(oid)).size) {
554
602
  return git.readBlob(Object.assign({ oid }, repo)).then(({ blob: target }) => {
555
- target = posixify && process.env.NODE_ENV === 'test' ? posixify(target.toString()) : target.toString()
603
+ target = decodeUint8Array(target)
556
604
  let targetParent
557
605
  if (parent.dirname) {
558
606
  const dirname = parent.dirname + '/'
559
- target = path.join(dirname, target)
607
+ target = path.join(dirname, target) // join doesn't remove trailing separator
560
608
  if (target.startsWith(dirname)) {
561
609
  target = target.substr(dirname.length)
562
610
  targetParent = parent
@@ -564,10 +612,12 @@ function readGitSymlink (repo, root, parent, { oid }, following) {
564
612
  targetParent = root
565
613
  }
566
614
  } else {
567
- target = path.normalize(target)
615
+ target = path.normalize(target) // normalize doesn't remove trailing separator
568
616
  targetParent = root
569
617
  }
570
- return readGitObjectAtPath(repo, root, targetParent, target.split('/'), following)
618
+ const targetSegments = target.split('/')
619
+ if (!targetSegments[targetSegments.length - 1]) targetSegments.pop()
620
+ return readGitObjectAtPath(repo, root, targetParent, targetSegments, following)
571
621
  })
572
622
  }
573
623
  const err = { name: 'SymbolicLinkCycleError', code: 'SymbolicLinkCycleError', oid }
@@ -591,33 +641,25 @@ function readGitObjectAtPath (repo, root, parent, pathSegments, following) {
591
641
  : Promise.resolve(entry)
592
642
  }
593
643
  }
594
- const err = {
595
- name: git.E.TreeOrBlobNotFoundError,
596
- code: git.E.TreeOrBlobNotFoundError,
597
- oid: parent.oid,
598
- filepath: pathSegments.join('/'),
599
- }
600
- return Promise.reject(Object.assign(new Error(`No file or directory found at "${err.oid}:${err.filepath}".`), err))
644
+ return Promise.reject(new NotFoundError(`No file or directory found at "${parent.oid}:${pathSegments.join('/')}"`))
601
645
  }
602
646
 
603
647
  /**
604
- * Returns true if the entry should be processed or false if it should be skipped.
605
- * Ignores files that begin with dot ('.') (entry.path is a basename) or that do
606
- * not have a file extension.
648
+ * Returns true (or 'treeOnly' if the entry is a symlink tree) if the entry
649
+ * should be processed or false if it should be skipped. An entry with a path
650
+ * (basename) that begins with dot ('.') is marked as skipped.
607
651
  */
608
652
  function filterGitEntry (entry) {
609
- return (
610
- entry.path.charAt() !== '.' &&
611
- (entry.type !== 'blob' || entry.path.indexOf('.') > 0 || (entry.mode === SYMLINK_FILE_MODE ? 'treeOnly' : false))
612
- )
653
+ const entryPath = entry.path
654
+ if (entryPath.charAt() === '.') return false
655
+ if (entry.type === 'tree') return entry.mode === SYMLINK_FILE_MODE ? 'treeOnly' : true
656
+ return entryPath.charAt(entryPath.length - 1) !== '~'
613
657
  }
614
658
 
615
659
  function entryToFile (entry) {
616
660
  return git.readBlob(entry).then(({ blob: contents }) => {
617
- const stat = new fs.Stats()
618
- stat.mode = entry.mode
619
- stat.mtime = undefined
620
- stat.size = contents.length
661
+ contents = Buffer.from(contents.buffer)
662
+ const stat = Object.assign(new fs.Stats(), { mode: entry.mode, mtime: undefined, size: contents.byteLength })
621
663
  return new File({ path: entry.path, contents, stat })
622
664
  })
623
665
  }
@@ -643,7 +685,7 @@ function loadComponentDescriptor (files, ref, version) {
643
685
  if (version === undefined) throw new Error(`${COMPONENT_DESC_FILENAME} is missing a version`)
644
686
  version = ''
645
687
  } else if (version === true) {
646
- version = ref.shortname.replace(/[/]/g, '-')
688
+ version = ref.shortname.replace(PATH_SEPARATOR_RX, '-')
647
689
  } else if (version.constructor === Object) {
648
690
  const refname = ref.shortname
649
691
  let matched
@@ -662,7 +704,7 @@ function loadComponentDescriptor (files, ref, version) {
662
704
  if (matched === '.' || matched === '..') {
663
705
  throw new Error(`version in ${COMPONENT_DESC_FILENAME} cannot have path segments: ${matched}`)
664
706
  }
665
- version = matched.replace(/[/]/g, '-')
707
+ version = matched.replace(PATH_SEPARATOR_RX, '-')
666
708
  } else if ((version = String(version)) === '.' || version === '..' || ~version.indexOf('/')) {
667
709
  throw new Error(`version in ${COMPONENT_DESC_FILENAME} cannot have path segments: ${version}`)
668
710
  }
@@ -670,22 +712,23 @@ function loadComponentDescriptor (files, ref, version) {
670
712
  return camelCaseKeys(data, { deep: true, stopPaths: ['asciidoc'] })
671
713
  }
672
714
 
673
- function computeOrigin (url, authStatus, ref, startPath, worktreePath = undefined, editUrl = true) {
715
+ function computeOrigin (url, authStatus, gitdir, ref, startPath, worktreePath = undefined, editUrl = true) {
674
716
  const { shortname: refname, oid: refhash, type: reftype } = ref
675
- const remote = !url.startsWith('file://')
676
- const origin = { type: 'git', refname, startPath }
717
+ const origin = { type: 'git', url, gitdir, refname, [reftype]: refname, startPath }
677
718
  if (authStatus) origin.private = authStatus
678
- origin[reftype] = refname
679
- if (worktreePath) {
680
- if (remote) origin.url = url
681
- origin.fileUriPattern =
682
- (posixify ? 'file:///' + posixify(worktreePath) : 'file://' + worktreePath) + path.join('/', startPath, '%s')
683
- origin.worktree = worktreePath
684
- } else {
685
- origin.url = url
719
+ if (worktreePath === undefined) {
686
720
  origin.refhash = refhash
721
+ } else {
722
+ if (worktreePath) {
723
+ origin.fileUriPattern =
724
+ (posixify ? 'file:///' + posixify(worktreePath) : 'file://' + worktreePath) + path.join('/', startPath, '%s')
725
+ } else {
726
+ origin.refhash = refhash
727
+ }
728
+ origin.worktree = worktreePath
729
+ if (url.startsWith('file://')) url = undefined
687
730
  }
688
- if (remote) origin.webUrl = url.replace(GIT_SUFFIX_RX, '')
731
+ if (url) origin.webUrl = url.replace(GIT_SUFFIX_RX, '')
689
732
  if (editUrl === true) {
690
733
  let match
691
734
  if (url && (match = url.match(HOSTED_GIT_REPO_RX))) {
@@ -728,9 +771,14 @@ function assignFileProperties (file, origin) {
728
771
  return file
729
772
  }
730
773
 
731
- function getFetchOptions (repo, progress, url, credentials, fetchTags, operation) {
732
- const opts = Object.assign({ depth: 1 }, credentials, repo)
733
- if (progress) opts.emitter = createProgressEmitter(progress, url, operation)
774
+ function buildFetchOptions (repo, progress, displayUrl, credentialsFromUrl, gitPlugins, fetchTags, operation) {
775
+ const { credentialManager, http, urlRouter } = gitPlugins
776
+ const onAuth = resolveCredentials.bind(credentialManager, new Map().set(undefined, credentialsFromUrl))
777
+ const onAuthFailure = onAuth
778
+ const onAuthSuccess = (url) => credentialManager.approved({ url })
779
+ const opts = Object.assign({ corsProxy: false, depth: 1, http, onAuth, onAuthFailure, onAuthSuccess }, repo)
780
+ if (urlRouter) opts.url = urlRouter.ensureGitSuffix(opts.url)
781
+ if (progress) opts.onProgress = createProgressListener(progress, displayUrl, operation)
734
782
  if (operation === 'fetch') {
735
783
  opts.prune = true
736
784
  if (fetchTags) opts.tags = opts.pruneTags = true
@@ -756,7 +804,7 @@ function createProgress (urls, term) {
756
804
  }
757
805
  }
758
806
 
759
- function createProgressEmitter (progress, progressLabel, operation) {
807
+ function createProgressListener (progress, progressLabel, operation) {
760
808
  const progressBar = progress.newBar(formatProgressBar(progressLabel, progress.maxLabelWidth, operation), {
761
809
  complete: '#',
762
810
  incomplete: '-',
@@ -767,10 +815,7 @@ function createProgressEmitter (progress, progressLabel, operation) {
767
815
  // NOTE leave room for indeterminate progress at end of bar; this isn't strictly needed for a bare clone
768
816
  progressBar.scaleFactor = Math.max(0, (ticks - 1) / ticks)
769
817
  progressBar.tick(0)
770
- return new EventEmitter()
771
- .on('progress', onGitProgress.bind(null, progressBar))
772
- .on('complete', onGitComplete.bind(null, progressBar))
773
- .on('error', onGitComplete.bind(null, progressBar))
818
+ return Object.assign(onGitProgress.bind(progressBar), { finish: onGitComplete.bind(progressBar) })
774
819
  }
775
820
 
776
821
  function formatProgressBar (label, maxLabelWidth, operation) {
@@ -785,29 +830,46 @@ function formatProgressBar (label, maxLabelWidth, operation) {
785
830
  return `[${operation}] ${label}${padding} [:bar]`
786
831
  }
787
832
 
788
- function onGitProgress (progressBar, { phase, loaded, total }) {
833
+ function onGitProgress ({ phase, loaded, total }) {
789
834
  const phaseIdx = GIT_PROGRESS_PHASES.indexOf(phase)
790
835
  if (~phaseIdx) {
791
- const scaleFactor = progressBar.scaleFactor
836
+ const scaleFactor = this.scaleFactor
792
837
  let ratio = ((loaded / total) * scaleFactor) / GIT_PROGRESS_PHASES.length
793
838
  if (phaseIdx) ratio += (phaseIdx * scaleFactor) / GIT_PROGRESS_PHASES.length
794
839
  // NOTE: updates are automatically throttled based on renderThrottle option
795
- progressBar.update(ratio > scaleFactor ? scaleFactor : ratio)
840
+ this.update(ratio > scaleFactor ? scaleFactor : ratio)
796
841
  }
797
842
  }
798
843
 
799
- function onGitComplete (progressBar, err) {
844
+ function onGitComplete (err) {
800
845
  if (err) {
801
846
  // TODO: could use progressBar.interrupt() to replace bar with message instead
802
- progressBar.chars.incomplete = '?'
803
- progressBar.update(0)
847
+ this.chars.incomplete = '?'
848
+ this.update(0)
804
849
  // NOTE: force progress bar to update regardless of throttle setting
805
- progressBar.render(undefined, true)
850
+ this.render(undefined, true)
806
851
  } else {
807
- progressBar.update(1)
852
+ this.update(1)
808
853
  }
809
854
  }
810
855
 
856
+ function resolveCredentials (credentialsFromUrlHolder, url, auth) {
857
+ const credentialsFromUrl = credentialsFromUrlHolder.get()
858
+ if ('Authorization' in auth.headers) {
859
+ if (!credentialsFromUrl) return this.rejected({ url, auth })
860
+ credentialsFromUrlHolder.clear()
861
+ } else if (credentialsFromUrl) {
862
+ return credentialsFromUrl
863
+ } else {
864
+ auth = undefined
865
+ }
866
+ return this.fill({ url }).then((credentials) =>
867
+ credentials
868
+ ? { username: credentials.token || credentials.username, password: credentials.token ? '' : credentials.password }
869
+ : this.rejected({ url, auth })
870
+ )
871
+ }
872
+
811
873
  /**
812
874
  * Generates a safe, unique folder name for a git URL.
813
875
  *
@@ -834,16 +896,20 @@ function generateCloneFolderName (url) {
834
896
  *
835
897
  * @param {Repository} repo - The repository on which to operate.
836
898
  * @param {String} remoteName - The name of the remote to resolve.
837
- * @returns {String} The URL of the specified remote, if present.
899
+ * @returns {String} The URL of the specified remote, if defined, or the file URI to the local repository.
838
900
  */
839
901
  function resolveRemoteUrl (repo, remoteName) {
840
- return git.config(Object.assign({ path: 'remote.' + remoteName + '.url' }, repo)).then((url) => {
841
- if (!url) return posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir
842
- if (url.startsWith('https://') || url.startsWith('http://')) {
843
- return ~url.indexOf('@') ? url.replace(URL_AUTH_CLEANER_RX, '$1') : url
844
- } else if (url.startsWith('git@')) {
845
- return 'https://' + url.substr(4).replace(':', '/')
902
+ return git.getConfig(Object.assign({ path: 'remote.' + remoteName + '.url' }, repo)).then((url) => {
903
+ if (url) {
904
+ if (url.startsWith('https://') || url.startsWith('http://')) {
905
+ return ~url.indexOf('@') ? url.replace(URL_AUTH_CLEANER_RX, '$1') : url
906
+ } else if (url.startsWith('git@')) {
907
+ return 'https://' + url.substr(4).replace(':', '/')
908
+ } else if (url.startsWith('ssh://')) {
909
+ return 'https://' + url.substr(url.indexOf('@') + 1 || 6).replace(URL_PORT_CLEANER_RX, '$1')
910
+ }
846
911
  }
912
+ return posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir
847
913
  })
848
914
  }
849
915
 
@@ -854,74 +920,30 @@ function resolveRemoteUrl (repo, remoteName) {
854
920
  * @return {Boolean} A flag indicating whether the URL matches a directory on the local filesystem.
855
921
  */
856
922
  function isDirectory (url) {
857
- return fsp
858
- .stat(url)
859
- .then((stat) => stat.isDirectory())
860
- .catch(invariably.false)
861
- }
862
-
863
- /**
864
- * Removes the specified directory (including all of its contents) or file.
865
- * Equivalent to fs.promises.rmdir(dir, { recursive: true }) in Node 12.
866
- */
867
- function rmdir (dir) {
868
- return fsp
869
- .readdir(dir, { withFileTypes: true })
870
- .then((lst) =>
871
- Promise.all(
872
- lst.map((it) =>
873
- it.isDirectory()
874
- ? rmdir(ospath.join(dir, it.name))
875
- : fsp.unlink(ospath.join(dir, it.name)).catch((unlinkErr) => {
876
- if (unlinkErr.code !== 'ENOENT') throw unlinkErr
877
- })
878
- )
879
- )
880
- )
881
- .then(() => fsp.rmdir(dir))
882
- .catch((err) => {
883
- if (err.code === 'ENOENT') return
884
- if (err.code === 'ENOTDIR') {
885
- return fsp.unlink(dir).catch((unlinkErr) => {
886
- if (unlinkErr.code !== 'ENOENT') throw unlinkErr
887
- })
888
- }
889
- throw err
890
- })
923
+ return fsp.stat(url).then((stat) => stat.isDirectory(), invariably.false)
891
924
  }
892
925
 
893
- function tagsSpecified (sources, defaultTags) {
894
- return ~sources.findIndex((source) => {
895
- const tags = source.tags || defaultTags || []
896
- return Array.isArray(tags) ? tags.length : true
897
- })
926
+ function tagsSpecified (sources) {
927
+ return sources.some(({ tags }) => tags && (Array.isArray(tags) ? tags.length : true))
898
928
  }
899
929
 
900
- function registerGitPlugins (credentials, network, startDir) {
901
- const plugins = git.cores.create(GIT_CORE)
902
- if (!plugins.has('fs')) plugins.set('fs', Object.assign({ _managed: true }, fs))
903
- let credentialManager
904
- if (plugins.has('credentialManager')) {
905
- credentialManager = plugins.get('credentialManager')
930
+ function loadGitPlugins (gitConfig, networkConfig, startDir) {
931
+ const plugins = new Map((git.cores || git.default.cores || new Map()).get(GIT_CORE))
932
+ for (const [name, request] of Object.entries(gitConfig.plugins || {})) {
933
+ if (request) plugins.set(name, userRequire(request, { dot: startDir, paths: [startDir, __dirname] }))
934
+ }
935
+ let credentialManager, urlRouter
936
+ if ((credentialManager = plugins.get('credentialManager'))) {
906
937
  if (typeof credentialManager.configure === 'function') {
907
- credentialManager.configure({ config: credentials, startDir })
908
- }
909
- if (typeof credentialManager.status !== 'function') {
910
- plugins.set('credentialManager', Object.assign({}, credentialManager, { status () {} }))
938
+ credentialManager.configure({ config: gitConfig.credentials, startDir })
911
939
  }
940
+ if (typeof credentialManager.status !== 'function') Object.assign(credentialManager, { status () {} })
912
941
  } else {
913
- credentialManager = new GitCredentialManagerStore().configure({ config: credentials, startDir })
914
- credentialManager._managed = true
915
- plugins.set('credentialManager', credentialManager)
916
- }
917
- if (!plugins.has('http') && (network.httpsProxy || network.httpProxy)) {
918
- plugins.set('http', Object.assign(require('./git-plugin-http')(network), { _managed: true }))
942
+ credentialManager = new GitCredentialManagerStore().configure({ config: gitConfig.credentials, startDir })
919
943
  }
920
- return plugins
921
- }
922
-
923
- function unregisterGitPlugins () {
924
- git.cores.create(GIT_CORE).forEach((val, key, map) => val._managed && map.delete(key))
944
+ if (gitConfig.ensureGitSuffix) urlRouter = { ensureGitSuffix: (url) => (url.endsWith('.git') ? url : url + '.git') }
945
+ const http = plugins.get('http') || createHttpPlugin(networkConfig, 'git/isomorphic-git@' + git.version())
946
+ return { credentialManager, http, urlRouter }
925
947
  }
926
948
 
927
949
  /**
@@ -938,47 +960,52 @@ function ensureCacheDir (preferredCacheDir, startDir) {
938
960
  const baseCacheDir =
939
961
  preferredCacheDir == null
940
962
  ? getCacheDir('antora' + (process.env.NODE_ENV === 'test' ? '-test' : '')) || ospath.resolve('.antora/cache')
941
- : expandPath(preferredCacheDir, '~+', startDir)
963
+ : expandPath(preferredCacheDir, { dot: startDir })
942
964
  const cacheDir = ospath.join(baseCacheDir, CONTENT_CACHE_FOLDER)
943
- return fsp
944
- .mkdir(cacheDir, { recursive: true })
945
- .then(() => cacheDir)
946
- .catch((err) => {
965
+ return fsp.mkdir(cacheDir, { recursive: true }).then(
966
+ () => cacheDir,
967
+ (err) => {
947
968
  throw Object.assign(err, { message: `Failed to create content cache directory: ${cacheDir}; ${err.message}` })
948
- })
969
+ }
970
+ )
949
971
  }
950
972
 
951
973
  function transformGitCloneError (err, displayUrl) {
952
- const { code, data, message, name, stack } = err
953
- let wrappedMsg
954
- let trimMessage
955
- if (code === git.E.HTTPError) {
956
- if (data.statusCode === 401) {
957
- wrappedMsg = err.rejected
958
- ? 'Content repository not found or credentials were rejected'
959
- : 'Content repository not found or requires credentials'
960
- } else if (data.statusCode === 404) {
961
- wrappedMsg = 'Content repository not found'
962
- } else {
963
- wrappedMsg = message
964
- trimMessage = true
974
+ let wrappedMsg, trimMessage
975
+ if (HTTP_ERROR_CODE_RX.test(err.code)) {
976
+ switch (err.data.statusCode) {
977
+ case 401:
978
+ wrappedMsg = err.rejected
979
+ ? 'Content repository not found or credentials were rejected'
980
+ : 'Content repository not found or requires credentials'
981
+ break
982
+ case 404:
983
+ wrappedMsg = 'Content repository not found'
984
+ break
985
+ default:
986
+ wrappedMsg = err.message
987
+ trimMessage = true
965
988
  }
966
- } else if (code === git.E.RemoteUrlParseError || code === git.E.UnknownTransportError) {
989
+ } else if (err instanceof UrlParseError || err instanceof UnknownTransportError) {
967
990
  wrappedMsg = 'Content source uses an unsupported transport protocol'
968
- } else if (code === 'ENOTFOUND') {
969
- wrappedMsg = 'Content repository host could not be resolved: ' + err.hostname
991
+ } else if (err.code === 'ENOTFOUND') {
992
+ wrappedMsg = `Content repository host could not be resolved: ${err.hostname}`
970
993
  } else {
971
- wrappedMsg = name + ': ' + message
994
+ wrappedMsg = `${err.name}: ${err.message}`
972
995
  trimMessage = true
973
996
  }
974
997
  if (trimMessage) {
975
998
  wrappedMsg = ~(wrappedMsg = wrappedMsg.trimRight()).indexOf('. ') ? wrappedMsg : wrappedMsg.replace(/\.$/, '')
976
999
  }
977
- const wrappedErr = new Error(wrappedMsg + ' (url: ' + displayUrl + ')')
978
- wrappedErr.stack += '\nCaused by: ' + (stack || 'unknown')
1000
+ const wrappedErr = new Error(`${wrappedMsg} (url: ${displayUrl})`)
1001
+ wrappedErr.stack += `\nCaused by: ${err.stack || 'unknown'}`
979
1002
  return wrappedErr
980
1003
  }
981
1004
 
1005
+ function splitRefPatterns (str) {
1006
+ return ~str.indexOf('{') ? str.split(VENTILATED_CSV_RX) : str.split(CSV_RX)
1007
+ }
1008
+
982
1009
  function coerceToString (value) {
983
1010
  return value == null ? '' : String(value)
984
1011
  }
@@ -991,11 +1018,11 @@ function findWorktrees (repo, patterns) {
991
1018
  if (!patterns.length) return new Map()
992
1019
  const linkedOnly = patterns[0] === '.' ? !(patterns = patterns.slice(1)) : true
993
1020
  let worktreesDir
1021
+ const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
994
1022
  return (patterns.length
995
1023
  ? fsp
996
1024
  .readdir((worktreesDir = ospath.join(repo.dir, '.git', 'worktrees')))
997
- .catch(invariably.emptyArray)
998
- .then((worktreeNames) => matcher(worktreeNames, [...patterns]))
1025
+ .then((worktreeNames) => filterRefs(worktreeNames, [...patterns], patternCache), invariably.emptyArray)
999
1026
  .then((worktreeNames) =>
1000
1027
  worktreeNames.length
1001
1028
  ? Promise.all(