Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 84 additions & 56 deletions src/e-cherry-pick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';

import { program } from 'commander';
import { Command } from 'commander';
import { Octokit } from '@octokit/rest';

const program = new Command();

import * as evmConfig from './evm-config.js';
import debug from 'debug';
import { getGerritPatchDetailsFromURL } from './utils/gerrit.js';
Expand All @@ -22,7 +24,7 @@ const ELECTRON_REPO_DATA = {
repo: 'electron',
};

interface PatchDetails {
export interface PatchDetails {
patchDirName: string;
shortCommit: string;
patch: string;
Expand Down Expand Up @@ -77,14 +79,84 @@ async function getGitHubPatchDetailsFromURL(
};
}

function isUrl(arg: string) {
export function isUrl(arg: string) {
return arg.startsWith('https://') || arg.startsWith('http://');
}

function commitSubject(patch: string) {
export function commitSubject(patch: string) {
return /Subject: \[PATCH\] (.+?)$/m.exec(patch)?.[1]?.trim() ?? '';
}

// The first positional argument is expected to be a patch URL and the second a
// target branch, but users frequently transpose them — accept either order.
// Any remaining positional that looks like a URL is another patch to include
// in the same PR; everything else is another target branch.
export function splitPositionalArgs(
patchUrlStr: string,
targetBranch: string,
rest: string[],
): { patchUrls: string[]; targetBranches: string[] } {
if (isUrl(targetBranch)) {
const tmp = patchUrlStr;
patchUrlStr = targetBranch;
targetBranch = tmp;
}

const patchUrls = [patchUrlStr, ...rest.filter(isUrl)];
const targetBranches = [targetBranch, ...rest.filter((a) => !isUrl(a))];
return { patchUrls, targetBranches };
}

export function computeBatchId(patchUrls: string[]): string {
return crypto.createHash('sha256').update(patchUrls.join('\n')).digest('hex').slice(0, 12);
}

export function formatPRTitleAndBody({
patches,
security,
}: {
patches: PatchDetails[];
security: boolean;
}): { title: string; body: string } {
const isBatch = patches.length > 1;
const first = patches[0]!;

if (isBatch) {
const patchDirNames = [...new Set(patches.map((p) => p.patchDirName))];
const title = `chore: cherry-pick ${patches.length} changes from ${patchDirNames.join(', ')}`;
const lines = patches.map((p) => {
const ref = p.cve || p.bugNumber || p.shortCommit;
return `* ${p.shortCommit} from ${p.patchDirName} — ${commitSubject(p.patch)} (${ref})`;
});
const notes = patches
.map((p) => p.cve || p.bugNumber)
.filter(Boolean)
.join(', ');
const body =
`Backports the following changes:\n\n${lines.join('\n')}\n\n` +
`Notes: ${
notes
? security
? `Security: backported fixes for ${notes}.`
: `Backported fixes for ${notes}.`
: `<!-- couldn't find bug numbers -->`
}`;
return { title, body };
}

const { shortCommit, patchDirName, patch, bugNumber, cve } = first;
const title = `chore: cherry-pick ${shortCommit} from ${patchDirName}`;
const commitMessage = /Subject: \[PATCH\] (.+?)^---$/ms.exec(patch)?.[1] ?? '';
const body = `${commitMessage}\n\nNotes: ${
bugNumber
? security
? `Security: backported fix for ${cve || bugNumber}.`
: `Backported fix for ${bugNumber}.`
: `<!-- couldn't find bug number -->`
}`;
return { title, body };
}

program
.arguments('<patch-url> <target-branch> [additionalBranchesOrUrls...]')
.option('--security', 'Whether this backport is for security reasons')
Expand All @@ -103,17 +175,7 @@ program
rest: string[],
{ security, cveLookup }: { security?: boolean; cveLookup: boolean },
) => {
if (isUrl(targetBranch)) {
const tmp = patchUrlStr;
patchUrlStr = targetBranch;
targetBranch = tmp;
}

// Any positional argument that looks like a URL is treated as an
// additional patch to include in the same PR; everything else is an
// additional target branch to also raise a PR against.
const patchUrls = [patchUrlStr, ...rest.filter(isUrl)];
const targetBranches = [targetBranch, ...rest.filter((a) => !isUrl(a))];
const { patchUrls, targetBranches } = splitPositionalArgs(patchUrlStr, targetBranch, rest);

const octokit = new Octokit({
auth: await getGitHubAuthToken(['repo']),
Expand Down Expand Up @@ -141,12 +203,7 @@ program
}

const isBatch = patches.length > 1;
const patchDirNames = [...new Set(patches.map((p) => p.patchDirName))];
const batchId = crypto
.createHash('sha256')
.update(patchUrls.join('\n'))
.digest('hex')
.slice(0, 12);
const batchId = computeBatchId(patchUrls);

d(`Cloning electron/electron to ${tmp}`);
cp.execSync(`git clone ${evmConfig.current().remotes.electron.origin}`, { cwd: tmp });
Expand Down Expand Up @@ -215,39 +272,7 @@ program
stdio: 'ignore',
});

let title: string;
let body: string;
if (isBatch) {
title = `chore: cherry-pick ${patches.length} changes from ${patchDirNames.join(', ')}`;
const lines = patches.map((p) => {
const ref = p.cve || p.bugNumber || p.shortCommit;
return `* ${p.shortCommit} from ${p.patchDirName} — ${commitSubject(p.patch)} (${ref})`;
});
const notes = patches
.map((p) => p.cve || p.bugNumber)
.filter(Boolean)
.join(', ');
body =
`Backports the following changes:\n\n${lines.join('\n')}\n\n` +
`Notes: ${
notes
? security
? `Security: backported fixes for ${notes}.`
: `Backported fixes for ${notes}.`
: `<!-- couldn't find bug numbers -->`
}`;
} else {
const { shortCommit, patchDirName, patch, bugNumber, cve } = first;
title = `chore: cherry-pick ${shortCommit} from ${patchDirName}`;
const commitMessage = /Subject: \[PATCH\] (.+?)^---$/ms.exec(patch)?.[1] ?? '';
body = `${commitMessage}\n\nNotes: ${
bugNumber
? security
? `Security: backported fix for ${cve || bugNumber}.`
: `Backported fix for ${bugNumber}.`
: `<!-- couldn't find bug number -->`
}`;
}
const { title, body } = formatPRTitleAndBody({ patches, security: !!security });

d(`Creating PR for ${branchName}`);
const { data: pr } = await octokit.pulls.create({
Expand Down Expand Up @@ -288,5 +313,8 @@ program
}
}
},
)
.parse(process.argv);
);

if (import.meta.main) {
program.parse(process.argv);
}
4 changes: 3 additions & 1 deletion src/e-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import * as cp from 'node:child_process';
import * as path from 'node:path';

import { program } from 'commander';
import { Command } from 'commander';

import * as evmConfig from './evm-config.js';

const program = new Command();
import { color, fatal } from './utils/logging.js';
import open from 'open';

Expand Down
4 changes: 3 additions & 1 deletion src/e-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import * as querystring from 'node:querystring';

import extractZip from 'extract-zip';
import * as semver from 'semver';
import { program } from 'commander';
import { Command } from 'commander';
import * as inquirer from '@inquirer/prompts';
import { Octokit } from '@octokit/rest';

import debug from 'debug';

const program = new Command();
import { progressStream } from './utils/download.js';
import { getGitHubAuthToken } from './utils/github-auth.js';
import open from 'open';
Expand Down
4 changes: 3 additions & 1 deletion src/e-rcv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { styleText } from 'node:util';

import * as inquirer from '@inquirer/prompts';
import { Octokit } from '@octokit/rest';
import { program } from 'commander';
import { Command } from 'commander';

const program = new Command();

import * as evmConfig from './evm-config.js';
import { spawnSync, type DepotOpts } from './utils/depot-tools.js';
Expand Down
Loading