@antora/content-aggregator 3.2.0-alpha.8 → 3.2.0-rc.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.
@@ -87,35 +87,41 @@ 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
- return ensureCacheDir(requestedCacheDir, startDir).then((cacheDir) => {
94
- const gitConfig = Object.assign({ ensureGitSuffix: true }, playbook.git)
95
- const gitPlugins = loadGitPlugins(gitConfig, playbook.network || {}, startDir)
96
- const concurrency = {
97
- fetch: Math.max(gitConfig.fetchConcurrency || Infinity, 1),
98
- read: Math.max(gitConfig.readConcurrency || Infinity, 1),
99
- }
100
- const sourcesByUrl = sources.reduce((accum, source) => {
101
- return accum.set(source.url, [...(accum.get(source.url) || []), Object.assign({}, sourceDefaults, source)])
102
- }, new Map())
103
- const progress = quiet ? undefined : createProgress(sourcesByUrl.keys(), process.stdout)
104
- const refPatternCache = Object.assign(new Map(), { braces: new Map() })
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()
109
- throw err
93
+ const READABLE_STREAM_DISABLED = process.env.READABLE_STREAM === 'disable'
94
+ if (!READABLE_STREAM_DISABLED) process.env.READABLE_STREAM = 'disable' // force use of Node.js stream APIs
95
+ return ensureCacheDir(requestedCacheDir, startDir)
96
+ .then((cacheDir) => {
97
+ const gitConfig = Object.assign({ ensureGitSuffix: true }, playbook.git)
98
+ const gitPlugins = loadGitPlugins(gitConfig, playbook.network || {}, startDir)
99
+ const concurrency = {
100
+ fetch: Math.max(gitConfig.fetchConcurrency || Infinity, 1),
101
+ read: Math.max(gitConfig.readConcurrency || Infinity, 1),
102
+ }
103
+ const sourcesByUrl = sources.reduce((accum, source) => {
104
+ return accum.set(source.url, [...(accum.get(source.url) || []), Object.assign({}, sourceDefaults, source)])
105
+ }, new Map())
106
+ const progress = quiet ? undefined : createProgress(sourcesByUrl.keys(), process.stdout)
107
+ const refPatternCache = Object.assign(new Map(), { braces: new Map() })
108
+ const fetchConfig = { always: fetch, depth: Math.max(0, gitConfig.fetchDepth ?? 1) }
109
+ const loadOpts = { cacheDir, fetch: fetchConfig, gitPlugins, progress, startDir, refPatternCache }
110
+ return collectFiles(sourcesByUrl, loadOpts, concurrency).then(buildAggregate, (err) => {
111
+ progress?.terminate()
112
+ throw err
113
+ })
110
114
  })
111
- })
115
+ .finally(() => READABLE_STREAM_DISABLED || delete process.env.READABLE_STREAM)
112
116
  }
113
117
 
114
118
  async function collectFiles (sourcesByUrl, loadOpts, concurrency, fetchedUrls = []) {
115
119
  const loadTasks = [...sourcesByUrl.entries()].map(([url, sources]) => {
116
120
  const loadOptsForUrl = Object.assign({}, loadOpts)
117
- if (loadOpts.fetch.always && fetchedUrls.length && fetchedUrls.includes(url)) loadOptsForUrl.fetch.always = false
121
+ if (loadOpts.fetch.always && fetchedUrls.length && ~fetchedUrls.indexOf(url)) loadOptsForUrl.fetch.always = false
118
122
  if (tagsSpecified(sources)) loadOptsForUrl.fetch.tags = true
123
+ const commits = commitsRequested(sources)
124
+ if (commits) loadOptsForUrl.fetch.commits = commits
119
125
  return loadRepository.bind(null, url, loadOptsForUrl, { url, sources })
120
126
  })
121
127
  return gracefulPromiseAllWithLimit(loadTasks, concurrency.fetch).then(([results, rejections]) => {
@@ -132,7 +138,7 @@ async function collectFiles (sourcesByUrl, loadOpts, concurrency, fetchedUrls =
132
138
  }
133
139
  return Promise.all(
134
140
  results.map(({ repo, authStatus, sources }) =>
135
- selectStartPathsForRepository(repo, authStatus, sources).then((startPaths) =>
141
+ selectStartPathsForRepository(repo, sources).then((startPaths) =>
136
142
  collectFilesFromStartPaths.bind(null, startPaths, repo, authStatus)
137
143
  )
138
144
  )
@@ -140,9 +146,10 @@ async function collectFiles (sourcesByUrl, loadOpts, concurrency, fetchedUrls =
140
146
  })
141
147
  }
142
148
 
143
- function buildAggregate (componentVersionBuckets) {
144
- const entries = Object.assign(new Map(), { accum: [] })
145
- for (const batchesForOrigin of componentVersionBuckets) {
149
+ function buildAggregate (componentVersionBatches) {
150
+ const contentAggregate = []
151
+ const entries = new Map()
152
+ for (const batchesForOrigin of componentVersionBatches) {
146
153
  for (const batch of batchesForOrigin) {
147
154
  let key, entry
148
155
  if ((entry = entries.get((key = batch.version + '@' + batch.name)))) {
@@ -151,11 +158,12 @@ function buildAggregate (componentVersionBuckets) {
151
158
  ;(batch.origins = entry.origins).push(origins[0])
152
159
  Object.assign(entry, batch)
153
160
  } else {
154
- entries.set(key, batch).accum.push(batch)
161
+ entries.set(key, batch)
162
+ contentAggregate.push(batch)
155
163
  }
156
164
  }
157
165
  }
158
- return entries.accum
166
+ return contentAggregate
159
167
  }
160
168
 
161
169
  async function loadRepository (url, opts, result = {}) {
@@ -177,6 +185,7 @@ async function loadRepository (url, opts, result = {}) {
177
185
  const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetch, 'fetch')
178
186
  await git
179
187
  .fetch(fetchOpts)
188
+ .then(() => ensureOids(fetchOpts))
180
189
  .then(() => {
181
190
  authStatus = identifyAuthStatus(credentialManager, credentials, url)
182
191
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
@@ -192,12 +201,13 @@ async function loadRepository (url, opts, result = {}) {
192
201
  authStatus = await git.getConfig(Object.assign({ path: 'remote.origin.private' }, repo))
193
202
  }
194
203
  } catch (gitErr) {
195
- await fsp['rm' in fsp ? 'rm' : 'rmdir'](dir, { recursive: true, force: true })
204
+ await fsp.rm(dir, { recursive: true, force: true })
196
205
  if (gitErr.rethrow) throw transformGitCloneError(gitErr, displayUrl)
197
206
  const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetch, 'clone')
198
207
  await git
199
208
  .clone(fetchOpts)
200
209
  .then(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo)))
210
+ .then(() => ensureOids(fetchOpts))
201
211
  .then(() => {
202
212
  authStatus = identifyAuthStatus(credentialManager, credentials, url)
203
213
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
@@ -216,6 +226,7 @@ async function loadRepository (url, opts, result = {}) {
216
226
  if (dotgitStat.isDirectory()) {
217
227
  repo = { cache, dir, fs, gitdir: dotgit }
218
228
  } else if (dotgitStat.isFile()) {
229
+ // NOTE isomorphic-git will discover the gitdir, but we must do it eagerly to process worktree patterns correctly
219
230
  repo = await resolveRepositoryFromWorktree({ cache, dir, fs, gitdir: dotgit })
220
231
  } else {
221
232
  repo = { cache, dir, fs, gitdir: dir, noCheckout: true }
@@ -245,33 +256,46 @@ function extractCredentials (url) {
245
256
  const credentials = username ? { username, password: password || '' } : {}
246
257
  return { displayUrl, url, credentials }
247
258
  }
248
- if (url.startsWith('git@')) return { displayUrl: url, url: 'https://' + url.substr(4).replace(':', '/') }
259
+ if (url.startsWith('git@')) return { displayUrl: url, url: 'https://' + url.substring(4).replace(':', '/') }
249
260
  return { displayUrl: url, url }
250
261
  }
251
262
 
252
- async function selectStartPathsForRepository (repo, authStatus, sources) {
263
+ async function selectStartPathsForRepository (repo, sources) {
253
264
  const startPaths = []
254
265
  const originUrls = {}
255
266
  for (const source of sources) {
256
267
  const { version, editUrl } = source
257
- // NOTE if repository is managed (has a url property), we can assume the remote name is origin
258
- // TODO if the repo has no remotes, then remoteName should be undefined
259
- const remoteName = repo.url ? 'origin' : source.remote || 'origin'
260
- const originUrl = repo.url || (originUrls[remoteName] ||= await resolveRemoteUrl(repo, remoteName))
268
+ let remoteName, originUrl
269
+ if (repo.url) {
270
+ remoteName = 'origin' // NOTE if repository is managed (has url property), we can assume remote name is origin
271
+ originUrl = repo.url
272
+ } else {
273
+ remoteName = source.remote || 'origin'
274
+ originUrl =
275
+ remoteName in originUrls
276
+ ? originUrls[remoteName]
277
+ : (originUrls[remoteName] = await resolveRemoteUrl(repo, remoteName))
278
+ if (!originUrl) {
279
+ remoteName = undefined
280
+ if ((originUrl = posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir).indexOf(' ')) {
281
+ originUrl = originUrl.replace(SPACE_RX, '%20')
282
+ }
283
+ }
284
+ }
261
285
  const refs = await selectReferences(source, repo, remoteName)
262
286
  if (refs.length) {
263
287
  for (const ref of refs) {
264
- for (const startPath of await selectStartPaths(source, repo, remoteName, ref)) {
288
+ for (const startPath of await selectStartPaths(source, repo, ref)) {
265
289
  startPaths.push({ startPath, ref, originUrl, editUrl, version })
266
290
  }
267
291
  }
268
292
  } else {
269
- const { url, branches, tags } = source
293
+ const { url, branches, tags, commits } = source
270
294
  const startPathInfo =
271
295
  'startPaths' in source
272
296
  ? { 'start paths': source.startPaths || undefined }
273
297
  : { 'start path': source.startPath || undefined }
274
- const sourceInfo = yaml.dump({ url, branches, tags, ...startPathInfo }, { flowLevel: 1 }).trimRight()
298
+ const sourceInfo = yaml.dump({ url, branches, tags, commits, ...startPathInfo }, { flowLevel: 1 }).trimRight()
275
299
  logger.info(`No matching references found for content source entry (${sourceInfo.replace(NEWLINE_RX, ' | ')})`)
276
300
  }
277
301
  }
@@ -280,11 +304,12 @@ async function selectStartPathsForRepository (repo, authStatus, sources) {
280
304
 
281
305
  // QUESTION should we resolve HEAD to a ref eagerly to avoid having to do a match on it?
282
306
  async function selectReferences (source, repo, remote) {
283
- let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns } = source
307
+ let { branches: branchPatterns, tags: tagPatterns, commits, worktrees: worktreePatterns } = source
284
308
  const managed = 'url' in repo
285
309
  const isBare = managed || repo.noCheckout
286
310
  const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
287
311
  const noWorktree = managed ? undefined : false
312
+ const isLinkedWorktree = repo.worktree?.name
288
313
  const refs = new Map()
289
314
  if (
290
315
  tagPatterns &&
@@ -295,11 +320,20 @@ async function selectReferences (source, repo, remote) {
295
320
  const tags = await git.listTags(repo)
296
321
  if (tags.length) {
297
322
  for (const shortname of filterRefs(tags, tagPatterns, patternCache)) {
298
- // NOTE tags are stored using symbol keys to distinguish them from branches
299
- refs.set(Symbol(shortname), { shortname, fullname: 'tags/' + shortname, type: 'tag', head: noWorktree })
323
+ // NOTE tags are stored using Buffer keys to distinguish them from commits and branches
324
+ refs.set(Buffer.from(shortname), { shortname, fullname: 'tags/' + shortname, type: 'tag', head: noWorktree })
300
325
  }
301
326
  }
302
327
  }
328
+ if (
329
+ commits &&
330
+ (commits = Array.isArray(commits) ? commits.map((commit) => String(commit)) : commits.split(CSV_RX)).length
331
+ ) {
332
+ for (const oid of commits) {
333
+ // NOTE commits are stored using Symbol keys to distinguish them from tags and branches
334
+ refs.set(Symbol(oid), { oid, shortname: oid, fullname: 'commits/' + oid, type: 'commit' })
335
+ }
336
+ }
303
337
  if (
304
338
  !branchPatterns ||
305
339
  !(branchPatterns = Array.isArray(branchPatterns)
@@ -308,38 +342,54 @@ async function selectReferences (source, repo, remote) {
308
342
  ) {
309
343
  return [...refs.values()]
310
344
  }
311
- const worktreeName = repo.worktreeName // possibly switch to worktree property ({ name, dir}) in future
312
- if (worktreeName) branchPatterns = branchPatterns.map((it) => (it === 'HEAD' ? 'HEAD@' + worktreeName : it))
313
- if (worktreePatterns) {
345
+ let useWorktree = false
346
+ if (!managed && (useWorktree = {})) {
314
347
  if (worktreePatterns === '.') {
315
- worktreePatterns = ['.']
348
+ isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
349
+ worktreePatterns = []
350
+ } else if (!worktreePatterns) {
351
+ worktreePatterns = []
316
352
  } else if (worktreePatterns === true) {
317
- worktreePatterns = ['.', '*']
353
+ if (!isBare) useWorktree.main = true
354
+ // NOTE if we don't start at a linked worktree, linked worktree cannot be current worktree
355
+ if (isLinkedWorktree) useWorktree.linked = isLinkedWorktree
356
+ worktreePatterns = ['*']
357
+ } else if (worktreePatterns === '/.') {
358
+ if (!isBare) useWorktree.main = true
359
+ worktreePatterns = []
318
360
  } else {
319
- worktreePatterns = Array.isArray(worktreePatterns)
320
- ? worktreePatterns.map((pattern) => String(pattern))
321
- : splitRefPatterns(String(worktreePatterns))
322
- if (worktreeName) worktreePatterns = worktreePatterns.map((it) => (it === '@' ? worktreeName : it))
361
+ worktreePatterns = (
362
+ Array.isArray(worktreePatterns)
363
+ ? worktreePatterns.map((pattern) => String(pattern))
364
+ : splitRefPatterns(String(worktreePatterns))
365
+ ).reduce((accum, it) => {
366
+ if (it === '/.') return (isBare || (useWorktree.main = true)) && accum
367
+ if (it === '.') {
368
+ isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
369
+ } else {
370
+ accum.push(it)
371
+ }
372
+ return accum
373
+ }, [])
323
374
  }
324
- } else {
325
- worktreePatterns = worktreePatterns === undefined ? [worktreeName || '.'] : []
375
+ if (!(useWorktree.main || useWorktree.linked)) useWorktree = false
326
376
  }
327
377
  let currentBranch
328
- if (branchPatterns.length === 1 && (branchPatterns[0] === 'HEAD' || branchPatterns[0] === '.')) {
329
- if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
330
- branchPatterns = [currentBranch]
331
- } else if (isBare) {
332
- return [...refs.values()]
333
- } else {
334
- // NOTE current branch is undefined when HEAD is detached
335
- const head = worktreePatterns[0] === '.' ? repo.dir : noWorktree
336
- refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
337
- return [...refs.values()]
338
- }
339
- } else {
378
+ if (!isLinkedWorktree) {
340
379
  let headBranchIdx
341
- // NOTE we can assume at least two entries if HEAD or . are present
342
- if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
380
+ if (branchPatterns.length === 1 && (branchPatterns[0] === 'HEAD' || branchPatterns[0] === '.')) {
381
+ if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
382
+ branchPatterns = [currentBranch]
383
+ } else if (isBare) {
384
+ return [...refs.values()]
385
+ } else {
386
+ // NOTE current branch is undefined when HEAD is detached
387
+ const head = useWorktree.main ? repo.dir : noWorktree
388
+ refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
389
+ return [...refs.values()]
390
+ }
391
+ } else if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
392
+ // NOTE we can assume at least two entries if HEAD or . are present
343
393
  if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
344
394
  if (~branchPatterns.indexOf(currentBranch)) {
345
395
  branchPatterns.splice(headBranchIdx, 1)
@@ -349,11 +399,7 @@ async function selectReferences (source, repo, remote) {
349
399
  } else if (isBare) {
350
400
  branchPatterns.splice(headBranchIdx, 1)
351
401
  } else {
352
- let head = noWorktree
353
- if (worktreePatterns[0] === '.') {
354
- worktreePatterns = worktreePatterns.slice(1)
355
- head = repo.dir
356
- }
402
+ const head = useWorktree.main ? repo.dir : noWorktree
357
403
  // NOTE current branch is undefined when HEAD is detached
358
404
  refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
359
405
  branchPatterns.splice(headBranchIdx, 1)
@@ -361,45 +407,54 @@ async function selectReferences (source, repo, remote) {
361
407
  }
362
408
  }
363
409
  // NOTE isomorphic-git includes HEAD in list of remote branches (see https://isomorphic-git.org/docs/listBranches)
364
- const remoteBranches = (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
410
+ const remoteBranches = remote
411
+ ? (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
412
+ : []
365
413
  if (remoteBranches.length) {
366
414
  for (const shortname of filterRefs(remoteBranches, branchPatterns, patternCache)) {
367
415
  const fullname = 'remotes/' + remote + '/' + shortname
368
416
  refs.set(shortname, { shortname, fullname, type: 'branch', remote, head: noWorktree })
369
417
  }
370
418
  }
371
- // NOTE only consider local branches if repo has a worktree or there are no remote tracking branches
372
- if (!isBare) {
419
+ if (!managed) {
373
420
  const localBranches = await git.listBranches(repo).then((branches) => {
374
- if (branches.length) return branches
375
- if (currentBranch == null) return getCurrentBranchName(repo).then((branch) => (branch ? [branch] : []))
376
- return currentBranch ? [currentBranch] : []
421
+ if (branches.length || isBare) return branches
422
+ if (currentBranch != null) return [currentBranch]
423
+ return getCurrentBranchName(repo).then((branch) => (branch ? [branch] : []))
377
424
  })
378
- if (localBranches.length) {
379
- const worktrees = await findWorktrees(repo, worktreePatterns)
380
- let onMatch
381
- if ((worktreePatterns.join('') || '.') !== '.') {
382
- const symbolicNames = new Map()
383
- worktrees.forEach(({ name, symbolicName = 'HEAD@' + name }, shortname) => {
384
- localBranches.push(symbolicName)
385
- symbolicNames.set(symbolicName, shortname)
386
- })
387
- onMatch = (candidate, { pattern }) => {
388
- const shortname = symbolicNames.get(candidate)
389
- return shortname ? (pattern.startsWith('HEAD@') ? shortname : undefined) : candidate
425
+ let onMatch, worktrees
426
+ if (
427
+ (useWorktree || worktreePatterns.length) &&
428
+ (worktrees = await findWorktrees(repo, worktreePatterns, useWorktree)).size
429
+ ) {
430
+ const headNames = new Map()
431
+ worktrees.forEach(({ name, symbolicNames }, shortname) => {
432
+ if (name) {
433
+ const headName = 'HEAD@' + name
434
+ localBranches.push(headName)
435
+ headNames.set(headName, shortname)
390
436
  }
391
- }
392
- for (const shortname of filterRefs(localBranches, branchPatterns, patternCache, onMatch)) {
393
- const head = (worktrees.get(shortname) || { head: noWorktree }).head
394
- refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
437
+ if (symbolicNames) {
438
+ for (const symbolicName of symbolicNames) {
439
+ const symbolicHeadName = symbolicName === 'HEAD' ? symbolicName : 'HEAD@' + symbolicName
440
+ localBranches.push(symbolicHeadName)
441
+ headNames.set(symbolicHeadName, shortname)
442
+ }
443
+ }
444
+ })
445
+ onMatch = (candidate, { pattern }) => {
446
+ const shortname = headNames.get(candidate)
447
+ if (!shortname) return candidate
448
+ if (pattern === 'HEAD' || pattern.startsWith('HEAD@')) return shortname
395
449
  }
396
450
  }
397
- } else if (!managed || !remoteBranches.length) {
398
- const localBranches = await git.listBranches(repo)
399
451
  if (localBranches.length) {
400
- for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
401
- if (refs.has(shortname)) continue // NOTE prefer remote branches in bare repository
402
- refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head: noWorktree })
452
+ const preferRemote = isBare && remoteBranches.length > 0
453
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache, onMatch)) {
454
+ if (preferRemote && refs.has(shortname)) continue
455
+ worktrees ??= await findWorktrees(repo, worktreePatterns, useWorktree)
456
+ const head = (worktrees.get(shortname) || { head: false }).head
457
+ refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
403
458
  }
404
459
  }
405
460
  }
@@ -419,14 +474,16 @@ function getCurrentBranchName (repo, remote) {
419
474
  ).then((ref) => (ref.startsWith('refs/') ? ref.replace(SHORTEN_REF_RX, '') : undefined))
420
475
  }
421
476
 
422
- async function selectStartPaths (source, repo, remoteName, ref) {
477
+ async function selectStartPaths (source, repo, ref) {
423
478
  const url = repo.url
424
479
  const displayUrl = url || repo.dir
425
480
  const worktreePath = ref.head
426
481
  if (!worktreePath) {
427
- ref.oid = await git.resolveRef(
428
- Object.assign(ref.detached ? { ref: 'HEAD', depth: 1 } : { ref: 'refs/' + ref.fullname }, repo)
429
- )
482
+ ref.oid = ref.oid
483
+ ? await git.expandOid(Object.assign({ oid: ref.oid }, repo)).catch(() => ref.oid)
484
+ : await git.resolveRef(
485
+ Object.assign(ref.detached ? { ref: 'HEAD', depth: 1 } : { ref: 'refs/' + ref.fullname }, repo)
486
+ )
430
487
  }
431
488
  if ('startPaths' in source) {
432
489
  let startPaths
@@ -461,7 +518,9 @@ function collectFilesFromStartPath (startPath, repo, authStatus, ref, originUrl,
461
518
  return (worktreePath ? readFilesFromWorktree(origin) : readFilesFromGitTree(repo, ref.oid, startPath))
462
519
  .then((files) => {
463
520
  const batch = deepClone((origin.descriptor = loadComponentDescriptor(files, ref, version)))
464
- if ('nav' in batch && Array.isArray(batch.nav)) batch.nav.origin = origin
521
+ if ('nav' in batch && Array.isArray(batch.nav)) {
522
+ Object.defineProperty(batch.nav, 'origin', { configurable: true, value: origin, writable: true })
523
+ }
465
524
  batch.files = files.map((file) => assignFileProperties(file, origin))
466
525
  batch.origins = [origin]
467
526
  return batch
@@ -648,10 +707,10 @@ function readGitSymlink (repo, root, parent, { oid, path: name }, following) {
648
707
  let target
649
708
  let targetParent = root
650
709
  if (dirname) {
651
- if (!(target = path.join('/', dirname, symlink).substr(1)) || target === dirname) {
710
+ if (!(target = path.join('/', dirname, symlink).substring(1)) || target === dirname) {
652
711
  target = '.'
653
712
  } else if (target.startsWith(dirname + '/')) {
654
- target = target.substr(dirname.length + 1) // join doesn't remove trailing separator
713
+ target = target.substring(dirname.length + 1) // join doesn't remove trailing separator
655
714
  targetParent = parent
656
715
  }
657
716
  } else {
@@ -734,19 +793,19 @@ function loadComponentDescriptor (files, ref, version) {
734
793
  if (!version) {
735
794
  if (version === undefined) throw new Error(`${COMPONENT_DESC_FILENAME} is missing a version`)
736
795
  if (version === false) throw new Error(`${COMPONENT_DESC_FILENAME} has an invalid version`)
737
- version = '' + (typeof version === 'number' ? version : '')
796
+ version = typeof version === 'number' ? '' + version : ''
738
797
  } else if (version === true) {
739
798
  version = ref.shortname.replace(PATH_SEPARATOR_RX, '-')
740
799
  } else if (version.constructor === Object) {
741
800
  const refname = ref.shortname
742
801
  let matched
743
802
  if (refname in version) {
744
- matched = version[refname]
803
+ matched = '' + (version[refname] ?? '')
745
804
  } else if (
746
805
  !Object.entries(version).some(([pattern, replacement]) => {
747
- const result = refname.replace(makeMatcherRx(pattern, VERSION_MATCHER_OPTS), '\0' + replacement)
748
- if (result === refname) return false
749
- matched = result.substr(1)
806
+ const result = refname.replace(makeMatcherRx(pattern, VERSION_MATCHER_OPTS), '\0' + (replacement ?? ''))
807
+ if (result === refname) return false // no match
808
+ matched = result.substring(1)
750
809
  return true
751
810
  })
752
811
  ) {
@@ -793,6 +852,7 @@ function buildFetchOptions (repo, progress, displayUrl, credentialsFromUrl, gitP
793
852
  } else if (!fetch.tags) {
794
853
  opts.noTags = true
795
854
  }
855
+ if (fetch.commits) opts.oids = fetch.commits
796
856
  return opts
797
857
  }
798
858
 
@@ -827,14 +887,17 @@ function createProgressListener (progress, progressLabel, operation) {
827
887
  // NOTE leave room for indeterminate progress at end of bar; this isn't strictly needed for a bare clone
828
888
  progressBar.scaleFactor = Math.max(0, (ticks - 1) / ticks)
829
889
  progressBar.tick(0)
830
- return Object.assign(onGitProgress.bind(progressBar), { finish: onGitComplete.bind(progressBar) })
890
+ return Object.assign(onGitProgress.bind(progressBar), {
891
+ finish: onGitComplete.bind(progressBar),
892
+ reset: () => progressBar.update(0),
893
+ })
831
894
  }
832
895
 
833
896
  function formatProgressBar (label, maxLabelWidth, operation) {
834
897
  const paddingSize = maxLabelWidth - label.length
835
898
  let padding = ''
836
899
  if (paddingSize < 0) {
837
- label = '...' + label.substr(-paddingSize + 3)
900
+ label = '...' + label.substring(-paddingSize + 3)
838
901
  } else if (paddingSize) {
839
902
  padding = ' '.repeat(paddingSize)
840
903
  }
@@ -914,21 +977,18 @@ function generateCloneFolderName (url) {
914
977
  *
915
978
  * @param {Repository} repo - The repository on which to operate.
916
979
  * @param {String} remoteName - The name of the remote to resolve.
917
- * @returns {String} The URL of the specified remote, if defined, or the file URI to the local repository.
980
+ * @returns {String} The URL of the specified remote, if defined
918
981
  */
919
982
  function resolveRemoteUrl (repo, remoteName) {
920
983
  return git.getConfig(Object.assign({ path: 'remote.' + remoteName + '.url' }, repo)).then((url) => {
921
- if (url) {
922
- if (url.startsWith('https://') || url.startsWith('http://')) {
923
- return ~url.indexOf('@') ? url.replace(URL_AUTH_CLEANER_RX, '$1') : url
924
- }
925
- if (url.startsWith('git@')) return 'https://' + url.substr(4).replace(':', '/')
926
- if (url.startsWith('ssh://')) {
927
- return 'https://' + url.substr(url.indexOf('@') + 1 || 6).replace(URL_PORT_CLEANER_RX, '$1')
928
- }
984
+ if (!url) return
985
+ if (url.startsWith('https://') || url.startsWith('http://')) {
986
+ return ~url.indexOf('@') ? url.replace(URL_AUTH_CLEANER_RX, '$1') : url
987
+ }
988
+ if (url.startsWith('git@')) return 'https://' + url.substring(4).replace(':', '/')
989
+ if (url.startsWith('ssh://')) {
990
+ return 'https://' + url.substring(url.indexOf('@') + 1 || 6).replace(URL_PORT_CLEANER_RX, '$1')
929
991
  }
930
- url = posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir
931
- return ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url
932
992
  })
933
993
  }
934
994
 
@@ -946,6 +1006,16 @@ function tagsSpecified (sources) {
946
1006
  return sources.some(({ tags }) => tags && (Array.isArray(tags) ? tags.length : true))
947
1007
  }
948
1008
 
1009
+ function commitsRequested (sources) {
1010
+ if (!sources.some(({ commits }) => commits && (Array.isArray(commits) ? commits.length : true))) return
1011
+ const result = new Set()
1012
+ for (const { commits } of sources) {
1013
+ if (!commits) continue
1014
+ for (const commit of Array.isArray(commits) ? commits : commits.split(CSV_RX)) result.add(String(commit))
1015
+ }
1016
+ return [...result]
1017
+ }
1018
+
949
1019
  function loadGitPlugins (gitConfig, networkConfig, startDir) {
950
1020
  const plugins = new Map((git.cores || git.default.cores || new Map()).get(GIT_CORE))
951
1021
  for (const [name, request] of Object.entries(gitConfig.plugins || {})) {
@@ -1023,7 +1093,7 @@ function transformGitCloneError (err, displayUrl, authRequested) {
1023
1093
  }
1024
1094
 
1025
1095
  function splitRefPatterns (str) {
1026
- return ~str.indexOf('{') ? str.split(VENTILATED_CSV_RX) : str.split(CSV_RX)
1096
+ return str.split(~str.indexOf('{') ? VENTILATED_CSV_RX : CSV_RX)
1027
1097
  }
1028
1098
 
1029
1099
  function camelCaseKeys (o, stopPaths = [], p = '') {
@@ -1049,47 +1119,57 @@ function coerceToString (value) {
1049
1119
  function resolveRepositoryFromWorktree (repo) {
1050
1120
  return fsp
1051
1121
  .readFile(repo.gitdir, 'utf8')
1052
- .then((contents) => contents.substr(8).trimEnd())
1122
+ .then((contents) => contents.substring(8).trimEnd())
1053
1123
  .then((worktreeGitdir) =>
1054
1124
  fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8').then(
1055
1125
  (contents) => {
1056
1126
  const gitdir = ospath.join(worktreeGitdir, contents.trimEnd())
1057
1127
  const dir = ospath.basename(gitdir) === '.git' ? ospath.dirname(gitdir) : gitdir
1058
- return Object.assign(repo, { dir, gitdir, worktreeName: ospath.basename(worktreeGitdir) })
1128
+ const name = ospath.basename(worktreeGitdir)
1129
+ return Object.assign(repo, { dir, gitdir, worktree: { gitdir: worktreeGitdir, name } })
1059
1130
  },
1060
1131
  () => repo
1061
1132
  )
1062
1133
  )
1063
1134
  }
1064
1135
 
1065
- function findWorktrees (repo, patterns) {
1066
- if (!patterns.length) return new Map()
1067
- const mainWorktree =
1068
- patterns[0] === '.' && (patterns = patterns.slice(1))
1069
- ? getCurrentBranchName(repo).then((branch) => branch && [branch, { head: repo.dir, name: '.' }])
1070
- : Promise.resolve()
1071
- const worktreesDir = patterns.length ? ospath.join(repo.dir, '.git', 'worktrees') : undefined
1136
+ function findWorktrees (repo, patterns, useWorktree) {
1137
+ const useLinkedWorktree = !!useWorktree.linked
1138
+ const mainWorktree = useWorktree.main
1139
+ ? getCurrentBranchName(repo).then((branch) => {
1140
+ if (!branch) return
1141
+ return [branch, { head: repo.dir, name: undefined, symbolicNames: useLinkedWorktree ? ['/.'] : ['/.', '.'] }]
1142
+ })
1143
+ : Promise.resolve()
1144
+ if (!(useLinkedWorktree || patterns.length)) return mainWorktree.then((entry) => new Map(entry && [entry]))
1145
+ const worktreesDir = ospath.join(repo.dir, repo.dir === repo.gitdir ? '' : '.git', 'worktrees')
1072
1146
  const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
1073
- return (
1074
- worktreesDir
1075
- ? fsp
1076
- .readdir(worktreesDir)
1077
- .then((worktreeNames) => filterRefs(worktreeNames, patterns, patternCache), invariably.emptyArray)
1078
- .then((worktreeNames) =>
1079
- Promise.all(
1080
- worktreeNames.map((worktreeName) => {
1081
- const gitdir = ospath.resolve(worktreesDir, worktreeName)
1082
- // NOTE branch name defaults to worktree name if HEAD is detached
1083
- return getCurrentBranchName(Object.assign({}, repo, { gitdir })).then((branch = worktreeName) =>
1084
- fsp
1085
- .readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
1086
- .then((contents) => [branch, { head: ospath.dirname(contents.trimEnd()), name: worktreeName }])
1087
- )
1088
- })
1089
- )
1147
+ const scanWorktrees = patterns.length
1148
+ ? fsp
1149
+ .readdir(worktreesDir)
1150
+ .then((worktreeNames) => filterRefs(worktreeNames, patterns, patternCache), invariably.emptyArray)
1151
+ .then((worktreeNames) => {
1152
+ if (useLinkedWorktree && !~worktreeNames.indexOf(useWorktree.linked)) worktreeNames.push(useWorktree.linked)
1153
+ return worktreeNames
1154
+ })
1155
+ : Promise.resolve(useLinkedWorktree ? [useWorktree.linked] : [])
1156
+ return scanWorktrees
1157
+ .then((worktreeNames) =>
1158
+ Promise.all(
1159
+ worktreeNames.map((name) => {
1160
+ const symbolicNames = useLinkedWorktree && name === useWorktree.linked ? ['.', 'HEAD'] : undefined
1161
+ const gitdir = ospath.resolve(worktreesDir, name)
1162
+ // NOTE branch name defaults to worktree name if HEAD is detached
1163
+ return getCurrentBranchName(Object.assign({}, repo, { gitdir })).then((branch = name) =>
1164
+ fsp
1165
+ .readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
1166
+ .then((contents) => [branch, { head: ospath.dirname(contents.trimEnd()), name, symbolicNames }])
1090
1167
  )
1091
- : Promise.resolve()
1092
- ).then((entries = []) => mainWorktree.then((entry) => new Map(entry ? entries.push(entry) && entries : entries)))
1168
+ })
1169
+ )
1170
+ )
1171
+ .then((entries) => new Map(entries))
1172
+ .then((worktrees) => mainWorktree.then((result) => (result ? worktrees.set(result[0], result[1]) : worktrees)))
1093
1173
  }
1094
1174
 
1095
1175
  async function gracefulPromiseAllWithLimit (tasks, limit = Infinity) {
@@ -1125,4 +1205,27 @@ async function promiseAllWithLimit (tasks, limit = Infinity) {
1125
1205
  return Promise.all(started)
1126
1206
  }
1127
1207
 
1208
+ async function ensureOids (opts) {
1209
+ if (!opts.oids) return
1210
+ let prevShallowCommits = await getShallowCommits(opts)
1211
+ if (prevShallowCommits == null) return
1212
+ let oids = opts.oids.slice()
1213
+ const deepenOpts = Object.assign({}, opts, { relative: true })
1214
+ const format = 'deflated'
1215
+ while (oids.length) {
1216
+ deepenOpts.onProgress?.reset()
1217
+ await git.fetch(deepenOpts)
1218
+ const shallowCommits = await getShallowCommits(opts)
1219
+ if (shallowCommits == null || shallowCommits === prevShallowCommits) break
1220
+ prevShallowCommits = shallowCommits
1221
+ oids = await Promise.all(
1222
+ oids.map((oid) => git.readObject(Object.assign({ oid, format }, opts)).then(invariably.void, () => oid))
1223
+ ).then((results) => results.filter((it) => it))
1224
+ }
1225
+ }
1226
+
1227
+ function getShallowCommits ({ gitdir }) {
1228
+ return fsp.readFile(ospath.join(gitdir, 'shallow'), 'utf8').catch(invariably.void)
1229
+ }
1230
+
1128
1231
  module.exports = aggregateContent
@@ -42,7 +42,7 @@ function computeOrigin (url, authStatus, gitdir, ref, startPath, worktreePath =
42
42
  } else if (editUrl) {
43
43
  const vars = {
44
44
  path: () => (startPath ? path.join(startPath, '%s') : '%s'),
45
- ref: () => 'refs/' + (reftype === 'branch' ? 'heads' : reftype) + '/' + refname,
45
+ ref: () => 'refs/' + (reftype === 'branch' ? 'head' : reftype) + 's/' + refname,
46
46
  refhash: () => refhash,
47
47
  reftype: () => reftype,
48
48
  refname: () => refname,
@@ -6,7 +6,7 @@ function compileRx (pattern, opts) {
6
6
  if (pattern === '*' || pattern === '**') return MATCH_ALL_RX
7
7
  const rx =
8
8
  pattern.charAt() === '!' // we handle negate ourselves
9
- ? Object.defineProperty(makeMatcherRx((pattern = pattern.substr(1)), opts), 'negated', { value: true })
9
+ ? Object.defineProperty(makeMatcherRx((pattern = pattern.substring(1)), opts), 'negated', { value: true })
10
10
  : makeMatcherRx(pattern, opts)
11
11
  return Object.defineProperty(rx, 'pattern', { value: pattern })
12
12
  }
@@ -39,6 +39,7 @@ function createMatcher (patterns, cache = Object.assign(new Map(), { braces: new
39
39
  }
40
40
 
41
41
  function filterRefs (candidates, patterns, cache, onMatch) {
42
+ if (!(patterns = patterns.filter(compact)).length) return []
42
43
  const match = createMatcher(patterns, cache)
43
44
  return candidates.reduce((accum, candidate) => {
44
45
  if ((candidate = match(candidate, onMatch))) accum.push(candidate)
@@ -46,4 +47,8 @@ function filterRefs (candidates, patterns, cache, onMatch) {
46
47
  }, [])
47
48
  }
48
49
 
50
+ function compact (str) {
51
+ return str !== ''
52
+ }
53
+
49
54
  module.exports = filterRefs
@@ -14,7 +14,7 @@ function resolvePathGlobs (base, patterns, listDirents, retrievePath, tree = { p
14
14
  if (pattern.charAt() === '!') {
15
15
  return paths.then((resolvedPaths) => {
16
16
  if (resolvedPaths.length) {
17
- const rx = makeMatcherRx(pattern.substr(1), MATCHER_OPTS)
17
+ const rx = makeMatcherRx(pattern.substring(1), MATCHER_OPTS)
18
18
  return resolvedPaths.filter((it) => !rx.test(it))
19
19
  }
20
20
  return resolvedPaths
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-aggregator",
3
- "version": "3.2.0-alpha.8",
3
+ "version": "3.2.0-rc.1",
4
4
  "description": "Fetches and aggregates content from distributed sources for use in an Antora documentation pipeline.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -33,14 +33,14 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@antora/expand-path-helper": "~3.0",
36
- "@antora/logger": "3.2.0-alpha.8",
36
+ "@antora/logger": "3.2.0-rc.1",
37
37
  "@antora/user-require-helper": "~3.0",
38
38
  "braces": "~3.0",
39
39
  "cache-directory": "~2.0",
40
40
  "fast-glob": "~3.3",
41
41
  "hpagent": "~1.2",
42
- "isomorphic-git": "~1.25",
43
- "js-yaml": "~4.1",
42
+ "isomorphic-git": "~1.38",
43
+ "js-yaml": "~4.2",
44
44
  "multi-progress": "~4.0",
45
45
  "picomatch": "~4.0",
46
46
  "progress": "~2.0",
@@ -49,7 +49,7 @@
49
49
  "vinyl": "~3.0"
50
50
  },
51
51
  "engines": {
52
- "node": ">=18.0.0"
52
+ "node": ">=20.0.0"
53
53
  },
54
54
  "files": [
55
55
  "lib/"
@@ -65,7 +65,7 @@
65
65
  "web publishing"
66
66
  ],
67
67
  "scripts": {
68
- "test": "_mocha",
68
+ "test": "node --test",
69
69
  "prepublishOnly": "npx -y downdoc@latest --prepublish",
70
70
  "postpublish": "npx -y downdoc@latest --postpublish"
71
71
  }