@wmfs/lerna-sync 1.37.1 → 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 +29 -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 +8 -8
- package/renovate.json +4 -0
- 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 -49
- 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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* eslint-env mocha */
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { expect } = require('chai')
|
|
6
|
+
const { Sandbox, git } = require('./helpers/git-sandbox')
|
|
7
|
+
const { executePlan, OUTCOMES } = require('../lib/execute-plan')
|
|
8
|
+
const { ACTIONS, REASONS } = require('../lib/plan-sync')
|
|
9
|
+
|
|
10
|
+
describe('executePlan (real git)', function () {
|
|
11
|
+
this.timeout(30000)
|
|
12
|
+
|
|
13
|
+
let sandbox, mono, origin, options
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
sandbox = new Sandbox()
|
|
17
|
+
mono = sandbox.dir('mono')
|
|
18
|
+
origin = sandbox.createOrigin('tymly-test-plugin')
|
|
19
|
+
options = {
|
|
20
|
+
monorepoPath: mono,
|
|
21
|
+
gitHubOrgName: 'wmfs',
|
|
22
|
+
cloneUrl: (org, name) => path.join(sandbox.root, 'remotes', org, `${name}.git`)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => sandbox.cleanup())
|
|
27
|
+
|
|
28
|
+
const decision = (action, reason, repoPath, extra = {}) => ({
|
|
29
|
+
repoName: origin.name,
|
|
30
|
+
action,
|
|
31
|
+
reason,
|
|
32
|
+
folder: 'plugins',
|
|
33
|
+
defaultBranch: 'master',
|
|
34
|
+
remote: { name: origin.name },
|
|
35
|
+
local: repoPath ? { path: repoPath, branch: git(repoPath, 'rev-parse', '--abbrev-ref', 'HEAD') } : null,
|
|
36
|
+
warnings: [],
|
|
37
|
+
...extra
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const runOne = async d => {
|
|
41
|
+
const [result] = await executePlan({ decisions: [d], ignored: [] }, options)
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const head = (repo, ref = 'HEAD') => git(repo, 'rev-parse', ref)
|
|
46
|
+
const originHead = () => git(origin.bare, 'rev-parse', 'master')
|
|
47
|
+
|
|
48
|
+
describe('CLONE', () => {
|
|
49
|
+
it('clones into <folder>/<repo name>', async () => {
|
|
50
|
+
const result = await runOne(decision(ACTIONS.CLONE, REASONS.NEW, null))
|
|
51
|
+
|
|
52
|
+
const dest = path.join(mono, 'plugins', origin.name)
|
|
53
|
+
expect(result.outcome).to.equal(OUTCOMES.CLONED)
|
|
54
|
+
expect(head(dest)).to.equal(originHead())
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('reports a failure without throwing', async () => {
|
|
58
|
+
options.cloneUrl = () => path.join(sandbox.root, 'does-not-exist.git')
|
|
59
|
+
const result = await runOne(decision(ACTIONS.CLONE, REASONS.NEW, null))
|
|
60
|
+
|
|
61
|
+
expect(result.outcome).to.equal(OUTCOMES.FAILED)
|
|
62
|
+
expect(result.error).to.be.a('string').and.not.equal('')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('FAST_FORWARD', () => {
|
|
67
|
+
it('pulls new commits on a clean default branch', async () => {
|
|
68
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
69
|
+
origin.push('a.txt', 'a', 'First new commit')
|
|
70
|
+
origin.push('b.txt', 'b', 'Second new commit')
|
|
71
|
+
|
|
72
|
+
const result = await runOne(decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, repo))
|
|
73
|
+
|
|
74
|
+
expect(result).to.include({ outcome: OUTCOMES.UPDATED, ahead: 0, behind: 2, packageJsonChanged: false })
|
|
75
|
+
expect(result.commits.map(c => c.subject)).to.deep.equal(['Second new commit', 'First new commit'])
|
|
76
|
+
expect(head(repo)).to.equal(originHead())
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('flags when package.json changed', async () => {
|
|
80
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
81
|
+
origin.push('package.json', '{"name":"changed"}', 'Bump deps')
|
|
82
|
+
|
|
83
|
+
const result = await runOne(decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, repo))
|
|
84
|
+
expect(result.packageJsonChanged).to.equal(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('reports current when there is nothing new', async () => {
|
|
88
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
89
|
+
const result = await runOne(decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, repo))
|
|
90
|
+
expect(result).to.include({ outcome: OUTCOMES.CURRENT, ahead: 0, behind: 0 })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('reports unpushed commits and leaves them alone', async () => {
|
|
94
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
95
|
+
const local = sandbox.commit(repo, 'mine.txt', 'mine', 'Not pushed yet')
|
|
96
|
+
|
|
97
|
+
const result = await runOne(decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, repo))
|
|
98
|
+
expect(result).to.include({ outcome: OUTCOMES.LOCAL_AHEAD, ahead: 1, behind: 0 })
|
|
99
|
+
expect(head(repo)).to.equal(local)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('does not merge when the default branch has diverged', async () => {
|
|
103
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
104
|
+
const local = sandbox.commit(repo, 'mine.txt', 'mine', 'Local work')
|
|
105
|
+
origin.push('theirs.txt', 'theirs', 'Remote work')
|
|
106
|
+
|
|
107
|
+
const result = await runOne(decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, repo))
|
|
108
|
+
expect(result).to.include({ outcome: OUTCOMES.DIVERGED, ahead: 1, behind: 1 })
|
|
109
|
+
expect(result.commits.map(c => c.subject)).to.deep.equal(['Remote work'])
|
|
110
|
+
expect(head(repo)).to.equal(local)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('does not overwrite an untracked file that is now tracked upstream', async () => {
|
|
114
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
115
|
+
origin.push('notes.txt', 'from upstream', 'Add notes')
|
|
116
|
+
sandbox.write(repo, 'notes.txt', 'my local notes')
|
|
117
|
+
const before = head(repo)
|
|
118
|
+
|
|
119
|
+
const result = await runOne(decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, repo))
|
|
120
|
+
expect(result.outcome).to.equal(OUTCOMES.BLOCKED_BY_UNTRACKED)
|
|
121
|
+
expect(head(repo)).to.equal(before)
|
|
122
|
+
expect(fs.readFileSync(path.join(repo, 'notes.txt'), 'utf8')).to.equal('my local notes')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('FETCH_ONLY', () => {
|
|
127
|
+
it('leaves a feature branch alone and reports what the default branch has', async () => {
|
|
128
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
129
|
+
git(repo, 'checkout', '--quiet', '-b', 'feature/x')
|
|
130
|
+
const branchHead = sandbox.commit(repo, 'feature.txt', 'wip', 'Feature work')
|
|
131
|
+
origin.push('fix.txt', 'fix', 'Fix on master')
|
|
132
|
+
|
|
133
|
+
const result = await runOne(decision(ACTIONS.FETCH_ONLY, REASONS.ON_BRANCH, repo))
|
|
134
|
+
|
|
135
|
+
expect(result).to.include({ outcome: OUTCOMES.BEHIND, ahead: 1, behind: 1 })
|
|
136
|
+
expect(result.commits.map(c => c.subject)).to.deep.equal(['Fix on master'])
|
|
137
|
+
expect(git(repo, 'rev-parse', '--abbrev-ref', 'HEAD')).to.equal('feature/x')
|
|
138
|
+
expect(head(repo)).to.equal(branchHead)
|
|
139
|
+
expect(fs.existsSync(path.join(repo, 'fix.txt'))).to.equal(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('fast-forwards the local default branch without checking it out', async () => {
|
|
143
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
144
|
+
git(repo, 'checkout', '--quiet', '-b', 'feature/x')
|
|
145
|
+
origin.push('fix.txt', 'fix', 'Fix on master')
|
|
146
|
+
|
|
147
|
+
await runOne(decision(ACTIONS.FETCH_ONLY, REASONS.ON_BRANCH, repo))
|
|
148
|
+
expect(head(repo, 'master')).to.equal(originHead())
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('does not move a local default branch that has its own commits', async () => {
|
|
152
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
153
|
+
const localMaster = sandbox.commit(repo, 'mine.txt', 'mine', 'Unpushed on master')
|
|
154
|
+
git(repo, 'checkout', '--quiet', '-b', 'feature/x')
|
|
155
|
+
origin.push('fix.txt', 'fix', 'Fix on master')
|
|
156
|
+
|
|
157
|
+
await runOne(decision(ACTIONS.FETCH_ONLY, REASONS.ON_BRANCH, repo))
|
|
158
|
+
expect(head(repo, 'master')).to.equal(localMaster)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('respects updateLocalDefaultBranch: false', async () => {
|
|
162
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
163
|
+
const before = head(repo, 'master')
|
|
164
|
+
git(repo, 'checkout', '--quiet', '-b', 'feature/x')
|
|
165
|
+
origin.push('fix.txt', 'fix', 'Fix on master')
|
|
166
|
+
|
|
167
|
+
options.updateLocalDefaultBranch = false
|
|
168
|
+
await runOne(decision(ACTIONS.FETCH_ONLY, REASONS.ON_BRANCH, repo))
|
|
169
|
+
expect(head(repo, 'master')).to.equal(before)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('reports current when the default branch has nothing new', async () => {
|
|
173
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
174
|
+
git(repo, 'checkout', '--quiet', '-b', 'feature/x')
|
|
175
|
+
|
|
176
|
+
const result = await runOne(decision(ACTIONS.FETCH_ONLY, REASONS.ON_BRANCH, repo))
|
|
177
|
+
expect(result).to.include({ outcome: OUTCOMES.CURRENT, behind: 0 })
|
|
178
|
+
expect(result.commits).to.deep.equal([])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('does not touch a checked-out default branch with uncommitted changes', async () => {
|
|
182
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
183
|
+
const before = head(repo)
|
|
184
|
+
sandbox.write(repo, 'package.json', '{"editing": true}')
|
|
185
|
+
origin.push('fix.txt', 'fix', 'Fix on master')
|
|
186
|
+
|
|
187
|
+
const result = await runOne(decision(ACTIONS.FETCH_ONLY, REASONS.LOCAL_CHANGES, repo))
|
|
188
|
+
expect(result).to.include({ outcome: OUTCOMES.BEHIND, behind: 1 })
|
|
189
|
+
expect(head(repo)).to.equal(before)
|
|
190
|
+
expect(fs.readFileSync(path.join(repo, 'package.json'), 'utf8')).to.equal('{"editing": true}')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('handles a detached HEAD', async () => {
|
|
194
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
195
|
+
git(repo, 'checkout', '--quiet', '--detach')
|
|
196
|
+
origin.push('fix.txt', 'fix', 'Fix on master')
|
|
197
|
+
|
|
198
|
+
const result = await runOne(decision(ACTIONS.FETCH_ONLY, REASONS.DETACHED, repo))
|
|
199
|
+
expect(result).to.include({ outcome: OUTCOMES.BEHIND, behind: 1 })
|
|
200
|
+
expect(head(repo, 'master')).to.equal(originHead())
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('running a whole plan', () => {
|
|
205
|
+
it('skips NONE decisions and isolates failures', async () => {
|
|
206
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins')
|
|
207
|
+
origin.push('a.txt', 'a', 'New commit')
|
|
208
|
+
|
|
209
|
+
const good = decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, repo)
|
|
210
|
+
const bad = decision(ACTIONS.FAST_FORWARD, REASONS.BEHIND, null, {
|
|
211
|
+
repoName: 'missing',
|
|
212
|
+
local: { path: path.join(mono, 'plugins', 'missing'), branch: 'master' }
|
|
213
|
+
})
|
|
214
|
+
const none = decision(ACTIONS.NONE, REASONS.ORPHANED, null, { repoName: 'gone' })
|
|
215
|
+
|
|
216
|
+
const seen = []
|
|
217
|
+
options.onResult = r => seen.push(r.decision.repoName)
|
|
218
|
+
const results = await executePlan({ decisions: [good, bad, none], ignored: [] }, options)
|
|
219
|
+
|
|
220
|
+
expect(results.map(r => [r.decision.repoName, r.outcome])).to.deep.equal([
|
|
221
|
+
[origin.name, OUTCOMES.UPDATED],
|
|
222
|
+
['missing', OUTCOMES.FAILED]
|
|
223
|
+
])
|
|
224
|
+
expect(seen).to.have.members([origin.name, 'missing'])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('returns the same decision objects it was given', async () => {
|
|
228
|
+
const d = decision(ACTIONS.CLONE, REASONS.NEW, null)
|
|
229
|
+
const [result] = await executePlan({ decisions: [d], ignored: [] }, options)
|
|
230
|
+
expect(result.decision).to.equal(d)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Builders for the plain records each stage passes to the next.
|
|
2
|
+
// Defaults describe the "boring" case; tests override only what they care about.
|
|
3
|
+
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
|
|
6
|
+
// Keep assertions on output free of colour codes
|
|
7
|
+
chalk.level = 0
|
|
8
|
+
|
|
9
|
+
const router = pkg => {
|
|
10
|
+
const keywords = pkg.keywords || []
|
|
11
|
+
if (!keywords.includes('tymly')) return null
|
|
12
|
+
if (pkg.config && pkg.config.tymly && pkg.config.tymly.sync === false) {
|
|
13
|
+
return { skip: 'Opted out (config.tymly.sync = false)' }
|
|
14
|
+
}
|
|
15
|
+
if (keywords.includes('blueprint')) return 'blueprints'
|
|
16
|
+
if (keywords.includes('plugin')) return 'plugins'
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function remote (name, overrides = {}) {
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
isPrivate: false,
|
|
24
|
+
defaultBranch: 'master',
|
|
25
|
+
sha: 'remote-sha',
|
|
26
|
+
messageHeadline: 'Latest commit',
|
|
27
|
+
author: { name: 'Someone', date: '2026-01-01T00:00:00Z' },
|
|
28
|
+
packageJson: { name: `@wmfs/${name}`, description: `${name} description`, keywords: ['tymly', 'plugin'] },
|
|
29
|
+
...overrides
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function local (name, overrides = {}) {
|
|
34
|
+
return {
|
|
35
|
+
path: `/mono/plugins/${name}`,
|
|
36
|
+
repoName: name,
|
|
37
|
+
folder: 'plugins',
|
|
38
|
+
isGitRepo: true,
|
|
39
|
+
branch: 'master',
|
|
40
|
+
isDetached: false,
|
|
41
|
+
headSha: 'local-sha',
|
|
42
|
+
statusKnown: true,
|
|
43
|
+
trackedChanges: 0,
|
|
44
|
+
untrackedFiles: 0,
|
|
45
|
+
operationInProgress: false,
|
|
46
|
+
origin: { owner: 'wmfs', name, url: `https://github.com/wmfs/${name}.git` },
|
|
47
|
+
packageJson: null,
|
|
48
|
+
...overrides
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function captureConsole () {
|
|
53
|
+
const lines = []
|
|
54
|
+
const original = { log: console.log, warn: console.warn }
|
|
55
|
+
console.log = (...args) => lines.push(args.join(' '))
|
|
56
|
+
console.warn = (...args) => lines.push(args.join(' '))
|
|
57
|
+
return {
|
|
58
|
+
lines,
|
|
59
|
+
text: () => lines.join('\n'),
|
|
60
|
+
restore: () => Object.assign(console, original)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { router, remote, local, captureConsole }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Throwaway git repositories in a temp directory, for tests that need real git.
|
|
2
|
+
// No network: "origins" are local bare repos.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const { execFileSync } = require('child_process')
|
|
8
|
+
|
|
9
|
+
// Isolate from the developer's own git config (signing, hooks, default branch, ...).
|
|
10
|
+
// lib/utils/git.js inherits process.env, so this applies to the code under test too.
|
|
11
|
+
process.env.GIT_CONFIG_GLOBAL = os.devNull
|
|
12
|
+
process.env.GIT_CONFIG_NOSYSTEM = '1'
|
|
13
|
+
process.env.GIT_AUTHOR_NAME = 'Test Author'
|
|
14
|
+
process.env.GIT_AUTHOR_EMAIL = 'author@example.com'
|
|
15
|
+
process.env.GIT_COMMITTER_NAME = 'Test Author'
|
|
16
|
+
process.env.GIT_COMMITTER_EMAIL = 'author@example.com'
|
|
17
|
+
process.env.GIT_TERMINAL_PROMPT = '0'
|
|
18
|
+
|
|
19
|
+
function git (cwd, ...args) {
|
|
20
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tryGit (cwd, ...args) {
|
|
24
|
+
try {
|
|
25
|
+
return git(cwd, ...args)
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class Sandbox {
|
|
32
|
+
constructor () {
|
|
33
|
+
this.root = fs.mkdtempSync(path.join(os.tmpdir(), 'lerna-sync-test-'))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
dir (...parts) {
|
|
37
|
+
const p = path.join(this.root, ...parts)
|
|
38
|
+
fs.mkdirSync(p, { recursive: true })
|
|
39
|
+
return p
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
write (repoPath, file, content) {
|
|
43
|
+
const full = path.join(repoPath, file)
|
|
44
|
+
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
45
|
+
fs.writeFileSync(full, content)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
commit (repoPath, file, content, message = `Change ${file}`) {
|
|
49
|
+
this.write(repoPath, file, content)
|
|
50
|
+
git(repoPath, 'add', file)
|
|
51
|
+
git(repoPath, 'commit', '--quiet', '-m', message)
|
|
52
|
+
return git(repoPath, 'rev-parse', 'HEAD')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A bare "GitHub" repo plus a working clone used to push new commits to it
|
|
56
|
+
createOrigin (name) {
|
|
57
|
+
const bare = this.dir('remotes', 'wmfs', `${name}.git`)
|
|
58
|
+
git(bare, 'init', '--quiet', '--bare', '-b', 'master')
|
|
59
|
+
|
|
60
|
+
const pusher = path.join(this.root, 'pushers', name)
|
|
61
|
+
git(this.root, 'clone', '--quiet', bare, pusher)
|
|
62
|
+
git(pusher, 'symbolic-ref', 'HEAD', 'refs/heads/master')
|
|
63
|
+
this.commit(pusher, 'package.json', JSON.stringify({ name, keywords: ['tymly', 'plugin'] }, null, 2), 'Initial commit')
|
|
64
|
+
git(pusher, 'push', '--quiet', 'origin', 'master')
|
|
65
|
+
|
|
66
|
+
const push = (file, content, message) => {
|
|
67
|
+
const sha = this.commit(pusher, file, content, message)
|
|
68
|
+
git(pusher, 'push', '--quiet', 'origin', 'master')
|
|
69
|
+
return sha
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { name, bare, pusher, push }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A developer's clone of an origin, inside the monorepo husk
|
|
76
|
+
cloneInto (origin, ...folderParts) {
|
|
77
|
+
const dest = path.join(this.dir(...folderParts), origin.name)
|
|
78
|
+
git(this.root, 'clone', '--quiet', origin.bare, dest)
|
|
79
|
+
return dest
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
cleanup () {
|
|
83
|
+
fs.rmSync(this.root, { recursive: true, force: true })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { Sandbox, git, tryGit }
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/* eslint-env mocha */
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { expect } = require('chai')
|
|
6
|
+
const { Sandbox, git, tryGit } = require('./helpers/git-sandbox')
|
|
7
|
+
const { inspectLocalRepos, inspectRepo } = require('../lib/inspect')
|
|
8
|
+
|
|
9
|
+
describe('inspect (real git)', function () {
|
|
10
|
+
this.timeout(30000)
|
|
11
|
+
|
|
12
|
+
let sandbox, mono, origin
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
sandbox = new Sandbox()
|
|
16
|
+
origin = sandbox.createOrigin('tymly-test-plugin')
|
|
17
|
+
|
|
18
|
+
// The husk is itself a git repo, as the real tymly monorepo is
|
|
19
|
+
mono = sandbox.dir('mono')
|
|
20
|
+
git(mono, 'init', '--quiet', '-b', 'master')
|
|
21
|
+
sandbox.dir('mono', 'plugins')
|
|
22
|
+
sandbox.dir('mono', 'blueprints')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => sandbox.cleanup())
|
|
26
|
+
|
|
27
|
+
const cloneAndPointAtGitHub = (...folder) => {
|
|
28
|
+
const repo = sandbox.cloneInto(origin, 'mono', ...folder)
|
|
29
|
+
git(repo, 'remote', 'set-url', 'origin', `https://github.com/wmfs/${origin.name}.git`)
|
|
30
|
+
return repo
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it('finds one record per folder matched by the lerna patterns', async () => {
|
|
34
|
+
cloneAndPointAtGitHub('plugins')
|
|
35
|
+
sandbox.dir('mono', 'blueprints', 'scratch')
|
|
36
|
+
fs.writeFileSync(path.join(mono, 'plugins', 'README.md'), 'a file, not a package folder')
|
|
37
|
+
|
|
38
|
+
const records = await inspectLocalRepos(mono, ['plugins/*', 'blueprints/*'])
|
|
39
|
+
|
|
40
|
+
expect(records.map(r => `${r.folder}/${r.repoName}`))
|
|
41
|
+
.to.deep.equal(['blueprints/scratch', `plugins/${origin.name}`])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns nothing for empty package folders', async () => {
|
|
45
|
+
expect(await inspectLocalRepos(mono, ['plugins/*', 'blueprints/*'])).to.deep.equal([])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reads a clean clone on the default branch', async () => {
|
|
49
|
+
const repo = cloneAndPointAtGitHub('plugins')
|
|
50
|
+
const record = await inspectRepo(mono, repo)
|
|
51
|
+
|
|
52
|
+
expect(record).to.include({
|
|
53
|
+
repoName: origin.name,
|
|
54
|
+
folder: 'plugins',
|
|
55
|
+
isGitRepo: true,
|
|
56
|
+
branch: 'master',
|
|
57
|
+
isDetached: false,
|
|
58
|
+
headSha: git(repo, 'rev-parse', 'HEAD'),
|
|
59
|
+
statusKnown: true,
|
|
60
|
+
trackedChanges: 0,
|
|
61
|
+
untrackedFiles: 0,
|
|
62
|
+
operationInProgress: false
|
|
63
|
+
})
|
|
64
|
+
expect(record.origin).to.include({ owner: 'wmfs', name: origin.name })
|
|
65
|
+
expect(record.packageJson.name).to.equal(origin.name)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('does not mistake a plain folder for a repo, even inside the husk\'s repo', async () => {
|
|
69
|
+
const plain = sandbox.dir('mono', 'plugins', 'not-a-repo')
|
|
70
|
+
const record = await inspectRepo(mono, plain)
|
|
71
|
+
|
|
72
|
+
expect(record).to.deep.equal({
|
|
73
|
+
path: plain,
|
|
74
|
+
repoName: 'not-a-repo',
|
|
75
|
+
folder: 'plugins',
|
|
76
|
+
isGitRepo: false
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('reports the current feature branch', async () => {
|
|
81
|
+
const repo = cloneAndPointAtGitHub('plugins')
|
|
82
|
+
git(repo, 'checkout', '--quiet', '-b', 'feature/new-thing')
|
|
83
|
+
|
|
84
|
+
const record = await inspectRepo(mono, repo)
|
|
85
|
+
expect(record).to.include({ branch: 'feature/new-thing', isDetached: false })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('reports a detached HEAD', async () => {
|
|
89
|
+
const repo = cloneAndPointAtGitHub('plugins')
|
|
90
|
+
git(repo, 'checkout', '--quiet', '--detach')
|
|
91
|
+
|
|
92
|
+
const record = await inspectRepo(mono, repo)
|
|
93
|
+
expect(record).to.include({ branch: null, isDetached: true })
|
|
94
|
+
expect(record.headSha).to.equal(git(repo, 'rev-parse', 'HEAD'))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('counts tracked changes and untracked files separately', async () => {
|
|
98
|
+
const repo = cloneAndPointAtGitHub('plugins')
|
|
99
|
+
sandbox.write(repo, 'package.json', '{"changed": true}')
|
|
100
|
+
sandbox.write(repo, 'new-1.txt', 'x')
|
|
101
|
+
sandbox.write(repo, 'new-2.txt', 'x')
|
|
102
|
+
|
|
103
|
+
const record = await inspectRepo(mono, repo)
|
|
104
|
+
expect(record).to.include({ trackedChanges: 1, untrackedFiles: 2 })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('detects a merge in progress', async () => {
|
|
108
|
+
const repo = cloneAndPointAtGitHub('plugins')
|
|
109
|
+
git(repo, 'checkout', '--quiet', '-b', 'other')
|
|
110
|
+
sandbox.commit(repo, 'conflict.txt', 'theirs')
|
|
111
|
+
git(repo, 'checkout', '--quiet', 'master')
|
|
112
|
+
sandbox.commit(repo, 'conflict.txt', 'ours')
|
|
113
|
+
tryGit(repo, 'merge', 'other') // conflicts, leaving MERGE_HEAD
|
|
114
|
+
|
|
115
|
+
const record = await inspectRepo(mono, repo)
|
|
116
|
+
expect(record.operationInProgress).to.equal(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('handles a repo with no commits yet', async () => {
|
|
120
|
+
const repo = sandbox.dir('mono', 'plugins', 'brand-new')
|
|
121
|
+
git(repo, 'init', '--quiet', '-b', 'master')
|
|
122
|
+
|
|
123
|
+
const record = await inspectRepo(mono, repo)
|
|
124
|
+
expect(record).to.include({ isGitRepo: true, headSha: null, branch: 'master' })
|
|
125
|
+
expect(record.origin).to.equal(null)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('parses SSH origin URLs', async () => {
|
|
129
|
+
const repo = cloneAndPointAtGitHub('plugins')
|
|
130
|
+
git(repo, 'remote', 'set-url', 'origin', 'git@github.com:wmfs/renamed-repo.git')
|
|
131
|
+
|
|
132
|
+
const record = await inspectRepo(mono, repo)
|
|
133
|
+
expect(record.origin).to.include({ owner: 'wmfs', name: 'renamed-repo' })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('records a non-GitHub origin without an owner', async () => {
|
|
137
|
+
const repo = sandbox.cloneInto(origin, 'mono', 'plugins') // origin is a local path
|
|
138
|
+
|
|
139
|
+
const record = await inspectRepo(mono, repo)
|
|
140
|
+
expect(record.origin).to.include({ owner: null, name: null })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('reports nested package folders relative to the husk', async () => {
|
|
144
|
+
const repo = cloneAndPointAtGitHub('plugins', 'core')
|
|
145
|
+
const record = await inspectRepo(mono, repo)
|
|
146
|
+
expect(record.folder).to.equal('plugins/core')
|
|
147
|
+
})
|
|
148
|
+
})
|