@wmfs/lerna-sync 1.38.0 → 1.39.0
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/.circleci/config.yml +21 -0
- package/CHANGELOG.md +15 -0
- package/README.md +147 -59
- package/lib/discover.js +52 -0
- package/lib/execute-plan.js +141 -0
- package/lib/github-graphql-query.js +39 -36
- package/lib/index.js +78 -345
- package/lib/inspect.js +99 -0
- package/lib/plan-sync.js +182 -0
- package/lib/report.js +195 -0
- package/lib/utils/git.js +59 -0
- package/lib/utils/map-limit.js +17 -0
- package/lib/utils/package-folders-from-patterns.js +25 -0
- package/package.json +7 -7
- package/test/discover-spec.js +128 -0
- package/test/execute-plan-spec.js +233 -0
- package/test/helpers/fixtures.js +64 -0
- package/test/helpers/git-sandbox.js +87 -0
- package/test/inspect-spec.js +148 -0
- package/test/plan-sync-spec.js +247 -0
- package/test/report-spec.js +178 -0
- package/test/sync-spec.js +130 -8
- package/test/utils-spec.js +136 -0
- package/lib/actions/clone.js +0 -46
- package/lib/actions/index.js +0 -4
- package/lib/actions/pull.js +0 -52
- package/lib/utils/Messages.js +0 -69
- package/lib/utils/run.js +0 -11
package/lib/index.js
CHANGED
|
@@ -1,373 +1,106 @@
|
|
|
1
1
|
const jsonfile = require('jsonfile')
|
|
2
2
|
const path = require('path')
|
|
3
|
-
const fs = require('fs')
|
|
4
|
-
const glob = require('glob')
|
|
5
3
|
const humanizeDuration = require('humanize-duration')
|
|
6
4
|
const chalk = require('chalk')
|
|
7
|
-
const getLocalGitInfo = require('git-repo-info')
|
|
8
|
-
const dottie = require('dottie')
|
|
9
5
|
const { graphql } = require('@octokit/graphql')
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
6
|
+
const discover = require('./discover')
|
|
7
|
+
const inspect = require('./inspect')
|
|
8
|
+
const planSync = require('./plan-sync')
|
|
9
|
+
const { report, printPlan, describeResult } = require('./report')
|
|
10
|
+
const { executePlan } = require('./execute-plan')
|
|
11
|
+
const packageFoldersFromPatterns = require('./utils/package-folders-from-patterns')
|
|
14
12
|
|
|
15
13
|
class LernaSync {
|
|
16
14
|
constructor (options) {
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
afterValue,
|
|
31
|
-
queryString: `org:${this.options.gitHubOrgName}`,
|
|
32
|
-
headers: {
|
|
33
|
-
authorization: `token ${this.options.gitHubToken}`
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
result.search.edges.forEach(
|
|
37
|
-
(edge) => {
|
|
38
|
-
const repoName = dottie.get(edge, 'node.name')
|
|
39
|
-
const historyEdges = dottie.get(edge, 'node.ref.target.history.edges')
|
|
40
|
-
if (historyEdges && historyEdges.length > 0) {
|
|
41
|
-
let packageJson = dottie.get(edge, 'node.object.text')
|
|
42
|
-
if (packageJson) {
|
|
43
|
-
packageJson = JSON.parse(packageJson)
|
|
44
|
-
}
|
|
45
|
-
this.applyRepo(
|
|
46
|
-
repoName,
|
|
47
|
-
{
|
|
48
|
-
gitHubSha: historyEdges[0].node.oid,
|
|
49
|
-
gitHubMessageHeadline: historyEdges[0].node.messageHeadline,
|
|
50
|
-
gitHubAuthor: historyEdges[0].node.author,
|
|
51
|
-
gitHubPackageJson: packageJson,
|
|
52
|
-
gitHubLernaPackageName: this.options.lernaPackageRouterFunction(packageJson)
|
|
53
|
-
}
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
)
|
|
58
|
-
hasNextPage = result.search.pageInfo.hasNextPage
|
|
59
|
-
afterValue = result.search.pageInfo.endCursor
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
collectLocalGitInfo () {
|
|
64
|
-
console.log(chalk.gray('Collecting local Git information'))
|
|
65
|
-
this.lernaJson.packages.forEach(
|
|
66
|
-
(relPackagePath) => {
|
|
67
|
-
const fullPackagePath = path.resolve(this.options.monorepoPath, relPackagePath + '/')
|
|
68
|
-
const packageRepoPaths = glob.sync(fullPackagePath, {
|
|
69
|
-
windowsPathsNoEscape: true
|
|
70
|
-
})
|
|
71
|
-
packageRepoPaths.forEach(
|
|
72
|
-
(repoPath) => {
|
|
73
|
-
const localRepoInfo = {
|
|
74
|
-
localGit: getLocalGitInfo(repoPath),
|
|
75
|
-
localLernaPackageName: path.basename(path.resolve(repoPath, '..'))
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const packageJsonPath = path.join(repoPath, 'package.json')
|
|
79
|
-
try {
|
|
80
|
-
localRepoInfo.localPackageJson = jsonfile.readFileSync(packageJsonPath)
|
|
81
|
-
} catch (e) {
|
|
82
|
-
// Just ignore failure to grab package.json.
|
|
83
|
-
console.error(e)
|
|
84
|
-
}
|
|
85
|
-
this.applyRepo(
|
|
86
|
-
path.basename(repoPath),
|
|
87
|
-
localRepoInfo
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
)
|
|
15
|
+
this.lernaPackageRouterFunction = options.lernaPackageRouterFunction
|
|
16
|
+
this.cloneUrl = options.cloneUrl
|
|
17
|
+
this.updateLocalDefaultBranch = options.updateLocalDefaultBranch
|
|
18
|
+
this.verbose = options.verbose
|
|
19
|
+
this.dryRun = options.dryRun
|
|
20
|
+
this.monorepoPath = options.monorepoPath
|
|
21
|
+
this.concurrency = validateConcurrency(options.concurrency)
|
|
22
|
+
this.gitHubOrgName = options.gitHubOrgName
|
|
23
|
+
this.lernaJson = jsonfile.readFileSync(path.join(options.monorepoPath, 'lerna.json'))
|
|
24
|
+
this.graphql = graphql.defaults({
|
|
25
|
+
headers: {
|
|
26
|
+
authorization: `token ${options.gitHubToken}`
|
|
91
27
|
}
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
applyRepo (repoName, info) {
|
|
96
|
-
if (Object.prototype.hasOwnProperty.call(this.repos, repoName)) {
|
|
97
|
-
const existingInfo = this.repos[repoName]
|
|
98
|
-
this.repos[repoName] = _.defaults(info, existingInfo)
|
|
99
|
-
} else {
|
|
100
|
-
this.repos[repoName] = info
|
|
101
|
-
}
|
|
28
|
+
})
|
|
102
29
|
}
|
|
103
30
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
containingDirName: path.basename(this.options.monorepoPath),
|
|
107
|
-
packages: {},
|
|
108
|
-
flattenedPackages: [],
|
|
109
|
-
totalPackageCount: 0,
|
|
110
|
-
rejectedCount: 0,
|
|
111
|
-
actions: [],
|
|
112
|
-
allNpmPackageNames: [],
|
|
113
|
-
gitHubDeps: {
|
|
114
|
-
dependencyPackageNames: [],
|
|
115
|
-
devDependencyPackageNames: []
|
|
116
|
-
},
|
|
117
|
-
existingLocalDeps: {
|
|
118
|
-
dependencyPackageNames: [],
|
|
119
|
-
devDependencyPackageNames: []
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function applyDeps (target, packageJson) {
|
|
124
|
-
if (packageJson) {
|
|
125
|
-
if (packageJson.dependencies) {
|
|
126
|
-
target.dependencyPackageNames = _.union(
|
|
127
|
-
target.dependencyPackageNames,
|
|
128
|
-
_.keys(packageJson.dependencies)
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
if (packageJson.devDependencies) {
|
|
132
|
-
target.devDependencyPackageNames = _.union(
|
|
133
|
-
target.devDependencyPackageNames,
|
|
134
|
-
_.keys(packageJson.devDependencies)
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function applyNpmPackageName (packageJson) {
|
|
141
|
-
if (packageJson) {
|
|
142
|
-
const name = packageJson.name
|
|
143
|
-
if (processed.allNpmPackageNames.indexOf(name) === -1) {
|
|
144
|
-
processed.allNpmPackageNames.push(name)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
31
|
+
async sync () {
|
|
32
|
+
const start = Date.now()
|
|
148
33
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (pkgName) {
|
|
152
|
-
processed.flattenedPackages.push({ repoName, pkgName })
|
|
34
|
+
const packagePatterns = this.lernaJson.packages
|
|
35
|
+
const packageFolders = packageFoldersFromPatterns(packagePatterns)
|
|
153
36
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
repos: {}
|
|
159
|
-
}
|
|
160
|
-
processed.packages[pkgName] = pkgInfo
|
|
161
|
-
} else {
|
|
162
|
-
pkgInfo = processed.packages[pkgName]
|
|
163
|
-
}
|
|
37
|
+
// 1. Discover (remote)
|
|
38
|
+
// Make one paginated GraphQL query that returns the name,
|
|
39
|
+
// default branch, head commit and package.json
|
|
40
|
+
// for every repo in the org.
|
|
164
41
|
|
|
165
|
-
|
|
166
|
-
name: repoName,
|
|
167
|
-
gitHubSha: rawRepo.gitHubSha,
|
|
168
|
-
gitHubMessageHeadline: rawRepo.gitHubMessageHeadline,
|
|
169
|
-
gitHubAuthor: rawRepo.gitHubAuthor,
|
|
170
|
-
localSha: dottie.get(rawRepo, 'localGit.sha'),
|
|
171
|
-
localLernaPackageName: rawRepo.localLernaPackageName,
|
|
172
|
-
gitHubLernaPackageName: rawRepo.gitHubLernaPackageName,
|
|
173
|
-
githubPackageJsonDescription: dottie.get(rawRepo, 'gitHubPackageJson.description')
|
|
174
|
-
}
|
|
42
|
+
const remoteRepos = await discover(this.graphql, this.gitHubOrgName)
|
|
175
43
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
} else if (!rep.localSha) {
|
|
181
|
-
rep.syncAction = 'CLONE'
|
|
182
|
-
applyDeps(processed.gitHubDeps, rawRepo.gitHubPackageJson)
|
|
183
|
-
applyNpmPackageName(rawRepo.gitHubPackageJson)
|
|
184
|
-
processed.actions.push(
|
|
185
|
-
{
|
|
186
|
-
type: 'CLONE',
|
|
187
|
-
config: {
|
|
188
|
-
packageName: rep.gitHubLernaPackageName,
|
|
189
|
-
repoName
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
)
|
|
193
|
-
} else {
|
|
194
|
-
rep.syncAction = 'PULL'
|
|
195
|
-
applyDeps(processed.gitHubDeps, rawRepo.gitHubPackageJson)
|
|
196
|
-
applyNpmPackageName(rawRepo.gitHubPackageJson)
|
|
197
|
-
processed.actions.push(
|
|
198
|
-
{
|
|
199
|
-
type: 'PULL',
|
|
200
|
-
config: {
|
|
201
|
-
packageName: rep.localLernaPackageName,
|
|
202
|
-
repoName
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
pkgInfo.repos[repoName] = rep
|
|
208
|
-
processed.totalPackageCount++
|
|
209
|
-
} else {
|
|
210
|
-
processed.rejectedCount++
|
|
211
|
-
}
|
|
212
|
-
}
|
|
44
|
+
// 2. Inspect (local)
|
|
45
|
+
// For each folder that already exists, read its
|
|
46
|
+
// current branch, whether it has uncommitted changes,
|
|
47
|
+
// its HEAD SHA and its folder name.
|
|
213
48
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
49
|
+
const localRepos = await inspect.inspectLocalRepos(
|
|
50
|
+
this.monorepoPath,
|
|
51
|
+
this.lernaJson.packages,
|
|
52
|
+
{ concurrency: this.concurrency }
|
|
217
53
|
)
|
|
218
|
-
processed.newDependenciesWithoutLinkables = _.difference(
|
|
219
|
-
processed.newDependencies,
|
|
220
|
-
processed.allNpmPackageNames
|
|
221
|
-
)
|
|
222
|
-
return processed
|
|
223
|
-
}
|
|
224
54
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (syncAction && syncAction !== 'SKIP') {
|
|
246
|
-
let repoLine = ` /${chalk.white(chalk.bold(repoName))} `
|
|
247
|
-
|
|
248
|
-
if (syncAction === 'CLONE') {
|
|
249
|
-
repoLine += chalk.green(chalk.inverse('Clone'))
|
|
250
|
-
} else {
|
|
251
|
-
repoLine += chalk.cyan(chalk.inverse('Pull'))
|
|
252
|
-
}
|
|
253
|
-
console.log(repoLine)
|
|
254
|
-
|
|
255
|
-
if (syncAction === 'CLONE') {
|
|
256
|
-
console.log(` ${chalk.green(repoInfo.githubPackageJsonDescription)}`)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
let agoLabel
|
|
260
|
-
const rawAuthorDate = dottie.get(repoInfo, 'gitHubAuthor.date')
|
|
261
|
-
if (rawAuthorDate) {
|
|
262
|
-
const authorDate = new Date(rawAuthorDate)
|
|
263
|
-
agoLabel = humanizeDuration(
|
|
264
|
-
new Date() - authorDate,
|
|
265
|
-
{
|
|
266
|
-
largest: 1
|
|
267
|
-
}
|
|
268
|
-
)
|
|
269
|
-
agoLabel = `{${agoLabel} ago)`
|
|
270
|
-
}
|
|
271
|
-
console.log(` ${chalk.yellow(repoInfo.gitHubAuthor.name + ':')} ${chalk.gray(agoLabel)}`)
|
|
272
|
-
console.log(chalk.magenta(` ${repoInfo.gitHubMessageHeadline}`))
|
|
273
|
-
} else {
|
|
274
|
-
sameCount++
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
let sameLine = chalk.gray(' OK count: ')
|
|
278
|
-
sameLine += chalk.green(sameCount)
|
|
279
|
-
console.log(sameLine)
|
|
55
|
+
// 3. Plan (pure function)
|
|
56
|
+
// Take `(remoteRepos, localRepos, router)` and
|
|
57
|
+
// return a list of decisions, each with a reason.
|
|
58
|
+
// This stage does no I/O.
|
|
59
|
+
|
|
60
|
+
const plan = planSync.planSync({
|
|
61
|
+
remoteRepos,
|
|
62
|
+
localRepos,
|
|
63
|
+
router: this.lernaPackageRouterFunction,
|
|
64
|
+
orgName: this.gitHubOrgName,
|
|
65
|
+
packageFolders
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// 4. Execute and report
|
|
69
|
+
// Run the decisions, then print a summary
|
|
70
|
+
// grouped by outcome.
|
|
71
|
+
|
|
72
|
+
if (this.dryRun) {
|
|
73
|
+
printPlan(plan, { verbose: this.verbose })
|
|
74
|
+
return { plan, results: [], errorCount: 0, attentionCount: 0 }
|
|
280
75
|
}
|
|
281
|
-
}
|
|
282
76
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
)
|
|
77
|
+
const results = await executePlan(plan, {
|
|
78
|
+
monorepoPath: this.monorepoPath,
|
|
79
|
+
gitHubOrgName: this.gitHubOrgName,
|
|
80
|
+
concurrency: this.concurrency,
|
|
81
|
+
cloneUrl: this.cloneUrl,
|
|
82
|
+
updateLocalDefaultBranch: this.updateLocalDefaultBranch,
|
|
83
|
+
onResult: r => {
|
|
84
|
+
const { label, colour, detail } = describeResult(r)
|
|
85
|
+
const name = `${r.decision.folder}/${r.decision.repoName}`
|
|
86
|
+
console.log(` ${colour(label.padEnd(11))} ${name}${detail ? chalk.gray(` (${detail})`) : ''}`)
|
|
295
87
|
}
|
|
296
|
-
}
|
|
297
|
-
console.log(chalk.red('Oh no! You need to \'lerna bootstrap\', because these are unheard of:'))
|
|
298
|
-
newDependenciesWithoutLinkables.forEach(
|
|
299
|
-
(packageName) => {
|
|
300
|
-
console.log(chalk.red(` - ${packageName}`))
|
|
301
|
-
}
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
88
|
+
})
|
|
305
89
|
|
|
306
|
-
|
|
307
|
-
console.log(
|
|
308
|
-
if (actions.length > 0) {
|
|
309
|
-
console.log(chalk.underline(`Identified ${actions.length} action${actions.length === 1 ? '' : 's'} to run`))
|
|
310
|
-
console.log('')
|
|
311
|
-
// let idx = 0
|
|
312
|
-
// const totalActionCount = actions.length
|
|
313
|
-
for (const action of actions) {
|
|
314
|
-
await actionFunctions[action.type](action, this.options, messages)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
console.log('')
|
|
318
|
-
}
|
|
90
|
+
const { errorCount, attentionCount } = report(plan, results, { verbose: this.verbose })
|
|
91
|
+
console.log(`\nDone (${humanizeDuration(Date.now() - start, { largest: 2, round: true })}).`)
|
|
319
92
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
this.repos = {}
|
|
324
|
-
await this.discoverGithubRepos()
|
|
325
|
-
this.collectLocalGitInfo()
|
|
326
|
-
this.processed = this.process()
|
|
327
|
-
if (this.processed.actions.length > 0) {
|
|
328
|
-
await this.performActions(
|
|
329
|
-
this.processed.actions,
|
|
330
|
-
messages
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
this.outputSummary(this.processed)
|
|
334
|
-
messages.output()
|
|
335
|
-
|
|
336
|
-
const localNonMasterBranches = this.processed.flattenedPackages
|
|
337
|
-
.map(({ repoName, pkgName }) => {
|
|
338
|
-
const destPath = path.join(this.options.monorepoPath, pkgName, repoName)
|
|
339
|
-
const gitHeadPath = path.join(destPath, '.git', 'HEAD')
|
|
340
|
-
|
|
341
|
-
if (fs.existsSync(destPath)) {
|
|
342
|
-
if (fs.existsSync(gitHeadPath)) {
|
|
343
|
-
const currentBranch = fs.readFileSync(gitHeadPath, 'utf-8').trim().split('refs/heads/')[1]
|
|
344
|
-
|
|
345
|
-
if (currentBranch !== 'master') {
|
|
346
|
-
return { repoName, pkgName, currentBranch }
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return null
|
|
352
|
-
})
|
|
353
|
-
.filter(p => p)
|
|
354
|
-
|
|
355
|
-
if (localNonMasterBranches.length > 0) {
|
|
356
|
-
console.log(chalk.bgYellow.gray('\n\n ----- Found some non-master branches, best double check that you should be on these! ----- '))
|
|
357
|
-
localNonMasterBranches.forEach(branch => {
|
|
358
|
-
console.log(chalk.yellow.underline(`${branch.repoName}:`))
|
|
359
|
-
console.log(chalk.yellow(`Branch name: ${branch.currentBranch}\n`))
|
|
360
|
-
})
|
|
361
|
-
}
|
|
93
|
+
return { plan, results, errorCount, attentionCount }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
362
96
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
console.log('')
|
|
369
|
-
console.log(`Done (${humanizeDuration(this.syncEnd - this.syncStart)}).`)
|
|
97
|
+
// undefined/null means "use each stage's default"; anything else must be a positive integer
|
|
98
|
+
function validateConcurrency (value) {
|
|
99
|
+
if (value == null) return undefined
|
|
100
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
101
|
+
throw new TypeError(`concurrency must be a positive integer, got ${JSON.stringify(value)}`)
|
|
370
102
|
}
|
|
103
|
+
return value
|
|
371
104
|
}
|
|
372
105
|
|
|
373
106
|
module.exports = LernaSync
|
package/lib/inspect.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { globSync } = require('glob')
|
|
4
|
+
const jsonfile = require('jsonfile')
|
|
5
|
+
const { tryGit } = require('./utils/git')
|
|
6
|
+
const mapLimit = require('./utils/map-limit')
|
|
7
|
+
|
|
8
|
+
async function inspectLocalRepos (monorepoPath, lernaPackagePatterns, { concurrency = 8 } = {}) {
|
|
9
|
+
const dirs = new Set()
|
|
10
|
+
|
|
11
|
+
for (const pattern of lernaPackagePatterns) {
|
|
12
|
+
// A trailing slash makes glob match directories only
|
|
13
|
+
const matches = globSync(`${pattern.replace(/\/+$/, '')}/`, { cwd: monorepoPath, absolute: true })
|
|
14
|
+
matches.forEach(m => dirs.add(path.resolve(m)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return mapLimit([...dirs].sort(), concurrency, dir => inspectRepo(monorepoPath, dir))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function inspectRepo (monorepoPath, repoPath) {
|
|
21
|
+
const record = {
|
|
22
|
+
path: repoPath,
|
|
23
|
+
repoName: path.basename(repoPath),
|
|
24
|
+
folder: path.relative(monorepoPath, path.dirname(repoPath)).split(path.sep).join('/'),
|
|
25
|
+
isGitRepo: false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Each package folder sits inside the husk's own repo, so in a folder that
|
|
29
|
+
// isn't a repo of its own, git would happily answer for the husk instead.
|
|
30
|
+
// Only trust the result if this folder is the top level.
|
|
31
|
+
const toplevel = await tryGit(['rev-parse', '--show-toplevel'], repoPath)
|
|
32
|
+
if (!toplevel || !samePath(toplevel, repoPath)) {
|
|
33
|
+
return record
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [
|
|
37
|
+
branch,
|
|
38
|
+
headSha,
|
|
39
|
+
status,
|
|
40
|
+
originUrl,
|
|
41
|
+
gitPaths
|
|
42
|
+
] = await Promise.all([
|
|
43
|
+
tryGit(['symbolic-ref', '--quiet', '--short', 'HEAD'], repoPath),
|
|
44
|
+
tryGit(['rev-parse', '--verify', '--quiet', 'HEAD'], repoPath),
|
|
45
|
+
tryGit(['status', '--porcelain'], repoPath),
|
|
46
|
+
tryGit(['remote', 'get-url', 'origin'], repoPath),
|
|
47
|
+
tryGit(['rev-parse', '--git-path', 'MERGE_HEAD', '--git-path', 'rebase-merge', '--git-path', 'rebase-apply'], repoPath)
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
const statusLines = status === null ? null : status.split('\n').filter(Boolean)
|
|
51
|
+
|
|
52
|
+
const operationInProgress = (gitPaths || '')
|
|
53
|
+
.split('\n')
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.some(p => fs.existsSync(path.resolve(repoPath, p)))
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
...record,
|
|
59
|
+
isGitRepo: true,
|
|
60
|
+
branch, // null when HEAD is detached
|
|
61
|
+
isDetached: branch === null,
|
|
62
|
+
headSha, // null in a repo with no commits yet
|
|
63
|
+
statusKnown: statusLines !== null,
|
|
64
|
+
trackedChanges: statusLines ? statusLines.filter(l => !l.startsWith('??')).length : 0,
|
|
65
|
+
untrackedFiles: statusLines ? statusLines.filter(l => l.startsWith('??')).length : 0,
|
|
66
|
+
operationInProgress, // mid-merge or mid-rebase
|
|
67
|
+
origin: parseGitHubRemote(originUrl),
|
|
68
|
+
packageJson: readJsonOrNull(path.join(repoPath, 'package.json'))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function samePath (a, b) {
|
|
73
|
+
const norm = p => {
|
|
74
|
+
const real = path.resolve(fs.realpathSync.native(p))
|
|
75
|
+
return process.platform === 'win32' ? real.toLowerCase() : real
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return norm(a) === norm(b)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseGitHubRemote (url) {
|
|
85
|
+
if (!url) return null
|
|
86
|
+
// https://github.com/wmfs/repo(.git) or git@github.com:wmfs/repo(.git)
|
|
87
|
+
const m = url.match(/github\.com[/:]([^/]+)\/(.+?)(?:\.git)?\/?$/i)
|
|
88
|
+
return m ? { owner: m[1], name: m[2], url } : { owner: null, name: null, url }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJsonOrNull (file) {
|
|
92
|
+
try {
|
|
93
|
+
return jsonfile.readFileSync(file)
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { inspectLocalRepos, inspectRepo }
|