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