@antora/content-aggregator 3.2.0-alpha.1 → 3.2.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/aggregate-content.js +449 -318
- package/lib/compute-origin.js +6 -5
- package/lib/constants.js +2 -2
- package/lib/decode-uint8-array.js +1 -1
- package/lib/filter-refs.js +8 -4
- package/lib/git-credential-manager-store.js +3 -3
- package/lib/git-plugin-http.js +5 -4
- package/lib/git.js +8 -1
- package/lib/matcher.js +1 -6
- package/lib/posixify.js +1 -1
- package/lib/resolve-path-globs.js +25 -24
- package/package.json +17 -13
package/lib/aggregate-content.js
CHANGED
|
@@ -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
|
|
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
|
|
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,132 @@ 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
|
|
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 =
|
|
103
|
+
const progress = quiet ? undefined : createProgress(sourcesByUrl.keys(), process.stdout)
|
|
101
104
|
const refPatternCache = Object.assign(new Map(), { braces: new Map() })
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
const commits = commitsRequested(sources)
|
|
120
|
+
if (commits) loadOptsForUrl.fetch.commits = commits
|
|
121
|
+
return loadRepository.bind(null, url, loadOptsForUrl, { url, sources })
|
|
122
|
+
})
|
|
123
|
+
return gracefulPromiseAllWithLimit(loadTasks, concurrency.fetch).then(([results, rejections]) => {
|
|
124
|
+
if (rejections.length) {
|
|
125
|
+
if (concurrency.fetch > 1 && results.length > 1 && rejections.every(({ recoverable }) => recoverable)) {
|
|
126
|
+
if (loadOpts.progress) loadOpts.progress.terminate() // reset cursor position and allow it be reused
|
|
127
|
+
const msg0 = 'An unexpected error occurred while fetching content sources concurrently.'
|
|
128
|
+
const msg1 = 'Retrying with git.fetch_concurrency value of 1.'
|
|
129
|
+
logger.warn(rejections[0], msg0 + ' ' + msg1)
|
|
130
|
+
const fulfilledUrls = results.filter((it) => it?.repo.url).map((it) => it.url)
|
|
131
|
+
return collectFiles(sourcesByUrl, loadOpts, Object.assign(concurrency, { fetch: 1 }), fulfilledUrls)
|
|
132
|
+
}
|
|
133
|
+
throw rejections[0]
|
|
143
134
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
135
|
+
return Promise.all(
|
|
136
|
+
results.map(({ repo, authStatus, sources }) =>
|
|
137
|
+
selectStartPathsForRepository(repo, sources).then((startPaths) =>
|
|
138
|
+
collectFilesFromStartPaths.bind(null, startPaths, repo, authStatus)
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
).then((collectTasks) => promiseAllWithLimit(collectTasks, concurrency.read))
|
|
142
|
+
})
|
|
153
143
|
}
|
|
154
144
|
|
|
155
145
|
function buildAggregate (componentVersionBuckets) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!entry) return accum.set(key, batch)
|
|
146
|
+
const entries = Object.assign(new Map(), { accum: [] })
|
|
147
|
+
for (const batchesForOrigin of componentVersionBuckets) {
|
|
148
|
+
for (const batch of batchesForOrigin) {
|
|
149
|
+
let key, entry
|
|
150
|
+
if ((entry = entries.get((key = batch.version + '@' + batch.name)))) {
|
|
162
151
|
const { files, origins } = batch
|
|
163
152
|
;(batch.files = entry.files).push(...files)
|
|
164
153
|
;(batch.origins = entry.origins).push(origins[0])
|
|
165
154
|
Object.assign(entry, batch)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
155
|
+
} else {
|
|
156
|
+
entries.set(key, batch).accum.push(batch)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return entries.accum
|
|
170
161
|
}
|
|
171
162
|
|
|
172
|
-
async function loadRepository (url, opts) {
|
|
163
|
+
async function loadRepository (url, opts, result = {}) {
|
|
173
164
|
let authStatus, dir, repo
|
|
174
165
|
const cache = { [REF_PATTERN_CACHE_KEY]: opts.refPatternCache }
|
|
175
166
|
if (~url.indexOf(':') && GIT_URI_DETECTOR_RX.test(url)) {
|
|
176
167
|
let credentials, displayUrl
|
|
177
168
|
;({ displayUrl, url, credentials } = extractCredentials(url))
|
|
178
|
-
const { cacheDir, fetch,
|
|
169
|
+
const { cacheDir, fetch, gitPlugins, progress } = opts
|
|
179
170
|
dir = ospath.join(cacheDir, generateCloneFolderName(displayUrl))
|
|
180
171
|
// NOTE the presence of the url property on the repo object implies the repository is remote
|
|
181
172
|
repo = { cache, dir, fs, gitdir: dir, noCheckout: true, url }
|
|
173
|
+
const { credentialManager } = gitPlugins
|
|
182
174
|
const validStateFile = ospath.join(dir, VALID_STATE_FILENAME)
|
|
183
175
|
try {
|
|
184
176
|
await fsp.access(validStateFile)
|
|
185
|
-
if (fetch) {
|
|
177
|
+
if (fetch.always) {
|
|
186
178
|
await fsp.unlink(validStateFile)
|
|
187
|
-
const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins,
|
|
179
|
+
const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetch, 'fetch')
|
|
188
180
|
await git
|
|
189
181
|
.fetch(fetchOpts)
|
|
182
|
+
.then(() => ensureOids(fetchOpts))
|
|
190
183
|
.then(() => {
|
|
191
|
-
authStatus = identifyAuthStatus(
|
|
184
|
+
authStatus = identifyAuthStatus(credentialManager, credentials, url)
|
|
192
185
|
return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
|
|
193
186
|
})
|
|
194
187
|
.catch((fetchErr) => {
|
|
195
|
-
|
|
188
|
+
fetchOpts.onProgress?.finish(fetchErr)
|
|
196
189
|
if (HTTP_ERROR_CODE_RX.test(fetchErr.code) && fetchErr.data.statusCode === 401) fetchErr.rethrow = true
|
|
197
190
|
throw fetchErr
|
|
198
191
|
})
|
|
199
192
|
.then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
|
|
200
|
-
.then(() => fetchOpts.onProgress
|
|
193
|
+
.then(() => fetchOpts.onProgress?.finish())
|
|
201
194
|
} else {
|
|
202
195
|
authStatus = await git.getConfig(Object.assign({ path: 'remote.origin.private' }, repo))
|
|
203
196
|
}
|
|
204
197
|
} catch (gitErr) {
|
|
205
198
|
await fsp['rm' in fsp ? 'rm' : 'rmdir'](dir, { recursive: true, force: true })
|
|
206
199
|
if (gitErr.rethrow) throw transformGitCloneError(gitErr, displayUrl)
|
|
207
|
-
const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins,
|
|
200
|
+
const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetch, 'clone')
|
|
208
201
|
await git
|
|
209
202
|
.clone(fetchOpts)
|
|
210
203
|
.then(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo)))
|
|
204
|
+
.then(() => ensureOids(fetchOpts))
|
|
211
205
|
.then(() => {
|
|
212
|
-
authStatus = identifyAuthStatus(
|
|
206
|
+
authStatus = identifyAuthStatus(credentialManager, credentials, url)
|
|
213
207
|
return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
|
|
214
208
|
})
|
|
215
209
|
.catch((cloneErr) => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
throw transformGitCloneError(cloneErr, displayUrl)
|
|
210
|
+
fetchOpts.onProgress?.finish(cloneErr)
|
|
211
|
+
const authRequested = credentialManager.status({ url }) === 'requested'
|
|
212
|
+
throw transformGitCloneError(cloneErr, displayUrl, authRequested)
|
|
219
213
|
})
|
|
220
214
|
.then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
|
|
221
|
-
.then(() => fetchOpts.onProgress
|
|
215
|
+
.then(() => fetchOpts.onProgress?.finish())
|
|
222
216
|
}
|
|
223
217
|
} else if (await isDirectory((dir = expandPath(url, { dot: opts.startDir })))) {
|
|
224
218
|
const dotgit = ospath.join(dir, '.git')
|
|
@@ -239,7 +233,7 @@ async function loadRepository (url, opts) {
|
|
|
239
233
|
} else {
|
|
240
234
|
throw new Error(`Local content source does not exist: ${dir}${url !== dir ? ' (url: ' + url + ')' : ''}`)
|
|
241
235
|
}
|
|
242
|
-
return { repo, authStatus }
|
|
236
|
+
return Object.assign(result, { repo, authStatus })
|
|
243
237
|
}
|
|
244
238
|
|
|
245
239
|
function extractCredentials (url) {
|
|
@@ -254,34 +248,61 @@ function extractCredentials (url) {
|
|
|
254
248
|
// NOTE if only username is present, assume it's an oauth token and set password to empty string
|
|
255
249
|
const credentials = username ? { username, password: password || '' } : {}
|
|
256
250
|
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
251
|
}
|
|
252
|
+
if (url.startsWith('git@')) return { displayUrl: url, url: 'https://' + url.substr(4).replace(':', '/') }
|
|
253
|
+
return { displayUrl: url, url }
|
|
262
254
|
}
|
|
263
255
|
|
|
264
|
-
async function
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
async function selectStartPathsForRepository (repo, sources) {
|
|
257
|
+
const startPaths = []
|
|
258
|
+
const originUrls = {}
|
|
259
|
+
for (const source of sources) {
|
|
260
|
+
const { version, editUrl } = source
|
|
261
|
+
let remoteName, originUrl
|
|
262
|
+
if (repo.url) {
|
|
263
|
+
remoteName = 'origin' // NOTE if repository is managed (has url property), we can assume remote name is origin
|
|
264
|
+
originUrl = repo.url
|
|
265
|
+
} else {
|
|
266
|
+
remoteName = source.remote || 'origin'
|
|
267
|
+
originUrl =
|
|
268
|
+
remoteName in originUrls
|
|
269
|
+
? originUrls[remoteName]
|
|
270
|
+
: (originUrls[remoteName] = await resolveRemoteUrl(repo, remoteName))
|
|
271
|
+
if (!originUrl) {
|
|
272
|
+
remoteName = undefined
|
|
273
|
+
if ((originUrl = posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir).indexOf(' ')) {
|
|
274
|
+
originUrl = originUrl.replace(SPACE_RX, '%20')
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const refs = await selectReferences(source, repo, remoteName)
|
|
279
|
+
if (refs.length) {
|
|
280
|
+
for (const ref of refs) {
|
|
281
|
+
for (const startPath of await selectStartPaths(source, repo, ref)) {
|
|
282
|
+
startPaths.push({ startPath, ref, originUrl, editUrl, version })
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
const { url, branches, tags, commits } = source
|
|
269
287
|
const startPathInfo =
|
|
270
|
-
'startPaths' in source
|
|
271
|
-
|
|
288
|
+
'startPaths' in source
|
|
289
|
+
? { 'start paths': source.startPaths || undefined }
|
|
290
|
+
: { 'start path': source.startPath || undefined }
|
|
291
|
+
const sourceInfo = yaml.dump({ url, branches, tags, commits, ...startPathInfo }, { flowLevel: 1 }).trimRight()
|
|
272
292
|
logger.info(`No matching references found for content source entry (${sourceInfo.replace(NEWLINE_RX, ' | ')})`)
|
|
273
|
-
return []
|
|
274
293
|
}
|
|
275
|
-
|
|
276
|
-
|
|
294
|
+
}
|
|
295
|
+
return startPaths
|
|
277
296
|
}
|
|
278
297
|
|
|
279
298
|
// QUESTION should we resolve HEAD to a ref eagerly to avoid having to do a match on it?
|
|
280
299
|
async function selectReferences (source, repo, remote) {
|
|
281
|
-
let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns } = source
|
|
282
|
-
const
|
|
300
|
+
let { branches: branchPatterns, tags: tagPatterns, commits, worktrees: worktreePatterns } = source
|
|
301
|
+
const managed = 'url' in repo
|
|
302
|
+
const isBare = managed || repo.noCheckout
|
|
283
303
|
const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
|
|
284
|
-
const noWorktree =
|
|
304
|
+
const noWorktree = managed ? undefined : false
|
|
305
|
+
const isLinkedWorktree = repo.worktree?.name
|
|
285
306
|
const refs = new Map()
|
|
286
307
|
if (
|
|
287
308
|
tagPatterns &&
|
|
@@ -292,51 +313,77 @@ async function selectReferences (source, repo, remote) {
|
|
|
292
313
|
const tags = await git.listTags(repo)
|
|
293
314
|
if (tags.length) {
|
|
294
315
|
for (const shortname of filterRefs(tags, tagPatterns, patternCache)) {
|
|
295
|
-
// NOTE tags are stored using
|
|
296
|
-
refs.set(
|
|
316
|
+
// NOTE tags are stored using Buffer keys to distinguish them from commits and branches
|
|
317
|
+
refs.set(Buffer.from(shortname), { shortname, fullname: 'tags/' + shortname, type: 'tag', head: noWorktree })
|
|
297
318
|
}
|
|
298
319
|
}
|
|
299
320
|
}
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
?
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
321
|
+
if (
|
|
322
|
+
commits &&
|
|
323
|
+
(commits = Array.isArray(commits) ? commits.map((commit) => String(commit)) : commits.split(CSV_RX)).length
|
|
324
|
+
) {
|
|
325
|
+
for (const oid of commits) {
|
|
326
|
+
// NOTE commits are stored using Symbol keys to distinguish them from tags and branches
|
|
327
|
+
refs.set(Symbol(oid), { oid, shortname: oid, fullname: 'commits/' + oid, type: 'commit' })
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (
|
|
331
|
+
!branchPatterns ||
|
|
332
|
+
!(branchPatterns = Array.isArray(branchPatterns)
|
|
333
|
+
? branchPatterns.map((pattern) => String(pattern))
|
|
334
|
+
: splitRefPatterns(String(branchPatterns))).length
|
|
335
|
+
) {
|
|
336
|
+
return [...refs.values()]
|
|
337
|
+
}
|
|
338
|
+
let useWorktree = false
|
|
339
|
+
if (!managed && (useWorktree = {})) {
|
|
308
340
|
if (worktreePatterns === '.') {
|
|
309
|
-
|
|
341
|
+
isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
|
|
342
|
+
worktreePatterns = []
|
|
343
|
+
} else if (!worktreePatterns) {
|
|
344
|
+
worktreePatterns = []
|
|
310
345
|
} else if (worktreePatterns === true) {
|
|
311
|
-
|
|
346
|
+
if (!isBare) useWorktree.main = true
|
|
347
|
+
// NOTE if we don't start at a linked worktree, linked worktree cannot be current worktree
|
|
348
|
+
if (isLinkedWorktree) useWorktree.linked = isLinkedWorktree
|
|
349
|
+
worktreePatterns = ['*']
|
|
350
|
+
} else if (worktreePatterns === '/.') {
|
|
351
|
+
if (!isBare) useWorktree.main = true
|
|
352
|
+
worktreePatterns = []
|
|
312
353
|
} else {
|
|
313
|
-
worktreePatterns =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
354
|
+
worktreePatterns = (
|
|
355
|
+
Array.isArray(worktreePatterns)
|
|
356
|
+
? worktreePatterns.map((pattern) => String(pattern))
|
|
357
|
+
: splitRefPatterns(String(worktreePatterns))
|
|
358
|
+
).reduce((accum, it) => {
|
|
359
|
+
if (it === '/.') return (isBare || (useWorktree.main = true)) && accum
|
|
360
|
+
if (it === '.') {
|
|
361
|
+
isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
|
|
362
|
+
} else {
|
|
363
|
+
accum.push(it)
|
|
364
|
+
}
|
|
365
|
+
return accum
|
|
366
|
+
}, [])
|
|
317
367
|
}
|
|
318
|
-
|
|
319
|
-
worktreePatterns = worktreePatterns === undefined ? [worktreeName || '.'] : []
|
|
368
|
+
if (!(useWorktree.main || useWorktree.linked)) useWorktree = false
|
|
320
369
|
}
|
|
321
|
-
|
|
322
|
-
|
|
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 {
|
|
370
|
+
let currentBranch
|
|
371
|
+
if (!isLinkedWorktree) {
|
|
334
372
|
let headBranchIdx
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
373
|
+
if (branchPatterns.length === 1 && (branchPatterns[0] === 'HEAD' || branchPatterns[0] === '.')) {
|
|
374
|
+
if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
|
|
375
|
+
branchPatterns = [currentBranch]
|
|
376
|
+
} else if (isBare) {
|
|
377
|
+
return [...refs.values()]
|
|
378
|
+
} else {
|
|
379
|
+
// NOTE current branch is undefined when HEAD is detached
|
|
380
|
+
const head = useWorktree.main ? repo.dir : noWorktree
|
|
381
|
+
refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
|
|
382
|
+
return [...refs.values()]
|
|
383
|
+
}
|
|
384
|
+
} else if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
|
|
385
|
+
// NOTE we can assume at least two entries if HEAD or . are present
|
|
386
|
+
if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
|
|
340
387
|
if (~branchPatterns.indexOf(currentBranch)) {
|
|
341
388
|
branchPatterns.splice(headBranchIdx, 1)
|
|
342
389
|
} else {
|
|
@@ -345,11 +392,7 @@ async function selectReferences (source, repo, remote) {
|
|
|
345
392
|
} else if (isBare) {
|
|
346
393
|
branchPatterns.splice(headBranchIdx, 1)
|
|
347
394
|
} else {
|
|
348
|
-
|
|
349
|
-
if (worktreePatterns[0] === '.') {
|
|
350
|
-
worktreePatterns = worktreePatterns.slice(1)
|
|
351
|
-
head = repo.dir
|
|
352
|
-
}
|
|
395
|
+
const head = useWorktree.main ? repo.dir : noWorktree
|
|
353
396
|
// NOTE current branch is undefined when HEAD is detached
|
|
354
397
|
refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
|
|
355
398
|
branchPatterns.splice(headBranchIdx, 1)
|
|
@@ -357,40 +400,51 @@ async function selectReferences (source, repo, remote) {
|
|
|
357
400
|
}
|
|
358
401
|
}
|
|
359
402
|
// NOTE isomorphic-git includes HEAD in list of remote branches (see https://isomorphic-git.org/docs/listBranches)
|
|
360
|
-
const remoteBranches =
|
|
403
|
+
const remoteBranches = remote
|
|
404
|
+
? (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
|
|
405
|
+
: []
|
|
361
406
|
if (remoteBranches.length) {
|
|
362
407
|
for (const shortname of filterRefs(remoteBranches, branchPatterns, patternCache)) {
|
|
363
408
|
const fullname = 'remotes/' + remote + '/' + shortname
|
|
364
409
|
refs.set(shortname, { shortname, fullname, type: 'branch', remote, head: noWorktree })
|
|
365
410
|
}
|
|
366
411
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
412
|
+
if (!managed) {
|
|
413
|
+
const localBranches = await git.listBranches(repo).then((branches) => {
|
|
414
|
+
if (branches.length || isBare) return branches
|
|
415
|
+
if (currentBranch != null) return [currentBranch]
|
|
416
|
+
return getCurrentBranchName(repo).then((branch) => (branch ? [branch] : []))
|
|
417
|
+
})
|
|
418
|
+
const worktrees = await findWorktrees(repo, worktreePatterns, useWorktree)
|
|
419
|
+
let onMatch
|
|
420
|
+
if ((useWorktree || worktreePatterns.length) && worktrees.size) {
|
|
421
|
+
const headNames = new Map()
|
|
422
|
+
worktrees.forEach(({ name, symbolicNames }, shortname) => {
|
|
423
|
+
if (name) {
|
|
424
|
+
const headName = 'HEAD@' + name
|
|
425
|
+
localBranches.push(headName)
|
|
426
|
+
headNames.set(headName, shortname)
|
|
382
427
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
428
|
+
if (symbolicNames) {
|
|
429
|
+
for (const symbolicName of symbolicNames) {
|
|
430
|
+
const symbolicHeadName = symbolicName === 'HEAD' ? symbolicName : 'HEAD@' + symbolicName
|
|
431
|
+
localBranches.push(symbolicHeadName)
|
|
432
|
+
headNames.set(symbolicHeadName, shortname)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
onMatch = (candidate, { pattern }) => {
|
|
437
|
+
const shortname = headNames.get(candidate)
|
|
438
|
+
if (!shortname) return candidate
|
|
439
|
+
if (pattern === 'HEAD' || pattern.startsWith('HEAD@')) return shortname
|
|
387
440
|
}
|
|
388
441
|
}
|
|
389
|
-
} else if (!remoteBranches.length) {
|
|
390
|
-
const localBranches = await git.listBranches(repo)
|
|
391
442
|
if (localBranches.length) {
|
|
392
|
-
|
|
393
|
-
|
|
443
|
+
const preferRemote = isBare && remoteBranches.length > 0
|
|
444
|
+
for (const shortname of filterRefs(localBranches, branchPatterns, patternCache, onMatch)) {
|
|
445
|
+
if (preferRemote && refs.has(shortname)) continue
|
|
446
|
+
const head = (worktrees.get(shortname) || { head: false }).head
|
|
447
|
+
refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
|
|
394
448
|
}
|
|
395
449
|
}
|
|
396
450
|
}
|
|
@@ -398,27 +452,28 @@ async function selectReferences (source, repo, remote) {
|
|
|
398
452
|
}
|
|
399
453
|
|
|
400
454
|
/**
|
|
401
|
-
* Returns the current branch name
|
|
455
|
+
* Returns the current branch name or undefined if the HEAD is detached.
|
|
402
456
|
*/
|
|
403
457
|
function getCurrentBranchName (repo, remote) {
|
|
404
458
|
return (
|
|
405
|
-
repo.noCheckout
|
|
459
|
+
remote && repo.noCheckout
|
|
406
460
|
? git
|
|
407
|
-
|
|
408
|
-
|
|
461
|
+
.resolveRef(Object.assign({ ref: 'refs/remotes/' + remote + '/HEAD', depth: 2 }, repo))
|
|
462
|
+
.catch(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 2 }, repo)))
|
|
409
463
|
: git.resolveRef(Object.assign({ ref: 'HEAD', depth: 2 }, repo))
|
|
410
464
|
).then((ref) => (ref.startsWith('refs/') ? ref.replace(SHORTEN_REF_RX, '') : undefined))
|
|
411
465
|
}
|
|
412
466
|
|
|
413
|
-
async function
|
|
467
|
+
async function selectStartPaths (source, repo, ref) {
|
|
414
468
|
const url = repo.url
|
|
415
469
|
const displayUrl = url || repo.dir
|
|
416
|
-
const { version, editUrl } = source
|
|
417
470
|
const worktreePath = ref.head
|
|
418
471
|
if (!worktreePath) {
|
|
419
|
-
ref.oid =
|
|
420
|
-
Object.assign(
|
|
421
|
-
|
|
472
|
+
ref.oid = ref.oid
|
|
473
|
+
? await git.expandOid(Object.assign({ oid: ref.oid }, repo)).catch(() => ref.oid)
|
|
474
|
+
: await git.resolveRef(
|
|
475
|
+
Object.assign(ref.detached ? { ref: 'HEAD', depth: 1 } : { ref: 'refs/' + ref.fullname }, repo)
|
|
476
|
+
)
|
|
422
477
|
}
|
|
423
478
|
if ('startPaths' in source) {
|
|
424
479
|
let startPaths
|
|
@@ -433,25 +488,31 @@ async function collectFilesFromReference (source, repo, remoteName, authStatus,
|
|
|
433
488
|
const flag = worktreePath ? ' <worktree>' : ref.remote && worktreePath === false ? ` <remotes/${ref.remote}>` : ''
|
|
434
489
|
throw new Error(`no start paths found in ${where} (${ref.type}: ${ref.shortname}${flag})`)
|
|
435
490
|
}
|
|
436
|
-
return
|
|
437
|
-
startPaths.map((startPath) =>
|
|
438
|
-
collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
|
|
439
|
-
)
|
|
440
|
-
)
|
|
491
|
+
return startPaths
|
|
441
492
|
}
|
|
442
|
-
|
|
443
|
-
return collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
|
|
493
|
+
return [cleanStartPath(coerceToString(source.startPath))]
|
|
444
494
|
}
|
|
445
495
|
|
|
446
|
-
function
|
|
496
|
+
async function collectFilesFromStartPaths (startPaths, repo, authStatus) {
|
|
497
|
+
const buckets = []
|
|
498
|
+
for (const { startPath, ref, originUrl, editUrl, version } of startPaths) {
|
|
499
|
+
buckets.push(await collectFilesFromStartPath(startPath, repo, authStatus, ref, originUrl, editUrl, version))
|
|
500
|
+
}
|
|
501
|
+
repo.cache = undefined
|
|
502
|
+
return buckets
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function collectFilesFromStartPath (startPath, repo, authStatus, ref, originUrl, editUrl, version) {
|
|
506
|
+
const worktreePath = ref.head
|
|
447
507
|
const origin = computeOrigin(originUrl, authStatus, repo.gitdir, ref, startPath, worktreePath, editUrl)
|
|
448
508
|
return (worktreePath ? readFilesFromWorktree(origin) : readFilesFromGitTree(repo, ref.oid, startPath))
|
|
449
|
-
.then((files) =>
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
509
|
+
.then((files) => {
|
|
510
|
+
const batch = deepClone((origin.descriptor = loadComponentDescriptor(files, ref, version)))
|
|
511
|
+
if ('nav' in batch && Array.isArray(batch.nav)) batch.nav.origin = origin
|
|
512
|
+
batch.files = files.map((file) => assignFileProperties(file, origin))
|
|
513
|
+
batch.origins = [origin]
|
|
514
|
+
return batch
|
|
515
|
+
})
|
|
455
516
|
.catch((err) => {
|
|
456
517
|
const where = worktreePath || (worktreePath === false ? repo.gitdir : repo.url || repo.dir)
|
|
457
518
|
const flag = worktreePath ? ' <worktree>' : ref.remote && worktreePath === false ? ` <remotes/${ref.remote}>` : ''
|
|
@@ -476,19 +537,18 @@ function readFilesFromWorktree (origin) {
|
|
|
476
537
|
}
|
|
477
538
|
|
|
478
539
|
function srcFs (cwd, origin) {
|
|
479
|
-
return new Promise((resolve, reject,
|
|
540
|
+
return new Promise((resolve, reject, files = []) =>
|
|
480
541
|
pipeline(
|
|
481
|
-
globStream(CONTENT_SRC_GLOB, Object.assign({
|
|
482
|
-
forEach(({ path:
|
|
483
|
-
if ((
|
|
484
|
-
const
|
|
485
|
-
const relpath =
|
|
486
|
-
|
|
487
|
-
(stat) =>
|
|
488
|
-
if (stat.isDirectory()) return done() // detects directories that slipped through cache check
|
|
542
|
+
globStream(CONTENT_SRC_GLOB, Object.assign({ cwd }, CONTENT_SRC_OPTS)),
|
|
543
|
+
forEach(({ path: relpath, dirent }, _, done) => {
|
|
544
|
+
if (dirent.isDirectory()) return done()
|
|
545
|
+
const relpathPosix = relpath
|
|
546
|
+
const abspath = posixify ? ospath.join(cwd, (relpath = ospath.normalize(relpath))) : cwd + '/' + relpath
|
|
547
|
+
fsp.stat(abspath).then(
|
|
548
|
+
(stat) =>
|
|
489
549
|
fsp.readFile(abspath).then(
|
|
490
550
|
(contents) => {
|
|
491
|
-
files.push(new File({ path:
|
|
551
|
+
files.push(new File({ path: relpathPosix, contents, stat, src: { abspath } }))
|
|
492
552
|
done()
|
|
493
553
|
},
|
|
494
554
|
(readErr) => {
|
|
@@ -498,22 +558,28 @@ function srcFs (cwd, origin) {
|
|
|
498
558
|
: logger.error(logObject, readErr.message.replace(`'${abspath}'`, relpath))
|
|
499
559
|
done()
|
|
500
560
|
}
|
|
501
|
-
)
|
|
502
|
-
},
|
|
561
|
+
),
|
|
503
562
|
(statErr) => {
|
|
504
563
|
const logObject = { file: { abspath, origin } }
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
564
|
+
if (dirent.isSymbolicLink()) {
|
|
565
|
+
fsp
|
|
566
|
+
.readlink(abspath)
|
|
567
|
+
.then(
|
|
568
|
+
(symlink) =>
|
|
569
|
+
(statErr.code === 'ELOOP' ? 'ELOOP: symbolic link cycle, ' : 'ENOENT: broken symbolic link, ') +
|
|
570
|
+
`${relpath} -> ${symlink}`,
|
|
571
|
+
() => statErr.message.replace(`'${abspath}'`, relpath)
|
|
572
|
+
)
|
|
573
|
+
.then((message) => {
|
|
574
|
+
logger.error(logObject, message)
|
|
575
|
+
done()
|
|
576
|
+
})
|
|
513
577
|
} else {
|
|
514
|
-
|
|
578
|
+
statErr.code === 'ENOENT'
|
|
579
|
+
? logger.warn(logObject, `ENOENT: file or directory disappeared, ${statErr.syscall} ${relpath}`)
|
|
580
|
+
: logger.error(logObject, statErr.message.replace(`'${abspath}'`, relpath))
|
|
581
|
+
done()
|
|
515
582
|
}
|
|
516
|
-
done()
|
|
517
583
|
}
|
|
518
584
|
)
|
|
519
585
|
}),
|
|
@@ -523,26 +589,25 @@ function srcFs (cwd, origin) {
|
|
|
523
589
|
}
|
|
524
590
|
|
|
525
591
|
function readFilesFromGitTree (repo, oid, startPath) {
|
|
526
|
-
return git
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
getGitTreeAtStartPath(repo, oid, startPath).then((start) =>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
592
|
+
return git.readTree(Object.assign({ oid }, repo)).then((root) => {
|
|
593
|
+
Object.assign(root, { dirname: '' })
|
|
594
|
+
return startPath
|
|
595
|
+
? getGitTreeAtStartPath(repo, oid, startPath).then((start) => {
|
|
596
|
+
Object.assign(start, { dirname: startPath })
|
|
597
|
+
return srcGitTree(repo, root, start)
|
|
598
|
+
})
|
|
599
|
+
: srcGitTree(repo, root)
|
|
600
|
+
})
|
|
533
601
|
}
|
|
534
602
|
|
|
535
603
|
function getGitTreeAtStartPath (repo, oid, startPath) {
|
|
536
|
-
return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).
|
|
537
|
-
|
|
538
|
-
(
|
|
539
|
-
|
|
540
|
-
throw new Error(`the start path '${startPath}' ${m}`)
|
|
541
|
-
}
|
|
542
|
-
)
|
|
604
|
+
return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).catch((err) => {
|
|
605
|
+
const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
|
|
606
|
+
throw new Error(`the start path '${startPath}' ${m}`)
|
|
607
|
+
})
|
|
543
608
|
}
|
|
544
609
|
|
|
545
|
-
function srcGitTree (repo, root, start) {
|
|
610
|
+
function srcGitTree (repo, root, start = root) {
|
|
546
611
|
return new Promise((resolve, reject) => {
|
|
547
612
|
const files = []
|
|
548
613
|
createGitTreeWalker(repo, root, filterGitEntry, gitEntryToFile)
|
|
@@ -585,7 +650,8 @@ function visitGitTree (emitter, repo, root, filter, convert, parent, dirname = '
|
|
|
585
650
|
(target) => {
|
|
586
651
|
if (target.type === 'tree') {
|
|
587
652
|
return visitGitTree(emitter, repo, root, filter, convert, target, vfilePath, target.following)
|
|
588
|
-
}
|
|
653
|
+
}
|
|
654
|
+
if (target.type === 'blob' && filterVerdict === true && (mode = FILE_MODES[target.mode])) {
|
|
589
655
|
return convert(Object.assign({ mode, oid: target.oid, path: vfilePath }, repo)).then((result) =>
|
|
590
656
|
emitter.emit('entry', result)
|
|
591
657
|
)
|
|
@@ -657,11 +723,11 @@ function readGitObjectAtPath (repo, root, parent, pathSegments, following) {
|
|
|
657
723
|
if (entry.path === firstPathSegment) {
|
|
658
724
|
return entry.type === 'tree'
|
|
659
725
|
? git.readTree(Object.assign({ oid: entry.oid }, repo)).then((subtree) => {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
726
|
+
Object.assign(subtree, { dirname: path.join(parent.dirname, entry.path) })
|
|
727
|
+
return (pathSegments = pathSegments.slice(1)).length
|
|
728
|
+
? readGitObjectAtPath(repo, root, subtree, pathSegments, following)
|
|
729
|
+
: Object.assign(subtree, { type: 'tree', following }) // Q: should this create copy?
|
|
730
|
+
})
|
|
665
731
|
: entry.mode === SYMLINK_FILE_MODE
|
|
666
732
|
? readGitSymlink(repo, root, parent, entry, following)
|
|
667
733
|
: Promise.resolve(entry)
|
|
@@ -684,8 +750,13 @@ function filterGitEntry (entry) {
|
|
|
684
750
|
|
|
685
751
|
function gitEntryToFile (entry) {
|
|
686
752
|
return git.readBlob(entry).then(({ blob: contents }) => {
|
|
687
|
-
|
|
688
|
-
|
|
753
|
+
const stat = {
|
|
754
|
+
mode: entry.mode,
|
|
755
|
+
size: (contents = Buffer.from(contents.buffer)).byteLength,
|
|
756
|
+
isDirectory: invariably.false,
|
|
757
|
+
isFile: invariably.true,
|
|
758
|
+
isSymbolicLink: invariably.false,
|
|
759
|
+
}
|
|
689
760
|
return new File({ path: entry.path, contents, stat })
|
|
690
761
|
})
|
|
691
762
|
}
|
|
@@ -697,7 +768,7 @@ function loadComponentDescriptor (files, ref, version) {
|
|
|
697
768
|
files.splice(descriptorFileIdx, 1)
|
|
698
769
|
let data
|
|
699
770
|
try {
|
|
700
|
-
data = yaml.load(descriptorFile.contents.toString(), { schema: yaml.CORE_SCHEMA })
|
|
771
|
+
data = Object(yaml.load(descriptorFile.contents.toString(), { schema: yaml.CORE_SCHEMA }))
|
|
701
772
|
} catch (err) {
|
|
702
773
|
throw Object.assign(err, { message: `${COMPONENT_DESC_FILENAME} has invalid syntax; ${err.message}` })
|
|
703
774
|
}
|
|
@@ -710,18 +781,18 @@ function loadComponentDescriptor (files, ref, version) {
|
|
|
710
781
|
if (!version) {
|
|
711
782
|
if (version === undefined) throw new Error(`${COMPONENT_DESC_FILENAME} is missing a version`)
|
|
712
783
|
if (version === false) throw new Error(`${COMPONENT_DESC_FILENAME} has an invalid version`)
|
|
713
|
-
version =
|
|
784
|
+
version = typeof version === 'number' ? '' + version : ''
|
|
714
785
|
} else if (version === true) {
|
|
715
786
|
version = ref.shortname.replace(PATH_SEPARATOR_RX, '-')
|
|
716
787
|
} else if (version.constructor === Object) {
|
|
717
788
|
const refname = ref.shortname
|
|
718
789
|
let matched
|
|
719
790
|
if (refname in version) {
|
|
720
|
-
matched = version[refname]
|
|
791
|
+
matched = '' + (version[refname] ?? '')
|
|
721
792
|
} else if (
|
|
722
793
|
!Object.entries(version).some(([pattern, replacement]) => {
|
|
723
|
-
const result = refname.replace(makeMatcherRx(pattern, VERSION_MATCHER_OPTS), '\0' + replacement)
|
|
724
|
-
if (result === refname) return false
|
|
794
|
+
const result = refname.replace(makeMatcherRx(pattern, VERSION_MATCHER_OPTS), '\0' + (replacement ?? ''))
|
|
795
|
+
if (result === refname) return false // no match
|
|
725
796
|
matched = result.substr(1)
|
|
726
797
|
return true
|
|
727
798
|
})
|
|
@@ -736,7 +807,7 @@ function loadComponentDescriptor (files, ref, version) {
|
|
|
736
807
|
throw new Error(`version in ${COMPONENT_DESC_FILENAME} cannot have path segments: ${version}`)
|
|
737
808
|
}
|
|
738
809
|
data.version = version
|
|
739
|
-
return camelCaseKeys(data, ['asciidoc'])
|
|
810
|
+
return camelCaseKeys(data, ['asciidoc', 'ext'])
|
|
740
811
|
}
|
|
741
812
|
|
|
742
813
|
function assignFileProperties (file, origin) {
|
|
@@ -753,20 +824,23 @@ function assignFileProperties (file, origin) {
|
|
|
753
824
|
return file
|
|
754
825
|
}
|
|
755
826
|
|
|
756
|
-
function buildFetchOptions (repo, progress, displayUrl, credentialsFromUrl, gitPlugins,
|
|
827
|
+
function buildFetchOptions (repo, progress, displayUrl, credentialsFromUrl, gitPlugins, fetch, operation) {
|
|
757
828
|
const { credentialManager, http, urlRouter } = gitPlugins
|
|
829
|
+
const corsProxy = false
|
|
830
|
+
const depth = fetch.depth || undefined
|
|
758
831
|
const onAuth = resolveCredentials.bind(credentialManager, new Map().set(undefined, credentialsFromUrl))
|
|
759
832
|
const onAuthFailure = onAuth
|
|
760
833
|
const onAuthSuccess = (url) => credentialManager.approved({ url })
|
|
761
|
-
const opts = Object.assign({ corsProxy
|
|
834
|
+
const opts = Object.assign({ corsProxy, depth, http, onAuth, onAuthFailure, onAuthSuccess }, repo)
|
|
762
835
|
if (urlRouter) opts.url = urlRouter.ensureGitSuffix(opts.url)
|
|
763
836
|
if (progress) opts.onProgress = createProgressListener(progress, displayUrl, operation)
|
|
764
837
|
if (operation === 'fetch') {
|
|
765
838
|
opts.prune = true
|
|
766
|
-
if (
|
|
767
|
-
} else if (!
|
|
839
|
+
if (fetch.tags) opts.tags = opts.pruneTags = true
|
|
840
|
+
} else if (!fetch.tags) {
|
|
768
841
|
opts.noTags = true
|
|
769
842
|
}
|
|
843
|
+
if (fetch.commits) opts.oids = fetch.commits
|
|
770
844
|
return opts
|
|
771
845
|
}
|
|
772
846
|
|
|
@@ -801,7 +875,10 @@ function createProgressListener (progress, progressLabel, operation) {
|
|
|
801
875
|
// NOTE leave room for indeterminate progress at end of bar; this isn't strictly needed for a bare clone
|
|
802
876
|
progressBar.scaleFactor = Math.max(0, (ticks - 1) / ticks)
|
|
803
877
|
progressBar.tick(0)
|
|
804
|
-
return Object.assign(onGitProgress.bind(progressBar), {
|
|
878
|
+
return Object.assign(onGitProgress.bind(progressBar), {
|
|
879
|
+
finish: onGitComplete.bind(progressBar),
|
|
880
|
+
reset: () => progressBar.update(0),
|
|
881
|
+
})
|
|
805
882
|
}
|
|
806
883
|
|
|
807
884
|
function formatProgressBar (label, maxLabelWidth, operation) {
|
|
@@ -829,7 +906,6 @@ function onGitProgress ({ phase, loaded, total }) {
|
|
|
829
906
|
|
|
830
907
|
function onGitComplete (err) {
|
|
831
908
|
if (err) {
|
|
832
|
-
// TODO could use progressBar.interrupt() to replace bar with message instead
|
|
833
909
|
this.chars.incomplete = '?'
|
|
834
910
|
this.update(0)
|
|
835
911
|
// NOTE force progress bar to update regardless of throttle setting
|
|
@@ -863,9 +939,8 @@ function identifyAuthStatus (credentialManager, credentials, url) {
|
|
|
863
939
|
const status = credentialManager.status({ url })
|
|
864
940
|
if (credentials) {
|
|
865
941
|
return typeof status === 'string' && status.startsWith('requested,') ? 'auth-required' : 'auth-embedded'
|
|
866
|
-
} else if (status != null) {
|
|
867
|
-
return 'auth-required'
|
|
868
942
|
}
|
|
943
|
+
if (status != null) return 'auth-required'
|
|
869
944
|
}
|
|
870
945
|
|
|
871
946
|
/**
|
|
@@ -874,7 +949,7 @@ function identifyAuthStatus (credentialManager, credentials, url) {
|
|
|
874
949
|
* The purpose of this function is generate a safe, unique folder name for the cloned
|
|
875
950
|
* repository that gets stored in the cache directory.
|
|
876
951
|
*
|
|
877
|
-
* The generated folder name follows the pattern: <basename>-<sha1
|
|
952
|
+
* The generated folder name follows the pattern: <basename>-<sha1-of-normalized-url>.git
|
|
878
953
|
*
|
|
879
954
|
* @param {String} url - The repository URL to convert.
|
|
880
955
|
* @returns {String} The generated folder name.
|
|
@@ -890,21 +965,18 @@ function generateCloneFolderName (url) {
|
|
|
890
965
|
*
|
|
891
966
|
* @param {Repository} repo - The repository on which to operate.
|
|
892
967
|
* @param {String} remoteName - The name of the remote to resolve.
|
|
893
|
-
* @returns {String} The URL of the specified remote, if defined
|
|
968
|
+
* @returns {String} The URL of the specified remote, if defined
|
|
894
969
|
*/
|
|
895
970
|
function resolveRemoteUrl (repo, remoteName) {
|
|
896
971
|
return git.getConfig(Object.assign({ path: 'remote.' + remoteName + '.url' }, repo)).then((url) => {
|
|
897
|
-
if (url)
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
972
|
+
if (!url) return
|
|
973
|
+
if (url.startsWith('https://') || url.startsWith('http://')) {
|
|
974
|
+
return ~url.indexOf('@') ? url.replace(URL_AUTH_CLEANER_RX, '$1') : url
|
|
975
|
+
}
|
|
976
|
+
if (url.startsWith('git@')) return 'https://' + url.substr(4).replace(':', '/')
|
|
977
|
+
if (url.startsWith('ssh://')) {
|
|
978
|
+
return 'https://' + url.substr(url.indexOf('@') + 1 || 6).replace(URL_PORT_CLEANER_RX, '$1')
|
|
905
979
|
}
|
|
906
|
-
url = posixify ? 'file:///' + posixify(repo.dir) : 'file://' + repo.dir
|
|
907
|
-
return ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url
|
|
908
980
|
})
|
|
909
981
|
}
|
|
910
982
|
|
|
@@ -918,24 +990,20 @@ function isDirectory (url) {
|
|
|
918
990
|
return fsp.stat(url).then((stat) => stat.isDirectory(), invariably.false)
|
|
919
991
|
}
|
|
920
992
|
|
|
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
993
|
function tagsSpecified (sources) {
|
|
936
994
|
return sources.some(({ tags }) => tags && (Array.isArray(tags) ? tags.length : true))
|
|
937
995
|
}
|
|
938
996
|
|
|
997
|
+
function commitsRequested (sources) {
|
|
998
|
+
if (!sources.some(({ commits }) => commits && (Array.isArray(commits) ? commits.length : true))) return
|
|
999
|
+
const result = new Set()
|
|
1000
|
+
for (const { commits } of sources) {
|
|
1001
|
+
if (!commits) continue
|
|
1002
|
+
for (const commit of Array.isArray(commits) ? commits : commits.split(CSV_RX)) result.add(String(commit))
|
|
1003
|
+
}
|
|
1004
|
+
return [...result]
|
|
1005
|
+
}
|
|
1006
|
+
|
|
939
1007
|
function loadGitPlugins (gitConfig, networkConfig, startDir) {
|
|
940
1008
|
const plugins = new Map((git.cores || git.default.cores || new Map()).get(GIT_CORE))
|
|
941
1009
|
for (const [name, request] of Object.entries(gitConfig.plugins || {})) {
|
|
@@ -946,7 +1014,7 @@ function loadGitPlugins (gitConfig, networkConfig, startDir) {
|
|
|
946
1014
|
if (typeof credentialManager.configure === 'function') {
|
|
947
1015
|
credentialManager.configure({ config: gitConfig.credentials, startDir })
|
|
948
1016
|
}
|
|
949
|
-
if (typeof credentialManager.status !== 'function') Object.assign(credentialManager, { status
|
|
1017
|
+
if (typeof credentialManager.status !== 'function') Object.assign(credentialManager, { status: invariably.void })
|
|
950
1018
|
} else {
|
|
951
1019
|
credentialManager = new GitCredentialManagerStore().configure({ config: gitConfig.credentials, startDir })
|
|
952
1020
|
}
|
|
@@ -979,8 +1047,8 @@ function ensureCacheDir (preferredCacheDir, startDir) {
|
|
|
979
1047
|
)
|
|
980
1048
|
}
|
|
981
1049
|
|
|
982
|
-
function transformGitCloneError (err, displayUrl) {
|
|
983
|
-
let wrappedMsg, trimMessage
|
|
1050
|
+
function transformGitCloneError (err, displayUrl, authRequested) {
|
|
1051
|
+
let wrappedMsg, recoverable, trimMessage
|
|
984
1052
|
if (HTTP_ERROR_CODE_RX.test(err.code)) {
|
|
985
1053
|
switch (err.data.statusCode) {
|
|
986
1054
|
case 401:
|
|
@@ -989,30 +1057,31 @@ function transformGitCloneError (err, displayUrl) {
|
|
|
989
1057
|
: 'Content repository not found or requires credentials'
|
|
990
1058
|
break
|
|
991
1059
|
case 404:
|
|
992
|
-
wrappedMsg =
|
|
1060
|
+
wrappedMsg = authRequested
|
|
1061
|
+
? 'Content repository not found or credentials were rejected'
|
|
1062
|
+
: 'Content repository not found'
|
|
993
1063
|
break
|
|
994
1064
|
default:
|
|
995
1065
|
wrappedMsg = err.message
|
|
996
|
-
trimMessage = true
|
|
1066
|
+
recoverable = trimMessage = true
|
|
997
1067
|
}
|
|
998
1068
|
} else if (err instanceof UrlParseError || err instanceof UnknownTransportError) {
|
|
999
1069
|
wrappedMsg = 'Content source uses an unsupported transport protocol'
|
|
1000
1070
|
} else if (err.code === 'ENOTFOUND') {
|
|
1001
1071
|
wrappedMsg = `Content repository host could not be resolved: ${err.hostname}`
|
|
1002
1072
|
} else {
|
|
1003
|
-
wrappedMsg =
|
|
1004
|
-
trimMessage = true
|
|
1005
|
-
}
|
|
1006
|
-
if (trimMessage) {
|
|
1007
|
-
wrappedMsg = ~(wrappedMsg = wrappedMsg.trimRight()).indexOf('. ') ? wrappedMsg : wrappedMsg.replace(/\.$/, '')
|
|
1073
|
+
wrappedMsg = err.message || String(err)
|
|
1074
|
+
recoverable = trimMessage = true
|
|
1008
1075
|
}
|
|
1076
|
+
if (trimMessage && !~(wrappedMsg = wrappedMsg.trimEnd()).indexOf('. ')) wrappedMsg = wrappedMsg.replace(/\.$/, '')
|
|
1009
1077
|
const errWrapper = new Error(`${wrappedMsg} (url: ${displayUrl})`)
|
|
1010
|
-
errWrapper.stack += `\nCaused by: ${err.stack
|
|
1078
|
+
errWrapper.stack += `\nCaused by: ${err.stack ? inspect(err).replace(/^Error \[(.+?)\](?=: )/, '$1') : err}`
|
|
1079
|
+
if (recoverable) Object.defineProperty(errWrapper, 'recoverable', { value: true })
|
|
1011
1080
|
return errWrapper
|
|
1012
1081
|
}
|
|
1013
1082
|
|
|
1014
1083
|
function splitRefPatterns (str) {
|
|
1015
|
-
return ~str.indexOf('{') ?
|
|
1084
|
+
return str.split(~str.indexOf('{') ? VENTILATED_CSV_RX : CSV_RX)
|
|
1016
1085
|
}
|
|
1017
1086
|
|
|
1018
1087
|
function camelCaseKeys (o, stopPaths = [], p = '') {
|
|
@@ -1035,54 +1104,116 @@ function coerceToString (value) {
|
|
|
1035
1104
|
return value == null ? '' : String(value)
|
|
1036
1105
|
}
|
|
1037
1106
|
|
|
1038
|
-
|
|
1107
|
+
function resolveRepositoryFromWorktree (repo) {
|
|
1039
1108
|
return fsp
|
|
1040
1109
|
.readFile(repo.gitdir, 'utf8')
|
|
1041
|
-
.then((contents) => contents.
|
|
1042
|
-
.then((worktreeGitdir) =>
|
|
1043
|
-
|
|
1044
|
-
return fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8').then(
|
|
1110
|
+
.then((contents) => contents.substr(8).trimEnd())
|
|
1111
|
+
.then((worktreeGitdir) =>
|
|
1112
|
+
fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8').then(
|
|
1045
1113
|
(contents) => {
|
|
1046
|
-
const gitdir = ospath.join(worktreeGitdir, contents.
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1114
|
+
const gitdir = ospath.join(worktreeGitdir, contents.trimEnd())
|
|
1115
|
+
const dir = ospath.basename(gitdir) === '.git' ? ospath.dirname(gitdir) : gitdir
|
|
1116
|
+
const name = ospath.basename(worktreeGitdir)
|
|
1117
|
+
return Object.assign(repo, { dir, gitdir, worktree: { gitdir: worktreeGitdir, name } })
|
|
1050
1118
|
},
|
|
1051
1119
|
() => repo
|
|
1052
1120
|
)
|
|
1053
|
-
|
|
1121
|
+
)
|
|
1054
1122
|
}
|
|
1055
1123
|
|
|
1056
|
-
function findWorktrees (repo, patterns) {
|
|
1057
|
-
|
|
1058
|
-
const mainWorktree =
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1124
|
+
function findWorktrees (repo, patterns, useWorktree) {
|
|
1125
|
+
const useLinkedWorktree = !!useWorktree.linked
|
|
1126
|
+
const mainWorktree = useWorktree.main
|
|
1127
|
+
? getCurrentBranchName(repo).then((branch) => {
|
|
1128
|
+
if (!branch) return
|
|
1129
|
+
return [branch, { head: repo.dir, name: undefined, symbolicNames: useLinkedWorktree ? ['/.'] : ['/.', '.'] }]
|
|
1130
|
+
})
|
|
1131
|
+
: Promise.resolve()
|
|
1132
|
+
if (!(useLinkedWorktree || patterns.length)) return mainWorktree.then((entry) => new Map(entry && [entry]))
|
|
1133
|
+
const worktreesDir = ospath.join(repo.dir, repo.dir === repo.gitdir ? '' : '.git', 'worktrees')
|
|
1063
1134
|
const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
? fsp
|
|
1135
|
+
const scanWorktrees = patterns.length
|
|
1136
|
+
? fsp
|
|
1067
1137
|
.readdir(worktreesDir)
|
|
1068
1138
|
.then((worktreeNames) => filterRefs(worktreeNames, patterns, patternCache), invariably.emptyArray)
|
|
1069
|
-
.then((worktreeNames) =>
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1139
|
+
.then((worktreeNames) => {
|
|
1140
|
+
if (useLinkedWorktree && !~worktreeNames.indexOf(useWorktree.linked)) worktreeNames.push(useWorktree.linked)
|
|
1141
|
+
return worktreeNames
|
|
1142
|
+
})
|
|
1143
|
+
: Promise.resolve(useLinkedWorktree ? [useWorktree.linked] : [])
|
|
1144
|
+
return scanWorktrees
|
|
1145
|
+
.then((worktreeNames) =>
|
|
1146
|
+
Promise.all(
|
|
1147
|
+
worktreeNames.map((name) => {
|
|
1148
|
+
const symbolicNames = useLinkedWorktree && name === useWorktree.linked ? ['.', 'HEAD'] : undefined
|
|
1149
|
+
const gitdir = ospath.resolve(worktreesDir, name)
|
|
1150
|
+
// NOTE branch name defaults to worktree name if HEAD is detached
|
|
1151
|
+
return getCurrentBranchName(Object.assign({}, repo, { gitdir })).then((branch = name) =>
|
|
1152
|
+
fsp
|
|
1153
|
+
.readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
|
|
1154
|
+
.then((contents) => [branch, { head: ospath.dirname(contents.trimEnd()), name, symbolicNames }])
|
|
1082
1155
|
)
|
|
1083
|
-
)
|
|
1084
|
-
|
|
1085
|
-
|
|
1156
|
+
})
|
|
1157
|
+
)
|
|
1158
|
+
)
|
|
1159
|
+
.then((entries) => new Map(entries))
|
|
1160
|
+
.then((worktrees) => mainWorktree.then((result) => (result ? worktrees.set(result[0], result[1]) : worktrees)))
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async function gracefulPromiseAllWithLimit (tasks, limit = Infinity) {
|
|
1164
|
+
const rejections = []
|
|
1165
|
+
const recordRejection = (err) => rejections.push(err) && undefined
|
|
1166
|
+
const started = []
|
|
1167
|
+
if (tasks.length <= limit) {
|
|
1168
|
+
for (const task of tasks) started.push(task().catch(recordRejection))
|
|
1169
|
+
} else {
|
|
1170
|
+
const pending = []
|
|
1171
|
+
for (const task of tasks) {
|
|
1172
|
+
const current = task()
|
|
1173
|
+
.catch(recordRejection)
|
|
1174
|
+
.finally(() => pending.splice(pending.indexOf(current), 1))
|
|
1175
|
+
started.push(current)
|
|
1176
|
+
if (pending.push(current) < limit) continue
|
|
1177
|
+
await Promise.race(pending)
|
|
1178
|
+
if (rejections.length) break
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return Promise.all(started).then((results) => [results, rejections])
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function promiseAllWithLimit (tasks, limit = Infinity) {
|
|
1185
|
+
if (tasks.length <= limit) return Promise.all(tasks.map((task) => task()))
|
|
1186
|
+
const started = []
|
|
1187
|
+
const pending = []
|
|
1188
|
+
for (const task of tasks) {
|
|
1189
|
+
const current = task().finally(() => pending.splice(pending.indexOf(current), 1))
|
|
1190
|
+
started.push(current)
|
|
1191
|
+
if (pending.push(current) >= limit) await Promise.race(pending)
|
|
1192
|
+
}
|
|
1193
|
+
return Promise.all(started)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function ensureOids (opts) {
|
|
1197
|
+
if (!opts.oids) return
|
|
1198
|
+
let prevShallowCommits = await getShallowCommits(opts)
|
|
1199
|
+
if (prevShallowCommits == null) return
|
|
1200
|
+
let oids = opts.oids.slice()
|
|
1201
|
+
const deepenOpts = Object.assign({}, opts, { relative: true })
|
|
1202
|
+
const format = 'deflated'
|
|
1203
|
+
while (oids.length) {
|
|
1204
|
+
deepenOpts.onProgress?.reset()
|
|
1205
|
+
await git.fetch(deepenOpts)
|
|
1206
|
+
const shallowCommits = await getShallowCommits(opts)
|
|
1207
|
+
if (shallowCommits == null || shallowCommits === prevShallowCommits) break
|
|
1208
|
+
prevShallowCommits = shallowCommits
|
|
1209
|
+
oids = await Promise.all(
|
|
1210
|
+
oids.map((oid) => git.readObject(Object.assign({ oid, format }, opts)).then(invariably.void, () => oid))
|
|
1211
|
+
).then((results) => results.filter((it) => it))
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function getShallowCommits ({ gitdir }) {
|
|
1216
|
+
return fsp.readFile(ospath.join(gitdir, 'shallow'), 'utf8').catch(invariably.void)
|
|
1086
1217
|
}
|
|
1087
1218
|
|
|
1088
1219
|
module.exports = aggregateContent
|