@node-core/utils 4.0.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/LICENSE +7 -0
- package/README.md +158 -0
- package/bin/get-metadata.js +11 -0
- package/bin/git-node.js +30 -0
- package/bin/ncu-ci.js +600 -0
- package/bin/ncu-config.js +101 -0
- package/bin/ncu-team.js +76 -0
- package/components/git/backport.js +70 -0
- package/components/git/epilogue.js +18 -0
- package/components/git/land.js +223 -0
- package/components/git/metadata.js +94 -0
- package/components/git/release.js +99 -0
- package/components/git/security.js +35 -0
- package/components/git/status.js +32 -0
- package/components/git/sync.js +24 -0
- package/components/git/v8.js +121 -0
- package/components/git/vote.js +84 -0
- package/components/git/wpt.js +87 -0
- package/components/metadata.js +49 -0
- package/lib/auth.js +133 -0
- package/lib/backport_session.js +302 -0
- package/lib/cache.js +107 -0
- package/lib/cherry_pick.js +304 -0
- package/lib/ci/build-types/benchmark_run.js +72 -0
- package/lib/ci/build-types/citgm_build.js +194 -0
- package/lib/ci/build-types/citgm_comparison_build.js +174 -0
- package/lib/ci/build-types/commit_build.js +112 -0
- package/lib/ci/build-types/daily_build.js +24 -0
- package/lib/ci/build-types/fanned_build.js +87 -0
- package/lib/ci/build-types/health_build.js +63 -0
- package/lib/ci/build-types/job.js +114 -0
- package/lib/ci/build-types/linter_build.js +35 -0
- package/lib/ci/build-types/normal_build.js +89 -0
- package/lib/ci/build-types/pr_build.js +101 -0
- package/lib/ci/build-types/test_build.js +186 -0
- package/lib/ci/build-types/test_run.js +41 -0
- package/lib/ci/ci_failure_parser.js +325 -0
- package/lib/ci/ci_type_parser.js +203 -0
- package/lib/ci/ci_utils.js +106 -0
- package/lib/ci/failure_aggregator.js +152 -0
- package/lib/ci/jenkins_constants.js +28 -0
- package/lib/ci/run_ci.js +120 -0
- package/lib/cli.js +192 -0
- package/lib/collaborators.js +140 -0
- package/lib/config.js +72 -0
- package/lib/figures.js +7 -0
- package/lib/file.js +43 -0
- package/lib/github/templates/next-security-release.md +97 -0
- package/lib/github/tree.js +162 -0
- package/lib/landing_session.js +506 -0
- package/lib/links.js +123 -0
- package/lib/mergeable_state.js +3 -0
- package/lib/metadata_gen.js +61 -0
- package/lib/pr_checker.js +605 -0
- package/lib/pr_data.js +115 -0
- package/lib/pr_summary.js +62 -0
- package/lib/prepare_release.js +772 -0
- package/lib/prepare_security.js +117 -0
- package/lib/proxy.js +21 -0
- package/lib/queries/DefaultBranchRef.gql +8 -0
- package/lib/queries/LastCommit.gql +16 -0
- package/lib/queries/PR.gql +37 -0
- package/lib/queries/PRComments.gql +27 -0
- package/lib/queries/PRCommits.gql +45 -0
- package/lib/queries/PRs.gql +25 -0
- package/lib/queries/Reviews.gql +23 -0
- package/lib/queries/SearchIssue.gql +51 -0
- package/lib/queries/Team.gql +22 -0
- package/lib/queries/TreeEntries.gql +12 -0
- package/lib/queries/VotePRInfo.gql +28 -0
- package/lib/release/utils.js +53 -0
- package/lib/request.js +185 -0
- package/lib/review_state.js +5 -0
- package/lib/reviews.js +178 -0
- package/lib/run.js +106 -0
- package/lib/session.js +415 -0
- package/lib/sync_session.js +15 -0
- package/lib/team_info.js +95 -0
- package/lib/update-v8/applyNodeChanges.js +49 -0
- package/lib/update-v8/backport.js +258 -0
- package/lib/update-v8/commitUpdate.js +26 -0
- package/lib/update-v8/common.js +35 -0
- package/lib/update-v8/constants.js +86 -0
- package/lib/update-v8/index.js +56 -0
- package/lib/update-v8/majorUpdate.js +171 -0
- package/lib/update-v8/minorUpdate.js +105 -0
- package/lib/update-v8/updateMaintainingDependencies.js +34 -0
- package/lib/update-v8/updateV8Clone.js +53 -0
- package/lib/update-v8/updateVersionNumbers.js +122 -0
- package/lib/update-v8/util.js +62 -0
- package/lib/user.js +4 -0
- package/lib/user_status.js +5 -0
- package/lib/utils.js +66 -0
- package/lib/verbosity.js +26 -0
- package/lib/voting_session.js +136 -0
- package/lib/wpt/index.js +243 -0
- package/lib/wpt/templates/README.md +16 -0
- package/package.json +69 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
runAsync, runSync, forceRunAsync
|
|
5
|
+
} from './run.js';
|
|
6
|
+
import Session from './session.js';
|
|
7
|
+
import {
|
|
8
|
+
shortSha, isGhAvailable, getEditor
|
|
9
|
+
} from './utils.js';
|
|
10
|
+
|
|
11
|
+
const isWindows = process.platform === 'win32';
|
|
12
|
+
|
|
13
|
+
const LINT_RESULTS = {
|
|
14
|
+
SKIPPED: 'skipped',
|
|
15
|
+
FAILED: 'failed',
|
|
16
|
+
SUCCESS: 'success'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default class LandingSession extends Session {
|
|
20
|
+
constructor(cli, req, dir, {
|
|
21
|
+
prid, backport, lint, autorebase, fixupAll,
|
|
22
|
+
checkCI, oneCommitMax, ...argv
|
|
23
|
+
} = {}) {
|
|
24
|
+
super(cli, dir, prid);
|
|
25
|
+
this.req = req;
|
|
26
|
+
this.backport = backport;
|
|
27
|
+
this.lint = lint;
|
|
28
|
+
this.autorebase = autorebase;
|
|
29
|
+
this.fixupAll = fixupAll;
|
|
30
|
+
this.gpgSign = argv?.['gpg-sign']
|
|
31
|
+
? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
|
|
32
|
+
: [];
|
|
33
|
+
this.oneCommitMax = oneCommitMax;
|
|
34
|
+
this.expectedCommitShas = [];
|
|
35
|
+
this.checkCI = !!checkCI;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get argv() {
|
|
39
|
+
const args = super.argv;
|
|
40
|
+
args.backport = this.backport;
|
|
41
|
+
args.lint = this.lint;
|
|
42
|
+
args.autorebase = this.autorebase;
|
|
43
|
+
args.fixupAll = this.fixupAll;
|
|
44
|
+
args.oneCommitMax = this.oneCommitMax;
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async start(metadata) {
|
|
49
|
+
const { cli } = this;
|
|
50
|
+
this.startLanding();
|
|
51
|
+
this.expectedCommitShas =
|
|
52
|
+
metadata.data.commits.map(({ commit }) => commit.oid);
|
|
53
|
+
const status = metadata.status ? 'should be ready' : 'is not ready';
|
|
54
|
+
// NOTE(mmarchini): default answer is yes. If --yes is given, we need to be
|
|
55
|
+
// more careful though, and we change the default to the result of our
|
|
56
|
+
// metadata check.
|
|
57
|
+
const defaultAnswer = !cli.assumeYes ? true : metadata.status;
|
|
58
|
+
const shouldContinue = await cli.prompt(
|
|
59
|
+
`This PR ${status} to land, do you want to continue?`, { defaultAnswer });
|
|
60
|
+
if (!shouldContinue) {
|
|
61
|
+
await this.abort(false);
|
|
62
|
+
return process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.saveMetadata(metadata);
|
|
66
|
+
this.startApplying();
|
|
67
|
+
return this.apply();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async abort(tryResetBranch = true) {
|
|
71
|
+
try {
|
|
72
|
+
const { cli } = this;
|
|
73
|
+
this.cleanFiles();
|
|
74
|
+
if (tryResetBranch) {
|
|
75
|
+
await this.tryResetBranch();
|
|
76
|
+
}
|
|
77
|
+
cli.ok(`Aborted \`git node land\` session in ${this.ncuDir}`);
|
|
78
|
+
} catch (ex) {
|
|
79
|
+
const { cli } = this;
|
|
80
|
+
cli.setExitCode(1);
|
|
81
|
+
cli.error(`Couldn't abort \`git node land\` session in ${this.ncuDir}`);
|
|
82
|
+
throw ex;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async downloadAndPatch() {
|
|
87
|
+
const { cli, repo, owner, prid, expectedCommitShas } = this;
|
|
88
|
+
|
|
89
|
+
cli.startSpinner(`Downloading patch for ${prid}`);
|
|
90
|
+
await runAsync('git', [
|
|
91
|
+
'fetch', `https://github.com/${owner}/${repo}.git`,
|
|
92
|
+
`refs/pull/${prid}/merge`]);
|
|
93
|
+
// We fetched the commit that would result if we used `git merge`.
|
|
94
|
+
// ^1 and ^2 refer to the PR base and the PR head, respectively.
|
|
95
|
+
const [base, head] = await runAsync('git',
|
|
96
|
+
['rev-parse', 'FETCH_HEAD^1', 'FETCH_HEAD^2'],
|
|
97
|
+
{ captureStdout: 'lines' });
|
|
98
|
+
const commitShas = await runAsync('git',
|
|
99
|
+
['rev-list', `${base}..${head}`],
|
|
100
|
+
{ captureStdout: 'lines' });
|
|
101
|
+
cli.stopSpinner(`Fetched commits as ${shortSha(base)}..${shortSha(head)}`);
|
|
102
|
+
cli.separator();
|
|
103
|
+
|
|
104
|
+
const mismatchedCommits = [
|
|
105
|
+
...commitShas.filter((sha) => !expectedCommitShas.includes(sha))
|
|
106
|
+
.map((sha) => `Unexpected commit ${sha}`),
|
|
107
|
+
...expectedCommitShas.filter((sha) => !commitShas.includes(sha))
|
|
108
|
+
.map((sha) => `Missing commit ${sha}`)
|
|
109
|
+
].join('\n');
|
|
110
|
+
if (mismatchedCommits.length > 0) {
|
|
111
|
+
cli.error(`Mismatched commits:\n${mismatchedCommits}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const commitInfo = { base, head, shas: commitShas };
|
|
116
|
+
this.saveCommitInfo(commitInfo);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await forceRunAsync('git',
|
|
120
|
+
['cherry-pick', '--allow-empty', ...this.gpgSign, `${base}..${head}`],
|
|
121
|
+
{ ignoreFailure: false });
|
|
122
|
+
} catch (ex) {
|
|
123
|
+
cli.error('Failed to apply patches');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
cli.ok('Patches applied');
|
|
128
|
+
return commitInfo;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getRebaseSuggestion(subjects) {
|
|
132
|
+
const { upstream, branch } = this;
|
|
133
|
+
let command = `git rebase ${upstream}/${branch} --no-keep-empty -i`;
|
|
134
|
+
command += ' -x "git node land --amend"';
|
|
135
|
+
|
|
136
|
+
const squashes = subjects.filter(
|
|
137
|
+
line => line.includes('fixup!') || line.includes('squash!') || line.includes('amend!'));
|
|
138
|
+
|
|
139
|
+
if (squashes.length !== 0) {
|
|
140
|
+
command += ' --autosquash';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.gpgSign) {
|
|
144
|
+
command += ' ' + this.gpgSign.join(' ');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return command;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
makeRebaseSuggestion(subjects) {
|
|
151
|
+
const suggestion = this.getRebaseSuggestion(subjects);
|
|
152
|
+
this.cli.log('Please run the following commands to complete landing\n\n' +
|
|
153
|
+
`$ ${suggestion}\n` +
|
|
154
|
+
'$ git node land --continue');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
canAutomaticallyRebase(subjects) {
|
|
158
|
+
return subjects.every(line => !line.startsWith('squash!'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async validateLint() {
|
|
162
|
+
// The linter is currently only run on non-Windows platforms.
|
|
163
|
+
if (os.platform() === 'win32') {
|
|
164
|
+
return LINT_RESULTS.SKIPPED;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!this.lint) {
|
|
168
|
+
return LINT_RESULTS.SKIPPED;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await runAsync('make', ['lint']);
|
|
173
|
+
return LINT_RESULTS.SUCCESS;
|
|
174
|
+
} catch {
|
|
175
|
+
return LINT_RESULTS.FAILED;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async tryCompleteLanding(commitInfo) {
|
|
180
|
+
const { cli } = this;
|
|
181
|
+
const subjects = await runAsync('git',
|
|
182
|
+
['log', '--pretty=format:%s', `${commitInfo.base}..${commitInfo.head}`],
|
|
183
|
+
{ captureStdout: 'lines' });
|
|
184
|
+
|
|
185
|
+
if (commitInfo.shas.length === 1) {
|
|
186
|
+
const shouldAmend = await cli.prompt(
|
|
187
|
+
'There is only one commit in this PR.\n' +
|
|
188
|
+
'do you want to amend the commit message?');
|
|
189
|
+
if (!shouldAmend) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const canFinal = await this.amend();
|
|
193
|
+
if (!canFinal) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return this.final();
|
|
198
|
+
} else if (this.fixupAll) {
|
|
199
|
+
cli.log(`There are ${subjects.length} commits in the PR. ` +
|
|
200
|
+
'Attempting to fixup everything into first commit.');
|
|
201
|
+
await runAsync('git', ['reset', '--soft', `HEAD~${subjects.length - 1}`]);
|
|
202
|
+
await runAsync('git', ['commit', '--amend', '--no-edit', ...this.gpgSign]);
|
|
203
|
+
return await this.amend() && this.final();
|
|
204
|
+
} else if (this.autorebase && this.canAutomaticallyRebase(subjects)) {
|
|
205
|
+
// Run git rebase in interactive mode with autosquash but without editor
|
|
206
|
+
// so that it will perform everything automatically.
|
|
207
|
+
cli.log(`There are ${subjects.length} commits in the PR. ` +
|
|
208
|
+
'Attempting autorebase.');
|
|
209
|
+
const { upstream, branch } = this;
|
|
210
|
+
const assumeYes = this.cli.assumeYes ? '--yes' : '';
|
|
211
|
+
const msgAmend = `-x "git node land --amend ${assumeYes}"`;
|
|
212
|
+
try {
|
|
213
|
+
await forceRunAsync('git',
|
|
214
|
+
['rebase', ...this.gpgSign, `${upstream}/${branch}`,
|
|
215
|
+
'--no-keep-empty', '-i', '--autosquash', msgAmend],
|
|
216
|
+
{
|
|
217
|
+
ignoreFailure: false,
|
|
218
|
+
spawnArgs: {
|
|
219
|
+
shell: true,
|
|
220
|
+
env: { ...process.env, GIT_SEQUENCE_EDITOR: ':' }
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return this.final();
|
|
224
|
+
} catch (e) {
|
|
225
|
+
await runAsync('git', ['rebase', '--abort']);
|
|
226
|
+
const count = subjects.length;
|
|
227
|
+
cli.log(`Couldn't rebase ${count} commits in the PR automatically`);
|
|
228
|
+
this.makeRebaseSuggestion(subjects);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
this.makeRebaseSuggestion(subjects);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async apply() {
|
|
236
|
+
const { cli } = this;
|
|
237
|
+
|
|
238
|
+
// Bail if another landing session is currently in progress.
|
|
239
|
+
if (!this.isApplying()) {
|
|
240
|
+
cli.warn('Landing session already in progress - ' +
|
|
241
|
+
'to start a new one run `git node land --abort`');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
await this.tryResetBranch();
|
|
245
|
+
|
|
246
|
+
const commitInfo = await this.downloadAndPatch();
|
|
247
|
+
|
|
248
|
+
const cleanLint = await this.validateLint();
|
|
249
|
+
if (cleanLint === LINT_RESULTS.FAILED) {
|
|
250
|
+
const tryFixLint = await cli.prompt(
|
|
251
|
+
'Lint failed - try fixing with \'make lint-js-fix\'?');
|
|
252
|
+
if (tryFixLint) {
|
|
253
|
+
await runAsync('make', ['lint-js-fix']);
|
|
254
|
+
const fixed = await this.validateLint();
|
|
255
|
+
if (fixed === LINT_RESULTS.FAILED) {
|
|
256
|
+
cli.warn('Patch still contains lint errors. ' +
|
|
257
|
+
'Please fix manually before proceeding');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const correctedLint = await cli.prompt('Corrected all lint errors?');
|
|
262
|
+
if (correctedLint) {
|
|
263
|
+
await runAsync('git', ['add', '.']);
|
|
264
|
+
|
|
265
|
+
// Final message will be edited later - don't try to change it here.
|
|
266
|
+
await runAsync('git', ['commit', '--amend', '--no-edit', ...this.gpgSign]);
|
|
267
|
+
} else {
|
|
268
|
+
cli.info('Please fix lint errors and then run ' +
|
|
269
|
+
'`git node land --amend` followed by ' +
|
|
270
|
+
'`git node land --continue`.');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
} else if (cleanLint === LINT_RESULTS.SUCCESS) {
|
|
274
|
+
cli.ok('Lint passed cleanly');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.startAmending();
|
|
278
|
+
|
|
279
|
+
await this.tryCompleteLanding(commitInfo);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async amend() {
|
|
283
|
+
const { cli } = this;
|
|
284
|
+
if (!this.readyToAmend()) {
|
|
285
|
+
cli.warn('Not yet ready to amend, run `git node land --abort`');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this.startAmending();
|
|
289
|
+
|
|
290
|
+
const rev = this.getCurrentRev();
|
|
291
|
+
const original = runSync('git', [
|
|
292
|
+
'show', 'HEAD', '-s', '--format=%B'
|
|
293
|
+
]).trim();
|
|
294
|
+
|
|
295
|
+
// git has very specific rules about what is a trailer and what is not.
|
|
296
|
+
// Instead of trying to implement those ourselves, let git parse the
|
|
297
|
+
// original commit message and see if it outputs any trailers.
|
|
298
|
+
const originalHasTrailers = runSync('git', [
|
|
299
|
+
'interpret-trailers', '--parse', '--no-divider'
|
|
300
|
+
], {
|
|
301
|
+
input: `${original}\n`
|
|
302
|
+
}).trim().length !== 0;
|
|
303
|
+
|
|
304
|
+
const metadata = this.metadata.trim().split('\n');
|
|
305
|
+
const amended = original.split('\n');
|
|
306
|
+
|
|
307
|
+
// If the original commit message already contains trailers (such as
|
|
308
|
+
// "Co-authored-by"), we simply add our own metadata after those. Otherwise,
|
|
309
|
+
// we have to add an empty line so that git recognizes our own metadata as
|
|
310
|
+
// trailers in the amended commit message.
|
|
311
|
+
if (!originalHasTrailers) {
|
|
312
|
+
amended.push('');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const BACKPORT_RE = /BACKPORT-PR-URL\s*:\s*(\S+)/i;
|
|
316
|
+
const PR_RE = /PR-URL\s*:\s*(\S+)/i;
|
|
317
|
+
const REVIEW_RE = /Reviewed-By\s*:\s*(\S+)/i;
|
|
318
|
+
|
|
319
|
+
for (const line of metadata) {
|
|
320
|
+
if (line.length !== 0 && original.includes(line)) {
|
|
321
|
+
if (originalHasTrailers) {
|
|
322
|
+
cli.warn(`Found ${line}, skipping..`);
|
|
323
|
+
} else {
|
|
324
|
+
cli.error('Git found no trailers in the original commit message, ' +
|
|
325
|
+
`but '${line}' is present and should be a trailer.`);
|
|
326
|
+
process.exit(1); // make it work with git rebase -x
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
if (line.match(BACKPORT_RE)) {
|
|
330
|
+
let prIndex = amended.findIndex(datum => datum.match(PR_RE));
|
|
331
|
+
if (prIndex === -1) {
|
|
332
|
+
prIndex = amended.findIndex(datum => datum.match(REVIEW_RE)) - 1;
|
|
333
|
+
}
|
|
334
|
+
amended.splice(prIndex + 1, 0, line);
|
|
335
|
+
} else {
|
|
336
|
+
amended.push(line);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const message = amended.join('\n');
|
|
342
|
+
const messageFile = this.saveMessage(rev, message);
|
|
343
|
+
cli.separator('New Message');
|
|
344
|
+
cli.log(message.trim());
|
|
345
|
+
const takeMessage = await cli.prompt('Use this message?');
|
|
346
|
+
if (takeMessage) {
|
|
347
|
+
await runAsync('git', ['commit', '--amend', '-F', messageFile, ...this.gpgSign]);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const editor = await getEditor({ git: true });
|
|
352
|
+
if (editor) {
|
|
353
|
+
try {
|
|
354
|
+
await forceRunAsync(
|
|
355
|
+
editor,
|
|
356
|
+
[`"${messageFile}"`],
|
|
357
|
+
{ ignoreFailure: false, spawnArgs: { shell: true } }
|
|
358
|
+
);
|
|
359
|
+
await runAsync('git', ['commit', '--amend', '-F', messageFile, ...this.gpgSign]);
|
|
360
|
+
return true;
|
|
361
|
+
} catch {
|
|
362
|
+
cli.error('Failed to edit the message using the configured editor');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
cli.log(`Please manually edit ${messageFile}, then run\n` +
|
|
367
|
+
`\`git commit --amend -F ${messageFile}\` ` +
|
|
368
|
+
'to finish amending the message');
|
|
369
|
+
process.exit(1); // make it work with git rebase -x
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async final() {
|
|
373
|
+
const {
|
|
374
|
+
cli, owner, repo, upstream, branch, prid, oneCommitMax
|
|
375
|
+
} = this;
|
|
376
|
+
|
|
377
|
+
// Check that git rebase/am has been completed.
|
|
378
|
+
if (!this.readyToFinal()) {
|
|
379
|
+
cli.warn('Not yet ready to final');
|
|
380
|
+
cli.log('A git rebase/am is in progress.' +
|
|
381
|
+
' Please complete it before running git node land --final');
|
|
382
|
+
return;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const stray = this.getStrayCommits();
|
|
386
|
+
if (stray.length > 1) {
|
|
387
|
+
const forceLand = await cli.prompt(
|
|
388
|
+
'There is more than one commit in the PR. ' +
|
|
389
|
+
'Do you still want to land it?',
|
|
390
|
+
{ defaultAnswer: !oneCommitMax });
|
|
391
|
+
|
|
392
|
+
if (!forceLand) {
|
|
393
|
+
cli.info(
|
|
394
|
+
'GITHUB_ACTION' in process.env
|
|
395
|
+
|
|
396
|
+
? 'Add `commit-queue-squash` label to land the PR as one commit, ' +
|
|
397
|
+
'or `commit-queue-rebase` to land as separate commits.'
|
|
398
|
+
|
|
399
|
+
: 'Use --fixupAll option, squash the PR manually or land the PR ' +
|
|
400
|
+
'from the command line.'
|
|
401
|
+
);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const strayVerbose = this.getStrayCommits(true);
|
|
406
|
+
const validateCommand = new URL(
|
|
407
|
+
'../node_modules/.bin/core-validate-commit' + (isWindows ? '.cmd' : ''),
|
|
408
|
+
import.meta.url
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await forceRunAsync(validateCommand, stray, { ignoreFailure: false });
|
|
413
|
+
} catch (e) {
|
|
414
|
+
let forceLand = false;
|
|
415
|
+
if (e.code === 1) {
|
|
416
|
+
forceLand = await cli.prompt(
|
|
417
|
+
'The commit did not pass the validation. ' +
|
|
418
|
+
'Do you still want to land it?',
|
|
419
|
+
{ defaultAnswer: false });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!forceLand) {
|
|
423
|
+
cli.info('Please fix the commit message and try again.');
|
|
424
|
+
cli.log('Please manually ammend the commit message, by running\n' +
|
|
425
|
+
'`git commit --amend`\n' +
|
|
426
|
+
'Once commit message is fixed, finish the landing command running\n' +
|
|
427
|
+
'`git node land --continue`');
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
cli.separator();
|
|
433
|
+
cli.log('The following commits are ready to be pushed to ' +
|
|
434
|
+
`${upstream}/${branch}`);
|
|
435
|
+
cli.log(`- ${strayVerbose.join('\n- ')}`);
|
|
436
|
+
cli.separator();
|
|
437
|
+
|
|
438
|
+
let willBeLanded = shortSha(stray[stray.length - 1]);
|
|
439
|
+
if (stray.length > 1) {
|
|
440
|
+
const head = shortSha(this.getUpstreamHead());
|
|
441
|
+
willBeLanded = `${head}...${willBeLanded}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.cleanFiles();
|
|
445
|
+
cli.log('Temporary files removed.');
|
|
446
|
+
cli.log('To finish landing:');
|
|
447
|
+
cli.log('1. Run: ');
|
|
448
|
+
cli.log(` git push ${upstream} ${branch}`);
|
|
449
|
+
const url = `https://github.com/${owner}/${repo}/pull/${prid}`;
|
|
450
|
+
cli.log(`2. Post "Landed in ${willBeLanded}" in ${url}`);
|
|
451
|
+
if (isGhAvailable()) {
|
|
452
|
+
cli.log(` gh pr comment ${prid} --body "Landed in ${willBeLanded}"`);
|
|
453
|
+
cli.log(` gh pr close ${prid}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async continue() {
|
|
458
|
+
const { cli } = this;
|
|
459
|
+
if (this.readyToFinal()) {
|
|
460
|
+
cli.log('Running `final`..');
|
|
461
|
+
return this.final();
|
|
462
|
+
}
|
|
463
|
+
if (this.readyToAmend()) {
|
|
464
|
+
cli.log('Running `amend`..');
|
|
465
|
+
return this.amend();
|
|
466
|
+
}
|
|
467
|
+
if (this.isApplying()) {
|
|
468
|
+
// We're still resolving conflicts.
|
|
469
|
+
if (this.cherryPickInProgress()) {
|
|
470
|
+
cli.log('Looks like you are resolving a `git cherry-pick` conflict');
|
|
471
|
+
cli.log('Please run `git status` for help');
|
|
472
|
+
} else {
|
|
473
|
+
// Conflicts has been resolved - amend.
|
|
474
|
+
this.startAmending();
|
|
475
|
+
return this.tryCompleteLanding(this.commitInfo);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (this.hasStarted()) {
|
|
480
|
+
cli.log('Running `apply`..');
|
|
481
|
+
return this.apply();
|
|
482
|
+
}
|
|
483
|
+
cli.log(
|
|
484
|
+
'Please run `git node land <PRID> to start a landing session`');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async status() {
|
|
488
|
+
// TODO
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async warnForWrongBranch() {
|
|
492
|
+
if (super.warnForWrongBranch()) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
const rev = this.getCurrentBranch();
|
|
496
|
+
const { repository: { defaultBranchRef } } = await this.req.gql(
|
|
497
|
+
'DefaultBranchRef',
|
|
498
|
+
{ owner: this.owner, repo: this.repo });
|
|
499
|
+
if ((rev === 'master' || rev === 'main') && defaultBranchRef.name !== rev) {
|
|
500
|
+
this.cli.warn(`You are running git-node-land on \`${rev}\`,` +
|
|
501
|
+
` but the default branch is \`${defaultBranchRef.name}\`.`);
|
|
502
|
+
this.cli.setExitCode(1);
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
package/lib/links.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio';
|
|
2
|
+
|
|
3
|
+
const FIXES_RE = /(Close[ds]?|Fix(e[ds])?|Resolve[sd]?)\s*:\s*(\S+)/mgi;
|
|
4
|
+
const FIX_RE = /(Close[ds]?|Fix(e[ds])?|Resolve[sd]?)\s*:\s*(\S+)/i;
|
|
5
|
+
const REFS_RE = /Refs?\s*:\s*(\S+)/mgi;
|
|
6
|
+
const REF_RE = /Refs?\s*:\s*(\S+)/i;
|
|
7
|
+
const PR_RE = /PR-URL\s*:\s*(\S+)/i;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Most of this class is ported from node-review
|
|
11
|
+
*/
|
|
12
|
+
export class LinkParser {
|
|
13
|
+
constructor(owner, repo, html) {
|
|
14
|
+
this.owner = owner;
|
|
15
|
+
this.repo = repo;
|
|
16
|
+
this.$ = cheerio.load(html);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getFixesUrlsFromArray(arr) {
|
|
20
|
+
const result = new Set();
|
|
21
|
+
for (const item of arr) {
|
|
22
|
+
const m = item.match(FIX_RE);
|
|
23
|
+
if (!m) continue;
|
|
24
|
+
const ref = m[3];
|
|
25
|
+
const url = this.getUrlFromOP(ref);
|
|
26
|
+
if (url) result.add(url);
|
|
27
|
+
}
|
|
28
|
+
return Array.from(result);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getRefsUrlsFromArray(arr) {
|
|
32
|
+
const result = new Set();
|
|
33
|
+
for (const item of arr) {
|
|
34
|
+
const m = item.match(REF_RE);
|
|
35
|
+
if (!m) continue;
|
|
36
|
+
const ref = m[1];
|
|
37
|
+
const url = this.getUrlFromOP(ref);
|
|
38
|
+
if (url) result.add(url);
|
|
39
|
+
}
|
|
40
|
+
return Array.from(result);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getPRUrlsFromArray(arr) {
|
|
44
|
+
const result = new Set();
|
|
45
|
+
for (const item of arr) {
|
|
46
|
+
const m = item.match(PR_RE);
|
|
47
|
+
if (!m) continue;
|
|
48
|
+
const prUrl = m[1];
|
|
49
|
+
const url = this.getUrlFromOP(prUrl);
|
|
50
|
+
if (url) result.add(url);
|
|
51
|
+
}
|
|
52
|
+
return Array.from(result);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Do this so we can reliably get the correct url.
|
|
56
|
+
// Otherwise, the number could reference a PR or an issue.
|
|
57
|
+
getUrlFromOP(ref) {
|
|
58
|
+
const as = this.$('a');
|
|
59
|
+
const links = as.map((i, el) => this.$(el)).get();
|
|
60
|
+
for (const link of links) {
|
|
61
|
+
const text = link.text();
|
|
62
|
+
if (text === ref) {
|
|
63
|
+
const href = link.attr('href');
|
|
64
|
+
if (href) return href;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getFixes() {
|
|
70
|
+
const text = this.$.text();
|
|
71
|
+
const fixes = text.match(FIXES_RE) || [];
|
|
72
|
+
return this.getFixesUrlsFromArray(fixes);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getRefs() {
|
|
76
|
+
const text = this.$.text();
|
|
77
|
+
const refs = text.match(REFS_RE) || [];
|
|
78
|
+
return this.getRefsUrlsFromArray(refs);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getAltPrUrl() {
|
|
82
|
+
const text = this.$.text();
|
|
83
|
+
const refs = text.match(PR_RE) || [];
|
|
84
|
+
return this.getPRUrlsFromArray(refs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const GITHUB_PULL_REQUEST_URL = /github.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/;
|
|
89
|
+
|
|
90
|
+
export function parsePRFromURL(url) {
|
|
91
|
+
if (typeof url !== 'string') {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const match = url.match(GITHUB_PULL_REQUEST_URL);
|
|
95
|
+
if (match) {
|
|
96
|
+
return {
|
|
97
|
+
owner: match[1],
|
|
98
|
+
repo: match[2],
|
|
99
|
+
prid: parseInt(match[3])
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function getPrURL({ owner, repo, prid }) {
|
|
106
|
+
return `https://github.com/${owner}/${repo}/pull/${prid}`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export function getMachineUrl(name) {
|
|
110
|
+
return `[${name}](https://ci.nodejs.org/computer/${name}/)`;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const PR_URL_RE = /PR-URL: https:\/\/github.com\/.+/;
|
|
114
|
+
export function parsePrURL(text) {
|
|
115
|
+
if (typeof text !== 'string') {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const match = text.match(PR_URL_RE);
|
|
119
|
+
if (!match) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return parsePRFromURL(match[0]);
|
|
123
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { LinkParser } from './links.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{reviewer: Collaborator}} Reviewer
|
|
5
|
+
*/
|
|
6
|
+
export default class MetadataGenerator {
|
|
7
|
+
/**
|
|
8
|
+
* @param {PRData} data
|
|
9
|
+
*/
|
|
10
|
+
constructor(data) {
|
|
11
|
+
const { owner, repo, pr, reviewers, argv, skipRefs } = data;
|
|
12
|
+
this.owner = owner;
|
|
13
|
+
this.skipRefs = skipRefs;
|
|
14
|
+
this.repo = repo;
|
|
15
|
+
this.pr = pr;
|
|
16
|
+
this.reviewers = reviewers;
|
|
17
|
+
this.argv = argv;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
getMetadata() {
|
|
24
|
+
const {
|
|
25
|
+
reviewers: { approved: reviewedBy },
|
|
26
|
+
pr: { url: prUrl, bodyHTML: op },
|
|
27
|
+
owner,
|
|
28
|
+
repo
|
|
29
|
+
} = this;
|
|
30
|
+
|
|
31
|
+
const parser = new LinkParser(owner, repo, op);
|
|
32
|
+
const fixes = parser.getFixes();
|
|
33
|
+
const refs = parser.getRefs().filter(f => f !== prUrl);
|
|
34
|
+
const altPrUrl = parser.getAltPrUrl();
|
|
35
|
+
|
|
36
|
+
const meta = [];
|
|
37
|
+
|
|
38
|
+
// If there are multiple commits in a PR, we may not want to add
|
|
39
|
+
// Fixes/Refs metadata to all of them.
|
|
40
|
+
if (!this.skipRefs) {
|
|
41
|
+
// Map all issues fixed by the commit(s) in this PR.
|
|
42
|
+
meta.push(...fixes.map((fix) => `Fixes: ${fix}`));
|
|
43
|
+
// Map all issues referenced by the commit(s) in this PR.
|
|
44
|
+
meta.push(...refs.map((ref) => `Refs: ${ref}`));
|
|
45
|
+
}
|
|
46
|
+
const backport = this.argv ? this.argv.backport : undefined;
|
|
47
|
+
if (backport) {
|
|
48
|
+
meta.unshift(`Backport-PR-URL: ${prUrl}`);
|
|
49
|
+
meta.unshift(`PR-URL: ${altPrUrl}`);
|
|
50
|
+
} else {
|
|
51
|
+
// Reviews are only added here as backports should not contain reviews
|
|
52
|
+
// for the backport itself in the metadata
|
|
53
|
+
meta.unshift(`PR-URL: ${prUrl}`);
|
|
54
|
+
meta.push(
|
|
55
|
+
...reviewedBy.map((r) => `Reviewed-By: ${r.reviewer.getContact()}`)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
meta.push(''); // creates final EOL
|
|
59
|
+
return meta.join('\n');
|
|
60
|
+
}
|
|
61
|
+
}
|