@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.
Files changed (98) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +158 -0
  3. package/bin/get-metadata.js +11 -0
  4. package/bin/git-node.js +30 -0
  5. package/bin/ncu-ci.js +600 -0
  6. package/bin/ncu-config.js +101 -0
  7. package/bin/ncu-team.js +76 -0
  8. package/components/git/backport.js +70 -0
  9. package/components/git/epilogue.js +18 -0
  10. package/components/git/land.js +223 -0
  11. package/components/git/metadata.js +94 -0
  12. package/components/git/release.js +99 -0
  13. package/components/git/security.js +35 -0
  14. package/components/git/status.js +32 -0
  15. package/components/git/sync.js +24 -0
  16. package/components/git/v8.js +121 -0
  17. package/components/git/vote.js +84 -0
  18. package/components/git/wpt.js +87 -0
  19. package/components/metadata.js +49 -0
  20. package/lib/auth.js +133 -0
  21. package/lib/backport_session.js +302 -0
  22. package/lib/cache.js +107 -0
  23. package/lib/cherry_pick.js +304 -0
  24. package/lib/ci/build-types/benchmark_run.js +72 -0
  25. package/lib/ci/build-types/citgm_build.js +194 -0
  26. package/lib/ci/build-types/citgm_comparison_build.js +174 -0
  27. package/lib/ci/build-types/commit_build.js +112 -0
  28. package/lib/ci/build-types/daily_build.js +24 -0
  29. package/lib/ci/build-types/fanned_build.js +87 -0
  30. package/lib/ci/build-types/health_build.js +63 -0
  31. package/lib/ci/build-types/job.js +114 -0
  32. package/lib/ci/build-types/linter_build.js +35 -0
  33. package/lib/ci/build-types/normal_build.js +89 -0
  34. package/lib/ci/build-types/pr_build.js +101 -0
  35. package/lib/ci/build-types/test_build.js +186 -0
  36. package/lib/ci/build-types/test_run.js +41 -0
  37. package/lib/ci/ci_failure_parser.js +325 -0
  38. package/lib/ci/ci_type_parser.js +203 -0
  39. package/lib/ci/ci_utils.js +106 -0
  40. package/lib/ci/failure_aggregator.js +152 -0
  41. package/lib/ci/jenkins_constants.js +28 -0
  42. package/lib/ci/run_ci.js +120 -0
  43. package/lib/cli.js +192 -0
  44. package/lib/collaborators.js +140 -0
  45. package/lib/config.js +72 -0
  46. package/lib/figures.js +7 -0
  47. package/lib/file.js +43 -0
  48. package/lib/github/templates/next-security-release.md +97 -0
  49. package/lib/github/tree.js +162 -0
  50. package/lib/landing_session.js +506 -0
  51. package/lib/links.js +123 -0
  52. package/lib/mergeable_state.js +3 -0
  53. package/lib/metadata_gen.js +61 -0
  54. package/lib/pr_checker.js +605 -0
  55. package/lib/pr_data.js +115 -0
  56. package/lib/pr_summary.js +62 -0
  57. package/lib/prepare_release.js +772 -0
  58. package/lib/prepare_security.js +117 -0
  59. package/lib/proxy.js +21 -0
  60. package/lib/queries/DefaultBranchRef.gql +8 -0
  61. package/lib/queries/LastCommit.gql +16 -0
  62. package/lib/queries/PR.gql +37 -0
  63. package/lib/queries/PRComments.gql +27 -0
  64. package/lib/queries/PRCommits.gql +45 -0
  65. package/lib/queries/PRs.gql +25 -0
  66. package/lib/queries/Reviews.gql +23 -0
  67. package/lib/queries/SearchIssue.gql +51 -0
  68. package/lib/queries/Team.gql +22 -0
  69. package/lib/queries/TreeEntries.gql +12 -0
  70. package/lib/queries/VotePRInfo.gql +28 -0
  71. package/lib/release/utils.js +53 -0
  72. package/lib/request.js +185 -0
  73. package/lib/review_state.js +5 -0
  74. package/lib/reviews.js +178 -0
  75. package/lib/run.js +106 -0
  76. package/lib/session.js +415 -0
  77. package/lib/sync_session.js +15 -0
  78. package/lib/team_info.js +95 -0
  79. package/lib/update-v8/applyNodeChanges.js +49 -0
  80. package/lib/update-v8/backport.js +258 -0
  81. package/lib/update-v8/commitUpdate.js +26 -0
  82. package/lib/update-v8/common.js +35 -0
  83. package/lib/update-v8/constants.js +86 -0
  84. package/lib/update-v8/index.js +56 -0
  85. package/lib/update-v8/majorUpdate.js +171 -0
  86. package/lib/update-v8/minorUpdate.js +105 -0
  87. package/lib/update-v8/updateMaintainingDependencies.js +34 -0
  88. package/lib/update-v8/updateV8Clone.js +53 -0
  89. package/lib/update-v8/updateVersionNumbers.js +122 -0
  90. package/lib/update-v8/util.js +62 -0
  91. package/lib/user.js +4 -0
  92. package/lib/user_status.js +5 -0
  93. package/lib/utils.js +66 -0
  94. package/lib/verbosity.js +26 -0
  95. package/lib/voting_session.js +136 -0
  96. package/lib/wpt/index.js +243 -0
  97. package/lib/wpt/templates/README.md +16 -0
  98. package/package.json +69 -0
@@ -0,0 +1,302 @@
1
+ import Session from './session.js';
2
+ import { runSync, runAsync, IGNORE } from './run.js';
3
+ import { getPrURL, parsePrURL } from './links.js';
4
+
5
+ const MAX_HISTORY = 10;
6
+ const OLDEST_ID = new Map([
7
+ [8, 13000],
8
+ [10, 20000],
9
+ [11, 23000]
10
+ ]);
11
+
12
+ export default class BackportSession extends Session {
13
+ constructor(cli, dir, prid, target) {
14
+ super(cli, dir, prid);
15
+ this.target = target;
16
+ }
17
+
18
+ getChangedFiles(rev) {
19
+ return runSync('git',
20
+ ['diff-tree', '--no-commit-id', '--name-only', '-r', rev]
21
+ ).trim().split('\n');
22
+ }
23
+
24
+ getPreviousCommits(rev, file, num) {
25
+ let logs;
26
+ try {
27
+ logs = runSync('git',
28
+ ['log', `-${num}`, '--format=%h', rev, '--', file]
29
+ ).trim();
30
+ } catch (e) {
31
+ return null;
32
+ }
33
+ if (!logs) {
34
+ return [];
35
+ }
36
+
37
+ return logs.trim().split('\n');
38
+ }
39
+
40
+ getCommitMessage(rev) {
41
+ return runSync('git',
42
+ ['show', '--format=%B', '-s', rev]
43
+ ).trim();
44
+ }
45
+
46
+ get stagingBranch() {
47
+ return `v${this.target}.x-staging`;
48
+ }
49
+
50
+ getPotentialConflicts(rev, targetBranch) {
51
+ const { cli } = this;
52
+ const files = this.getChangedFiles(rev);
53
+ const notBackported = new Map();
54
+ const oldest = OLDEST_ID.get(this.target);
55
+ for (const file of files) {
56
+ cli.startSpinner(`Analyzing ancestors of ${file}`);
57
+ // TODO(joyeecheung): if the file does not exit in the current revision,
58
+ // warn about it and skip it.
59
+ const ancestors = this.getPreviousCommits(`${rev}~1`, file, MAX_HISTORY);
60
+ if (!ancestors) {
61
+ cli.stopSpinner(`${file} does not exist in current working tree`,
62
+ cli.SPINNER_STATUS.WARN);
63
+ continue;
64
+ }
65
+ if (ancestors.length === 0) {
66
+ cli.stopSpinner(`Cannot find ancestor commits of ${file}`,
67
+ cli.SPINNER_STATUS.INFO);
68
+ continue;
69
+ }
70
+ for (const ancestor of ancestors) {
71
+ const message = this.getCommitMessage(ancestor);
72
+ cli.updateSpinner(`Analyzing ${message.split('\n')[0]}...`);
73
+ let data = parsePrURL(message);
74
+ if (!data) {
75
+ const match = message.match('/^PR-URL: #(\\d+)/');
76
+ if (!match) {
77
+ cli.stopSpinner(
78
+ `Commit message of ${ancestor} is ill-formed, skipping`,
79
+ cli.SPINNER_STATUS.WARN);
80
+ cli.startSpinner(`Analyzing ancestors of ${file}`);
81
+ continue;
82
+ }
83
+ data = {
84
+ repo: this.repo,
85
+ owner: this.owner,
86
+ prid: parseInt(match[1])
87
+ };
88
+ }
89
+ if (data.prid < oldest) {
90
+ cli.updateSpinner(
91
+ `Commit ${ancestor} iS too old, skipping`,
92
+ cli.SPINNER_STATUS.WARN);
93
+ break;
94
+ }
95
+ const backported = this.getCommitsFromBranch(
96
+ data.prid, targetBranch
97
+ );
98
+ if (backported.length === 0) {
99
+ const record = notBackported.get(ancestor);
100
+ if (record) {
101
+ record.files.add(file);
102
+ } else {
103
+ notBackported.set(ancestor, {
104
+ prid: data.prid,
105
+ url: getPrURL(data),
106
+ commit: ancestor,
107
+ title: message.split('\n')[0],
108
+ files: new Set([file])
109
+ });
110
+ }
111
+ }
112
+ }
113
+ cli.stopSpinner(`Analyzed ${file}`);
114
+ }
115
+ return notBackported;
116
+ }
117
+
118
+ warnForPotentialConflicts(rev) {
119
+ const { cli } = this;
120
+ const staging = this.stagingBranch;
121
+
122
+ cli.log(`Looking for potential conflicts of ${rev}...`);
123
+ const notBackported = this.getPotentialConflicts(rev, staging);
124
+
125
+ if (notBackported.size === 0) {
126
+ cli.info(`All ancestor commits of ${rev} have been backported`);
127
+ return;
128
+ }
129
+
130
+ cli.warn(`The following ancestor commits of ${rev} are not on ${staging}`);
131
+ for (const [commit, data] of notBackported) {
132
+ cli.log(` - ${commit} ${data.title}, ${data.url}`);
133
+ for (const file of data.files) {
134
+ cli.log(` ${file}`);
135
+ }
136
+ }
137
+ }
138
+
139
+ async backport() {
140
+ const { cli } = this;
141
+ // TODO(joyeechuneg): add more warnings
142
+ const { prid } = this;
143
+ const url = getPrURL(this);
144
+ cli.log(`Looking for commits of ${url} on main...`);
145
+
146
+ const commits = this.getCommitsFromBranch(prid, 'main');
147
+
148
+ if (commits.length === 0) {
149
+ cli.error('Could not find any commit matching the PR');
150
+ throw new Error(IGNORE);
151
+ }
152
+
153
+ cli.ok('Found the following commits:');
154
+ for (const commit of commits) {
155
+ cli.log(` - ${commit.sha} ${commit.title}`);
156
+ }
157
+
158
+ if (!this.isLocalBranchExists(this.stagingBranch)) {
159
+ const shouldCreateStagingBranch = await cli.prompt(
160
+ `It seems like ${this.stagingBranch} is missing locally, ` +
161
+ 'do you want to create it locally to get ready for backporting?', {
162
+ defaultAnswer: true
163
+ });
164
+
165
+ if (shouldCreateStagingBranch) {
166
+ this.syncBranchWithUpstream(this.stagingBranch);
167
+ }
168
+ } else if (!this.isBranchUpToDateWithUpstream(this.stagingBranch)) {
169
+ const shouldSyncBranch = await cli.prompt(
170
+ `It seems like your ${this.stagingBranch} is behind the ${this.upstream} remote ` +
171
+ 'do you want to sync it?', { defaultAnswer: true });
172
+
173
+ if (shouldSyncBranch) {
174
+ this.syncBranchWithUpstream(this.stagingBranch);
175
+ }
176
+ }
177
+
178
+ const newBranch = `backport-${this.prid}-to-${this.target}`;
179
+ const shouldCheckout = await cli.prompt(
180
+ `Do you want to checkout to a new branch \`${newBranch}\`` +
181
+ ' to start backporting?', { defaultAnswer: false });
182
+
183
+ if (shouldCheckout) {
184
+ await runAsync('git', ['checkout', '-b', newBranch, this.stagingBranch]);
185
+ }
186
+
187
+ const shouldAnalyze = await cli.prompt(
188
+ 'Do you want to analyze the dependencies of the commits? ' +
189
+ '(this could take a while)');
190
+ if (shouldAnalyze) {
191
+ for (const commit of commits) {
192
+ this.warnForPotentialConflicts(commit.sha);
193
+ }
194
+ }
195
+
196
+ const cherries = commits.map(i => i.sha).reverse();
197
+ const pendingCommands = [
198
+ `git cherry-pick ${cherries.join(' ')}`,
199
+ 'git push -u <your-fork-remote> <your-branch-name>'
200
+ ];
201
+ const shouldPick = await cli.prompt(
202
+ 'Do you want to cherry-pick the commits?');
203
+ if (!shouldPick) {
204
+ this.hintCommands(pendingCommands);
205
+ return;
206
+ }
207
+
208
+ cli.log(`Running \`${pendingCommands[0]}\`...`);
209
+ pendingCommands.shift();
210
+ await runAsync('git', ['cherry-pick', ...cherries]);
211
+ this.hintCommands(pendingCommands);
212
+ }
213
+
214
+ hintCommands(commands) {
215
+ this.cli.log('Tips: run the following commands to complete backport');
216
+ for (const command of commands) {
217
+ this.cli.log(`$ ${command}`);
218
+ }
219
+ }
220
+
221
+ getCommitsFromBranch(prid, branch, loose = true) {
222
+ let re;
223
+ const url = getPrURL({ prid, repo: this.repo, owner: this.owner });
224
+ re = `--grep=PR-URL: ${url}`;
225
+
226
+ let commits = runSync('git', [
227
+ 'log', re, '--format=%h %s', branch
228
+ ]).trim();
229
+ if (!commits) {
230
+ if (!loose) {
231
+ return [];
232
+ }
233
+ re = `--grep=PR-URL: #${prid}\\b`;
234
+ commits = runSync('git', [
235
+ 'log', re, '--format=%h %s', branch
236
+ ]).trim();
237
+ if (!commits) {
238
+ return [];
239
+ }
240
+ }
241
+
242
+ return commits.split('\n').map((i) => {
243
+ const match = i.match(/(\w+) (.+)/);
244
+ return {
245
+ sha: match[1],
246
+ title: match[2]
247
+ };
248
+ });
249
+ }
250
+
251
+ getCurrentBranch() {
252
+ return runSync('git',
253
+ ['rev-parse', '--abbrev-ref', 'HEAD']
254
+ ).trim();
255
+ }
256
+
257
+ updateUpstreamRefs(branchName) {
258
+ runSync('git',
259
+ ['fetch', this.upstream, branchName]
260
+ );
261
+ }
262
+
263
+ getBranchCommit(branch) {
264
+ return runSync('git',
265
+ ['rev-parse', branch]
266
+ ).trim();
267
+ }
268
+
269
+ isBranchUpToDateWithUpstream(branch) {
270
+ this.updateUpstreamRefs(branch);
271
+
272
+ const localCommit = this.getBranchCommit(branch);
273
+ const upstreamCommit = this.getBranchCommit(`${this.upstream}/${branch}`);
274
+
275
+ return localCommit === upstreamCommit;
276
+ };
277
+
278
+ isLocalBranchExists(branch) {
279
+ try {
280
+ // will exit with code 1 if branch does not exist
281
+ runSync('git',
282
+ ['rev-parse', '--verify', '--quiet', branch]
283
+ );
284
+ return true;
285
+ } catch (e) {
286
+ return false;
287
+ }
288
+ }
289
+
290
+ syncBranchWithUpstream(branch) {
291
+ const currentBranch = this.getCurrentBranch();
292
+
293
+ runSync('git',
294
+ [
295
+ currentBranch !== branch ? 'fetch' : 'pull',
296
+ this.upstream,
297
+ `${branch}:${branch}`,
298
+ '-f'
299
+ ]
300
+ );
301
+ }
302
+ }
package/lib/cache.js ADDED
@@ -0,0 +1,107 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { writeJson, readJson, writeFile, readFile } from './file.js';
6
+
7
+ function isAsync(fn) {
8
+ return fn[Symbol.toStringTag] === 'AsyncFunction';
9
+ }
10
+
11
+ const parentDir = fileURLToPath(new URL('..', import.meta.url));
12
+
13
+ export default class Cache {
14
+ constructor(dir) {
15
+ this.dir = dir || this.computeCacheDir(parentDir);
16
+ this.originals = {};
17
+ this.disabled = true;
18
+ }
19
+
20
+ computeCacheDir(base) {
21
+ return path.join(base, '.ncu', 'cache');
22
+ }
23
+
24
+ disable() {
25
+ this.disabled = true;
26
+ }
27
+
28
+ enable() {
29
+ this.disabled = false;
30
+ }
31
+
32
+ getFilename(key, ext) {
33
+ return path.join(this.dir, key) + ext;
34
+ }
35
+
36
+ has(key, ext) {
37
+ if (this.disabled) {
38
+ return false;
39
+ }
40
+
41
+ return fs.existsSync(this.getFilename(key, ext));
42
+ }
43
+
44
+ get(key, ext) {
45
+ if (!this.has(key, ext)) {
46
+ return undefined;
47
+ }
48
+ if (ext === '.json') {
49
+ return readJson(this.getFilename(key, ext));
50
+ } else {
51
+ return readFile(this.getFilename(key, ext));
52
+ }
53
+ }
54
+
55
+ write(key, ext, content) {
56
+ if (this.disabled) {
57
+ return;
58
+ }
59
+ const filename = this.getFilename(key, ext);
60
+ if (ext === '.json') {
61
+ return writeJson(filename, content);
62
+ } else {
63
+ return writeFile(filename, content);
64
+ }
65
+ }
66
+
67
+ wrapAsync(original, identity) {
68
+ const cache = this;
69
+ return async function(...args) {
70
+ const { key, ext } = identity.call(this, ...args);
71
+ const cached = cache.get(key, ext);
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+ const result = await original.call(this, ...args);
76
+ cache.write(key, ext, result);
77
+ return result;
78
+ };
79
+ }
80
+
81
+ wrapNormal(original, identity) {
82
+ const cache = this;
83
+ return function(...args) {
84
+ const { key, ext } = identity.call(this, ...args);
85
+ const cached = cache.get(key, ext);
86
+ if (cached) {
87
+ return cached;
88
+ }
89
+ const result = original.call(this, ...args);
90
+ cache.write(key, ext, result);
91
+ return result;
92
+ };
93
+ }
94
+
95
+ wrap(Class, identities) {
96
+ for (const method of Object.keys(identities)) {
97
+ const original = Class.prototype[method];
98
+ const identity = identities[method];
99
+ this.originals[method] = original;
100
+ if (isAsync(original)) {
101
+ Class.prototype[method] = this.wrapAsync(original, identity);
102
+ } else {
103
+ Class.prototype[method] = this.wrapNormal(original, identity);
104
+ }
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,304 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { getMetadata } from '../components/metadata.js';
4
+
5
+ import {
6
+ runAsync, runSync, forceRunAsync
7
+ } from './run.js';
8
+ import { writeFile } from './file.js';
9
+ import {
10
+ shortSha, getEditor
11
+ } from './utils.js';
12
+ import { getNcuDir } from './config.js';
13
+
14
+ const LINT_RESULTS = {
15
+ SKIPPED: 'skipped',
16
+ FAILED: 'failed',
17
+ SUCCESS: 'success'
18
+ };
19
+
20
+ export default class CheckPick {
21
+ constructor(prid, dir, cli, {
22
+ owner,
23
+ repo,
24
+ lint,
25
+ includeCVE
26
+ } = {}) {
27
+ this.prid = prid;
28
+ this.cli = cli;
29
+ this.dir = dir;
30
+ this.options = { owner, repo, lint, includeCVE };
31
+ }
32
+
33
+ get includeCVE() {
34
+ return this.options.includeCVE ?? false;
35
+ }
36
+
37
+ get owner() {
38
+ return this.options.owner || 'nodejs';
39
+ }
40
+
41
+ get repo() {
42
+ return this.options.repo || 'node';
43
+ }
44
+
45
+ get lint() {
46
+ return this.options.lint;
47
+ }
48
+
49
+ getUpstreamHead() {
50
+ const { upstream, branch } = this;
51
+ return runSync('git', ['rev-parse', `${upstream}/${branch}`]).trim();
52
+ }
53
+
54
+ getCurrentRev() {
55
+ return runSync('git', ['rev-parse', 'HEAD']).trim();
56
+ }
57
+
58
+ getStrayCommits(verbose) {
59
+ const { upstream, branch } = this;
60
+ const ref = `${upstream}/${branch}...HEAD`;
61
+ const gitCmd = verbose
62
+ ? ['log', '--oneline', '--reverse', ref]
63
+ : ['rev-list', '--reverse', ref];
64
+ const revs = runSync('git', gitCmd).trim();
65
+ return revs ? revs.split('\n') : [];
66
+ }
67
+
68
+ get ncuDir() {
69
+ return getNcuDir(this.dir);
70
+ }
71
+
72
+ get pullDir() {
73
+ return path.resolve(this.ncuDir, `${this.prid}`);
74
+ }
75
+
76
+ getMessagePath(rev) {
77
+ return path.resolve(this.pullDir, `${shortSha(rev)}.COMMIT_EDITMSG`);
78
+ }
79
+
80
+ saveMessage(rev, message) {
81
+ const file = this.getMessagePath(rev);
82
+ writeFile(file, message);
83
+ return file;
84
+ }
85
+
86
+ async start() {
87
+ const { cli } = this;
88
+
89
+ const metadata = await getMetadata({
90
+ prid: this.prid,
91
+ owner: this.owner,
92
+ repo: this.repo
93
+ }, false, cli);
94
+ const expectedCommitShas =
95
+ metadata.data.commits.map(({ commit }) => commit.oid);
96
+
97
+ const amend = await cli.prompt(
98
+ 'Would you like to amend this PR to the proposal?',
99
+ { default: true }
100
+ );
101
+
102
+ if (!amend) {
103
+ return true;
104
+ }
105
+
106
+ try {
107
+ const commitInfo = await this.downloadAndPatch(expectedCommitShas);
108
+ const cleanLint = await this.validateLint();
109
+ if (cleanLint === LINT_RESULTS.FAILED) {
110
+ cli.error('Patch still contains lint errors. ' +
111
+ 'Please fix manually before proceeding');
112
+ return false;
113
+ } else if (cleanLint === LINT_RESULTS.SUCCESS) {
114
+ cli.ok('Lint passed cleanly');
115
+ }
116
+ return this.amend(metadata.metadata, commitInfo);
117
+ } catch (e) {
118
+ cli.error(e.message);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ async downloadAndPatch(expectedCommitShas) {
124
+ const { cli, repo, owner, prid } = this;
125
+
126
+ cli.startSpinner(`Downloading patch for ${prid}`);
127
+ // fetch via ssh to handle private repo
128
+ await runAsync('git', [
129
+ 'fetch', `git@github.com:${owner}/${repo}.git`,
130
+ `refs/pull/${prid}/merge`]);
131
+ // We fetched the commit that would result if we used `git merge`.
132
+ // ^1 and ^2 refer to the PR base and the PR head, respectively.
133
+ const [base, head] = await runAsync('git',
134
+ ['rev-parse', 'FETCH_HEAD^1', 'FETCH_HEAD^2'],
135
+ { captureStdout: 'lines' });
136
+ const commitShas = await runAsync('git',
137
+ ['rev-list', `${base}..${head}`],
138
+ { captureStdout: 'lines' });
139
+ cli.stopSpinner(`Fetched commits as ${shortSha(base)}..${shortSha(head)}`);
140
+ cli.separator();
141
+
142
+ const mismatchedCommits = [
143
+ ...commitShas.filter((sha) => !expectedCommitShas.includes(sha))
144
+ .map((sha) => `Unexpected commit ${sha}`),
145
+ ...expectedCommitShas.filter((sha) => !commitShas.includes(sha))
146
+ .map((sha) => `Missing commit ${sha}`)
147
+ ].join('\n');
148
+ if (mismatchedCommits.length > 0) {
149
+ throw new Error(`Mismatched commits:\n${mismatchedCommits}`);
150
+ }
151
+
152
+ const commitInfo = { base, head, shas: commitShas };
153
+
154
+ try {
155
+ await forceRunAsync('git', ['cherry-pick', `${base}..${head}`], {
156
+ ignoreFailure: false
157
+ });
158
+ } catch (ex) {
159
+ await forceRunAsync('git', ['cherry-pick', '--abort']);
160
+ throw new Error('Failed to apply patches');
161
+ }
162
+
163
+ cli.ok('Patches applied');
164
+ return commitInfo;
165
+ }
166
+
167
+ async validateLint() {
168
+ // The linter is currently only run on non-Windows platforms.
169
+ if (os.platform() === 'win32') {
170
+ return LINT_RESULTS.SKIPPED;
171
+ }
172
+
173
+ if (!this.lint) {
174
+ return LINT_RESULTS.SKIPPED;
175
+ }
176
+
177
+ try {
178
+ await runAsync('make', ['lint']);
179
+ return LINT_RESULTS.SUCCESS;
180
+ } catch {
181
+ return LINT_RESULTS.FAILED;
182
+ }
183
+ }
184
+
185
+ async amend(metadata, commitInfo) {
186
+ const { cli } = this;
187
+ const subjects = await runAsync('git',
188
+ ['log', '--pretty=format:%s', `${commitInfo.base}..${commitInfo.head}`],
189
+ { captureStdout: 'lines' });
190
+
191
+ if (commitInfo.shas.length !== 1) {
192
+ const fixupAll = await cli.prompt(
193
+ `${subjects.length} commits from the original PR are going to be` +
194
+ 'squashed into a single commit. OK to proceed?', {
195
+ defaultAnswer: true
196
+ });
197
+ if (!fixupAll) {
198
+ // TODO: add this support?
199
+ throw new Error(`There are ${subjects.length} commits in the PR ` +
200
+ 'and the ammend were not able to succeed');
201
+ }
202
+ await runAsync('git', ['reset', '--soft', `HEAD~${subjects.length - 1}`]);
203
+ await runAsync('git', ['commit', '--amend', '--no-edit']);
204
+ }
205
+
206
+ return this._amend(metadata);
207
+ }
208
+
209
+ async _amend(metadataStr) {
210
+ const { cli } = this;
211
+
212
+ const rev = this.getCurrentRev();
213
+ const original = runSync('git', [
214
+ 'show', 'HEAD', '-s', '--format=%B'
215
+ ]).trim();
216
+ // git has very specific rules about what is a trailer and what is not.
217
+ // Instead of trying to implement those ourselves, let git parse the
218
+ // original commit message and see if it outputs any trailers.
219
+ const originalHasTrailers = runSync('git', [
220
+ 'interpret-trailers', '--parse', '--no-divider'
221
+ ], {
222
+ input: `${original}\n`
223
+ }).trim().length !== 0;
224
+ const metadata = metadataStr.trim().split('\n');
225
+ const amended = original.split('\n');
226
+
227
+ // If the original commit message already contains trailers (such as
228
+ // "Co-authored-by"), we simply add our own metadata after those. Otherwise,
229
+ // we have to add an empty line so that git recognizes our own metadata as
230
+ // trailers in the amended commit message.
231
+ if (!originalHasTrailers) {
232
+ amended.push('');
233
+ }
234
+
235
+ const BACKPORT_RE = /BACKPORT-PR-URL\s*:\s*(\S+)/i;
236
+ const PR_RE = /PR-URL\s*:\s*(\S+)/i;
237
+ const REVIEW_RE = /Reviewed-By\s*:\s*(\S+)/i;
238
+ const CVE_RE = /CVE-ID\s*:\s*(\S+)/i;
239
+
240
+ let containCVETrailer = false;
241
+ for (const line of metadata) {
242
+ if (line.length !== 0 && original.includes(line)) {
243
+ if (line.match(CVE_RE)) {
244
+ containCVETrailer = true;
245
+ }
246
+ if (originalHasTrailers) {
247
+ cli.warn(`Found ${line}, skipping..`);
248
+ } else {
249
+ throw new Error(
250
+ 'Git found no trailers in the original commit message, ' +
251
+ `but '${line}' is present and should be a trailer.`);
252
+ }
253
+ } else {
254
+ if (line.match(BACKPORT_RE)) {
255
+ let prIndex = amended.findIndex(datum => datum.match(PR_RE));
256
+ if (prIndex === -1) {
257
+ prIndex = amended.findIndex(datum => datum.match(REVIEW_RE)) - 1;
258
+ }
259
+ amended.splice(prIndex + 1, 0, line);
260
+ } else {
261
+ amended.push(line);
262
+ }
263
+ }
264
+ }
265
+
266
+ if (!containCVETrailer && this.includeCVE) {
267
+ const cveID = await cli.prompt(
268
+ 'Git found no CVE-ID trailer in the original commit message. ' +
269
+ 'Please, provide the CVE-ID',
270
+ { questionType: 'input', defaultAnswer: 'CVE-2023-XXXXX' }
271
+ );
272
+ amended.push('CVE-ID: ' + cveID);
273
+ }
274
+
275
+ const message = amended.join('\n');
276
+ const messageFile = this.saveMessage(rev, message);
277
+ cli.separator('New Message');
278
+ cli.log(message.trim());
279
+ const takeMessage = await cli.prompt('Use this message?');
280
+ if (takeMessage) {
281
+ await runAsync('git', ['commit', '--amend', '-F', messageFile]);
282
+ return true;
283
+ }
284
+
285
+ const editor = await getEditor({ git: true });
286
+ if (editor) {
287
+ try {
288
+ await forceRunAsync(
289
+ editor,
290
+ [`"${messageFile}"`],
291
+ { ignoreFailure: false, spawnArgs: { shell: true } }
292
+ );
293
+ await runAsync('git', ['commit', '--amend', '-F', messageFile]);
294
+ return true;
295
+ } catch {
296
+ cli.warn(`Please manually edit ${messageFile}, then run\n` +
297
+ `\`git commit --amend -F ${messageFile}\` ` +
298
+ 'to finish amending the message');
299
+ throw new Error(
300
+ 'Failed to edit the message using the configured editor');
301
+ }
302
+ }
303
+ }
304
+ }