Skip to content

Commit 88c5872

Browse files
committed
chore: add script to pull commits to cherrypick
1 parent 88631b9 commit 88c5872

File tree

8 files changed

+385
-38
lines changed

8 files changed

+385
-38
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"preinstall": "node ./tools/npm/check-npm.js",
3333
"format:ts": "git-clang-format HEAD $(git diff HEAD --name-only | grep -v \"\\.d\\.ts\")",
3434
"format:bazel": "yarn -s bazel:buildifier --lint=fix --mode=fix",
35-
"format": "yarn -s format:ts && yarn -s format:bazel"
35+
"format": "yarn -s format:ts && yarn -s format:bazel",
36+
"cherry-pick-patch": "ts-node --project tools/cherry-pick-patch/ tools/cherry-pick-patch/cherry-pick-patch.ts"
3637
},
3738
"version": "8.1.1",
3839
"requiredAngularVersion": "^8.0.0 || ^9.0.0-0",
@@ -71,7 +72,7 @@
7172
"@bazel/karma": "0.32.2",
7273
"@bazel/typescript": "0.32.2",
7374
"@firebase/app-types": "^0.3.2",
74-
"@octokit/rest": "^15.9.4",
75+
"@octokit/rest": "^16.28.7",
7576
"@schematics/angular": "^8.0.3",
7677
"@types/browser-sync": "^0.0.42",
7778
"@types/chalk": "^0.4.31",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {GitHub} from './github';
2+
import {outputResults} from './output-results';
3+
import {
4+
requestLatestCherryPickedCommitSha,
5+
requestPatchBranch,
6+
verifyLatestCherryPickedCommit
7+
} from './prompt';
8+
9+
10+
/**
11+
* Task to run the script that attempts to produce cherry-pick commands for the patch branch.
12+
*/
13+
class CherryPickPatchTask {
14+
github = new GitHub();
15+
16+
async run() {
17+
const patchBranchSuggestion = await this.github.getPatchBranchSuggestion();
18+
const branch = await requestPatchBranch(patchBranchSuggestion);
19+
const sha = await this.getLatestCherryPickedCommitSha(branch);
20+
21+
const commit = await this.github.getCommit(sha);
22+
const pullRequests = await this.github.getPatchPullRequestsSince(commit.commit.author.date);
23+
24+
outputResults(pullRequests);
25+
}
26+
27+
/** Returns the commit SHA of the last cherry-picked commit on master. */
28+
async getLatestCherryPickedCommitSha(branch): Promise<string> {
29+
const commits = await this.github.listCommits(branch);
30+
31+
/** Gets the SHA from the string: "(cherry picked from commit 4c6eeb9aba73d3)" */
32+
const regexp = new RegExp('cherry picked from commit (.*[^)])');
33+
const latestShas = commits
34+
.map(d => {
35+
const result = d.commit.message.match(regexp);
36+
return result ? result[1] : null;
37+
})
38+
.filter(d => !!d);
39+
40+
const latestSha = latestShas[0];
41+
if (!latestSha) {
42+
return await requestLatestCherryPickedCommitSha();
43+
} else {
44+
const commit = await this.github.getCommit(latestSha);
45+
return await verifyLatestCherryPickedCommit(commit);
46+
}
47+
}
48+
}
49+
50+
/** Entry-point for the script. */
51+
if (require.main === module) {
52+
new CherryPickPatchTask().run();
53+
}

tools/cherry-pick-patch/get-merged-pull-requests.ts

Whitespace-only changes.

tools/cherry-pick-patch/github.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as OctokitApi from '@octokit/rest';
2+
import {PullsGetResponse} from '@octokit/rest';
3+
4+
// TODO: Consider using local git information for the data to avoid worrying about rate limits. */
5+
/** Class to act as an interface to the GitHub API. */
6+
export class GitHub {
7+
// TODO: Use an authentication token to increase rate limits.
8+
/** Octokit API instance that can be used to make Github API calls. */
9+
private _api = new OctokitApi();
10+
11+
/** Owner of the repository to query. */
12+
private _owner = 'angular';
13+
14+
/** Name of the repository to query. */
15+
private _name = 'components';
16+
17+
/**
18+
* Retrieves merged patch-eligible pull requests that have been merged since the date.
19+
* Results are sorted by merge date.
20+
*/
21+
async getPatchPullRequestsSince(dateSince: string): Promise<OctokitApi.PullsGetResponse[]> {
22+
const query = 'base:master is:pr -label:"target: minor" -label:"target: major" is:merged' +
23+
` merged:>${dateSince}`;
24+
const result = await this._search(query);
25+
26+
// Load information for each pull request. Waits for each pull request response until loading
27+
// the next pull request to avoid GitHub's abuse detection (too many calls in a short amount
28+
// of time).
29+
const pullRequests: PullsGetResponse[] = [];
30+
for (let i = 0; i < result.items.length; i++) {
31+
pullRequests.push(await this.loadPullRequest(result.items[i].number));
32+
}
33+
34+
// Sort by merge date.
35+
pullRequests.sort((a, b) => (a.merged_at < b.merged_at) ? -1 : 1);
36+
return pullRequests;
37+
}
38+
39+
/** Loads the information for the provided pull request number. */
40+
async loadPullRequest(prNumber: number): Promise<OctokitApi.PullsGetResponse> {
41+
const response = await this._api.pulls.get({
42+
owner: this._owner,
43+
repo: this._name,
44+
pull_number: prNumber,
45+
});
46+
return response.data;
47+
}
48+
49+
/** Gets the commit information for the given SHA. */
50+
async getCommit(sha: string): Promise<OctokitApi.ReposGetCommitResponse> {
51+
const response = await this._api.repos.getCommit({
52+
owner: this._owner,
53+
repo: this._name,
54+
ref: sha,
55+
});
56+
57+
return response.data;
58+
}
59+
60+
/** Retrieves the list of latest commits from the branch. */
61+
async listCommits(branch: string): Promise<OctokitApi.ReposListCommitsResponse> {
62+
const response = await this._api.repos.listCommits({
63+
owner: this._owner,
64+
repo: this._name,
65+
sha: branch,
66+
});
67+
68+
return response.data;
69+
}
70+
71+
// TODO: Handle pagination in case there are more than 100 results.
72+
/** Gets a suggestion for the latest patch branch. */
73+
async getPatchBranchSuggestion(): Promise<string> {
74+
const response = await this._api.repos.listBranches({owner: this._owner, repo: this._name});
75+
76+
// Matches branch names that have two digits separated by period and ends with an x
77+
const patchBranches =
78+
response.data.map(branch => branch.name).filter(name => !!/^\d+\.\d+\.x$/g.exec(name));
79+
return patchBranches.pop() || '';
80+
}
81+
82+
// TODO: Handle pagination in case there are more than 100 results.
83+
/** Searches the repository using the provided query. */
84+
private async _search(query: string): Promise<{items: any[]}> {
85+
const scopedQuery = `repo:${this._owner}/${this._name} ${query}`;
86+
const result = await this._api.search.issuesAndPullRequests({per_page: 100, q: scopedQuery});
87+
return result.data;
88+
}
89+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {PullsGetResponse} from '@octokit/rest';
2+
import {cyan} from 'chalk';
3+
4+
/** Outputs the information of the pull requests to be cherry-picked and the commands to run. */
5+
export function outputResults(pullRequests: PullsGetResponse[]) {
6+
if (!pullRequests.length) {
7+
console.log('No pull requests need to be cherry-picked');
8+
return;
9+
}
10+
11+
console.log();
12+
console.log(cyan('------------------------'));
13+
console.log(cyan(' Results '));
14+
console.log(cyan('------------------------'));
15+
console.log();
16+
17+
pullRequests.forEach(p => {
18+
const data = [p.number, p.merged_at, p.merge_commit_sha, p.html_url, p.title];
19+
console.log(data.join('\t'));
20+
});
21+
22+
console.log();
23+
console.log(cyan('------------------------'));
24+
console.log(cyan(' Cherry Pick Commands'));
25+
console.log(cyan('------------------------'));
26+
27+
pullRequests.forEach((pr, index) => {
28+
if (index % 5 === 0) {
29+
console.log();
30+
}
31+
32+
console.log(`git cherry-pick -x ${pr.merge_commit_sha};`);
33+
});
34+
}

tools/cherry-pick-patch/prompt.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {prompt} from 'inquirer';
2+
import * as OctokitApi from '@octokit/rest';
3+
4+
/** Requests the user to provide the name of the patch branch. */
5+
export async function requestPatchBranch(suggestion: string): Promise<string> {
6+
const result = await prompt<{branch: string}>([{
7+
type: 'input',
8+
name: 'branch',
9+
message: `What is the name of the current patch branch?`,
10+
default: suggestion || null,
11+
}]);
12+
13+
return result.branch;
14+
}
15+
16+
/** Confirms the latest cherry-picked commit on master; requests one if not confirmed. */
17+
export async function verifyLatestCherryPickedCommit(commit: OctokitApi.ReposGetCommitResponse) {
18+
console.log(`\nThe last cherry-picked commit on master is "${commit.commit.message}"`);
19+
20+
const result = await prompt<{confirm: boolean}>([{
21+
type: 'confirm',
22+
name: 'confirm',
23+
message: `Is this correct?`,
24+
default: true,
25+
}]);
26+
27+
if (!result.confirm) {
28+
return await requestLatestCherryPickedCommitSha();
29+
} else {
30+
return commit.sha;
31+
}
32+
}
33+
34+
/** Requests the SHA of the latest cherry picked commit on master. */
35+
export async function requestLatestCherryPickedCommitSha(): Promise<string> {
36+
const result = await prompt<{sha: string}>([{
37+
type: 'input',
38+
name: 'sha',
39+
message: `What is the SHA of the latest cherry-picked commit on master?`,
40+
}]);
41+
42+
return result.sha;
43+
}

tools/cherry-pick-patch/tsconfig.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["es2016"],
4+
"types": ["node"],
5+
"strictNullChecks": true
6+
}
7+
}

0 commit comments

Comments
 (0)