Skip to content

Commit f586e2c

Browse files
committed
feat: reconstruct-chromium-version command
1 parent 5b80e17 commit f586e2c

File tree

4 files changed

+358
-58
lines changed

4 files changed

+358
-58
lines changed

src/e

+6-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,12 @@ program
179179
'Opens a PR to electron/electron that backport the given CL into our patches folder',
180180
)
181181
.alias('auto-cherry-pick')
182-
.command('gh-auth', 'Generates a device oauth token');
182+
.command('gh-auth', 'Generates a device oauth token')
183+
.command(
184+
'rcv [roll-pr] [chromium-version]',
185+
'Attempts to reconstruct an intermediate Chromium version from a roll PR',
186+
)
187+
.alias('reconstruct-chromium-version');
183188

184189
program
185190
.command('load-macos-sdk')

src/e-cherry-pick.js

-57
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const path = require('path');
88
const os = require('os');
99
const { Octokit } = require('@octokit/rest');
1010

11-
const { getCveForBugNr } = require('./utils/crbug');
1211
const { getGitHubAuthToken } = require('./utils/github-auth');
1312
const { fatal, color } = require('./utils/logging');
1413

@@ -17,14 +16,6 @@ const ELECTRON_REPO_DATA = {
1716
repo: 'electron',
1817
};
1918

20-
const gerritSources = [
21-
'chromium-review.googlesource.com',
22-
'skia-review.googlesource.com',
23-
'webrtc-review.googlesource.com',
24-
'pdfium-review.googlesource.com',
25-
'dawn-review.googlesource.com',
26-
];
27-
2819
async function getPatchDetailsFromURL(urlStr, security) {
2920
const parsedUrl = new URL(urlStr);
3021
if (parsedUrl.host.endsWith('.googlesource.com')) {
@@ -38,54 +29,6 @@ async function getPatchDetailsFromURL(urlStr, security) {
3829
);
3930
}
4031

41-
async function getGerritPatchDetailsFromURL(gerritUrl, security) {
42-
const { host, pathname } = gerritUrl;
43-
44-
if (!gerritSources.includes(host)) {
45-
fatal('Unsupported gerrit host');
46-
}
47-
const [, repo, number] = /^\/c\/(.+?)\/\+\/(\d+)/.exec(pathname);
48-
49-
d(`fetching patch from gerrit`);
50-
const changeId = `${repo}~${number}`;
51-
const patchUrl = new URL(
52-
`/changes/${encodeURIComponent(changeId)}/revisions/current/patch`,
53-
gerritUrl,
54-
);
55-
56-
const patch = await fetch(patchUrl)
57-
.then((resp) => resp.text())
58-
.then((text) => Buffer.from(text, 'base64').toString('utf8'));
59-
60-
const [, commitId] = /^From ([0-9a-f]+)/.exec(patch);
61-
62-
const bugNumber =
63-
/^(?:Bug|Fixed)[:=] ?(.+)$/im.exec(patch)?.[1] || /^Bug= ?chromium:(.+)$/m.exec(patch)?.[1];
64-
65-
let cve = '';
66-
if (security) {
67-
try {
68-
cve = await getCveForBugNr(bugNumber.replace('chromium:', ''));
69-
} catch (err) {
70-
d(err);
71-
console.error(
72-
`${color.warn} Failed to fetch CVE for ${bugNumber} - you'll need to find it manually`,
73-
);
74-
}
75-
}
76-
77-
const patchDirName =
78-
{
79-
'chromium-review.googlesource.com:chromium/src': 'chromium',
80-
'skia-review.googlesource.com:skia': 'skia',
81-
'webrtc-review.googlesource.com:src': 'webrtc',
82-
}[`${host}:${repo}`] || repo.split('/').reverse()[0];
83-
84-
const shortCommit = commitId.substr(0, 12);
85-
86-
return { patchDirName, shortCommit, patch, bugNumber, cve };
87-
}
88-
8932
async function getGitHubPatchDetailsFromURL(gitHubUrl, security) {
9033
if (security) {
9134
fatal('GitHub cherry-picks can not be security backports currently');

src/e-rcv.js

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
#!/usr/bin/env node
2+
3+
const { Octokit } = require('@octokit/rest');
4+
const chalk = require('chalk').default;
5+
const program = require('commander');
6+
const fs = require('fs');
7+
const path = require('path');
8+
const os = require('os');
9+
10+
const evmConfig = require('./evm-config');
11+
const { spawnSync } = require('./utils/depot-tools');
12+
const { getGerritPatchDetailsFromURL } = require('./utils/gerrit');
13+
const { getGitHubAuthToken } = require('./utils/github-auth');
14+
const { color, fatal } = require('./utils/logging');
15+
16+
const ELECTRON_REPO_DATA = {
17+
owner: 'electron',
18+
repo: 'electron',
19+
};
20+
const DEPS_REGEX = new RegExp(`chromium_version':\n +'(.+?)',`, 'm');
21+
const CL_REGEX = /https:\/\/chromium-review\.googlesource\.com\/c\/chromium\/src\/\+\/(\d+)/;
22+
23+
async function getChromiumVersion(octokit, ref) {
24+
const { data } = await octokit.repos.getContent({
25+
...ELECTRON_REPO_DATA,
26+
path: 'DEPS',
27+
ref,
28+
});
29+
30+
if (!data.content) {
31+
fatal('Could not read content of PR');
32+
return;
33+
}
34+
35+
const [, version] = DEPS_REGEX.exec(Buffer.from(data.content, 'base64').toString('utf8'));
36+
37+
return version;
38+
}
39+
40+
// Copied from https://github.com/electron/electron/blob/3a3595f2af59cb08fb09e3e2e4b7cdf713db2b27/script/release/notes/notes.ts#L605-L623
41+
const compareChromiumVersions = (v1, v2) => {
42+
const [split1, split2] = [v1.split('.'), v2.split('.')];
43+
44+
if (split1.length !== split2.length) {
45+
throw new Error(
46+
`Expected version strings to have same number of sections: ${split1} and ${split2}`,
47+
);
48+
}
49+
for (let i = 0; i < split1.length; i++) {
50+
const p1 = parseInt(split1[i], 10);
51+
const p2 = parseInt(split2[i], 10);
52+
53+
if (p1 > p2) return 1;
54+
else if (p1 < p2) return -1;
55+
// Continue checking the value if this portion is equal
56+
}
57+
58+
return 0;
59+
};
60+
61+
program
62+
.arguments('[roll-pr] [chromium-version]')
63+
.description('Attempts to reconstruct an intermediate Chromium version from a roll PR')
64+
.option('--sort', 'Sort cherry-picked commits by CL merge time', false)
65+
.option(
66+
'--merge-strategy-option',
67+
'Git merge strategy option to use when cherry-picking',
68+
'theirs',
69+
)
70+
.action(async (prNumberStr, chromiumVersionStr, options) => {
71+
const prNumber = parseInt(prNumberStr, 10);
72+
if (isNaN(prNumber) || `${prNumber}` !== prNumberStr) {
73+
fatal(`rcv requires a PR number, "${prNumberStr}" was provided`);
74+
return;
75+
}
76+
77+
if (!chromiumVersionStr) {
78+
fatal('rcv requires a Chromium version, none was provided');
79+
return;
80+
}
81+
82+
const octokit = new Octokit({
83+
auth: await getGitHubAuthToken(['repo']),
84+
});
85+
const { data: pr } = await octokit.pulls.get({
86+
...ELECTRON_REPO_DATA,
87+
pull_number: prNumber,
88+
});
89+
if (!pr.merge_commit_sha) {
90+
fatal('No merge SHA available on PR');
91+
return;
92+
}
93+
94+
const initialVersion = await getChromiumVersion(octokit, pr.base.sha);
95+
const newVersion = await getChromiumVersion(octokit, pr.head.sha);
96+
97+
if (initialVersion === newVersion) {
98+
fatal('Does not look like a Chromium roll PR');
99+
return;
100+
}
101+
102+
if (
103+
compareChromiumVersions(chromiumVersionStr, initialVersion) < 0 ||
104+
compareChromiumVersions(chromiumVersionStr, newVersion) > 0
105+
) {
106+
fatal(
107+
`Chromium version ${chalk.blueBright(chromiumVersionStr)} is not between ${chalk.blueBright(initialVersion)} and ${chalk.blueBright(newVersion)}`,
108+
);
109+
return;
110+
}
111+
112+
// TODO(dsanders11) - confirm chromiumVersionStr is a tagged Chromium version
113+
114+
// TODO(dsanders11): pull list of possible Chromium versions for the
115+
// PR in question and make the version argument optional, and show the
116+
// user a list of versions to choose from if they don't provide one
117+
/**
118+
const { version: chromiumVersion } = await inquirer.prompt([
119+
{
120+
type: 'list',
121+
name: 'version',
122+
message: 'Which Chromium version do you want to reconstruct?',
123+
choices: chromiumVersions,
124+
},
125+
]);
126+
*/
127+
128+
const config = evmConfig.current();
129+
const gitOpts = {
130+
cwd: path.resolve(config.root, 'src', 'electron'),
131+
stdio: 'pipe',
132+
};
133+
const result = spawnSync(config, 'git', ['status', '--porcelain'], gitOpts);
134+
if (result.status !== 0 || result.stdout.toString().trim().length !== 0) {
135+
fatal(
136+
"Your current git working directory is not clean, we won't erase your local changes. Clean it up and try again",
137+
);
138+
return;
139+
}
140+
141+
// Checkout the parent of the merge commit if it was merged, else the base SHA
142+
let targetSha = pr.base.sha;
143+
144+
if (pr.merged) {
145+
const { data: mergeCommit } = await octokit.git.getCommit({
146+
...ELECTRON_REPO_DATA,
147+
commit_sha: pr.merge_commit_sha,
148+
});
149+
if (mergeCommit.parents.length !== 1) {
150+
fatal('Expected merge commit to have one parent');
151+
return;
152+
}
153+
targetSha = mergeCommit.parents[0].sha;
154+
}
155+
156+
const ensureShaLocal = spawnSync(config, 'git', ['fetch', 'origin', targetSha], gitOpts);
157+
if (ensureShaLocal.status !== 0) {
158+
fatal('Failed to fetch upstream base');
159+
return;
160+
}
161+
162+
const checkoutResult = spawnSync(config, 'git', ['checkout', targetSha], gitOpts);
163+
if (checkoutResult.status !== 0) {
164+
fatal('Failed to checkout base commit');
165+
return;
166+
}
167+
168+
const rcvBranch = `rcv/pr/${prNumber}/version/${chromiumVersionStr}`;
169+
spawnSync(config, 'git', ['branch', '-D', rcvBranch], gitOpts);
170+
const checkoutBranchResult = spawnSync(config, 'git', ['checkout', '-b', rcvBranch], gitOpts);
171+
if (checkoutBranchResult.status !== 0) {
172+
fatal(`Failed to checkout new branch "${rcvBranch}"`);
173+
return;
174+
}
175+
176+
const yarnInstallResult = spawnSync(config, 'yarn', ['install'], gitOpts);
177+
if (yarnInstallResult.status !== 0) {
178+
fatal(`Failed to do "yarn install" on new branch`);
179+
return;
180+
}
181+
182+
// Update the Chromium version in DEPS
183+
const regexToReplace = new RegExp(`(chromium_version':\n +').+?',`, 'gm');
184+
const content = await fs.promises.readFile(path.resolve(gitOpts.cwd, 'DEPS'), 'utf8');
185+
const newContent = content.replace(regexToReplace, `$1${chromiumVersionStr}',`);
186+
await fs.promises.writeFile(path.resolve(gitOpts.cwd, 'DEPS'), newContent, 'utf8');
187+
188+
// Make a commit with this change
189+
const addResult = spawnSync(config, 'git', ['add', 'DEPS'], gitOpts);
190+
if (addResult.status !== 0) {
191+
fatal('Failed to add DEPS file for commit');
192+
return;
193+
}
194+
195+
const author = 'Electron Bot <[email protected]>';
196+
const message = `chore: bump chromium to ${chromiumVersionStr}`;
197+
const commitResult = spawnSync(
198+
config,
199+
'git',
200+
[
201+
'commit',
202+
'--no-verify', // There's a bug on Windows that creates incorrect changes
203+
'--author',
204+
os.platform() === 'win32' ? `"${author}"` : author,
205+
'-m',
206+
os.platform() === 'win32' ? `"${message}"` : message,
207+
],
208+
gitOpts,
209+
);
210+
if (commitResult.status !== 0) {
211+
fatal('Failed to commit DEPS file change');
212+
return;
213+
}
214+
215+
const { data: commits } = await octokit.pulls.listCommits({
216+
...ELECTRON_REPO_DATA,
217+
pull_number: prNumber,
218+
});
219+
220+
const commitsToCherryPick = [];
221+
222+
for (const commit of commits) {
223+
const shortSha = commit.sha.substring(0, 7);
224+
const message = commit.commit.message.split('\n')[0];
225+
const clMatch = CL_REGEX.exec(commit.commit.message);
226+
227+
if (clMatch) {
228+
const parsedUrl = new URL(clMatch[0]);
229+
const { shortCommit: chromiumShortSha } = await getGerritPatchDetailsFromURL(parsedUrl);
230+
const { commits: chromiumCommits } = await fetch(
231+
`https://chromiumdash.appspot.com/fetch_commits?commit=${chromiumShortSha}`,
232+
).then((resp) => resp.json());
233+
if (chromiumCommits.length !== 1) {
234+
fatal(`Expected to find exactly one commit for SHA "${chromiumShortSha}"`);
235+
return;
236+
}
237+
// Grab the earliest Chromium version the CL was released in, and the merge time
238+
const { earliest, time } = chromiumCommits[0];
239+
240+
// Only cherry pick the commit if the earliest version is within target version
241+
if (compareChromiumVersions(earliest, chromiumVersionStr) <= 0) {
242+
console.log(
243+
`${color.success} Cherry-picking CL commit: ${chalk.yellow(shortSha)} ${message} (${chalk.greenBright(earliest)})`,
244+
);
245+
commitsToCherryPick.push({ sha: commit.sha, chromiumVersion: earliest, mergeTime: time });
246+
} else {
247+
console.info(
248+
`${color.info} Skipping CL commit: ${chalk.yellow(shortSha)} ${message} (${chalk.greenBright(earliest)})`,
249+
);
250+
}
251+
} else {
252+
console.info(`${color.info} Skipping non-CL commit: ${chalk.yellow(shortSha)} ${message}`);
253+
}
254+
}
255+
256+
// Reorder the commits by the merge time of their CL
257+
if (options.sort) {
258+
commitsToCherryPick.sort((a, b) => a.mergeTime - b.mergeTime);
259+
}
260+
261+
for (const commit of commitsToCherryPick) {
262+
const ensureCommitLocal = spawnSync(config, 'git', ['fetch', 'origin', commit.sha], gitOpts);
263+
if (ensureCommitLocal.status !== 0) {
264+
fatal('Failed to fetch commit to cherry-pick');
265+
return;
266+
}
267+
268+
const cherryPickResult = spawnSync(
269+
config,
270+
'git',
271+
[
272+
'cherry-pick',
273+
'--allow-empty',
274+
`--strategy-option=${options.mergeStrategyOption}`,
275+
commit.sha,
276+
],
277+
gitOpts,
278+
);
279+
if (cherryPickResult.status !== 0) {
280+
fatal(`Failed to cherry-pick commit "${commit.sha}"`);
281+
return;
282+
}
283+
}
284+
285+
// TODO - update filenames and commit
286+
});
287+
288+
program.parse(process.argv);

0 commit comments

Comments
 (0)