Skip to content

Commit fcf57db

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

File tree

8 files changed

+398
-38
lines changed

8 files changed

+398
-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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as OctokitApi from '@octokit/rest';
2+
3+
import {GitHub} from './github';
4+
import {outputResults} from './output-results';
5+
import {
6+
requestLatestCherryPickedCommitSha,
7+
requestPatchBranch,
8+
verifyLatestCherryPickedCommit
9+
} from './prompt';
10+
11+
12+
// TODO: Allow user to input this in the future
13+
/** Repository that will be used to check for commits. */
14+
const REPOSITORY = 'angular/components';
15+
16+
const OWNER = REPOSITORY.split('/')[0];
17+
const NAME = REPOSITORY.split('/')[1];
18+
19+
/** Octokit API instance that can be used to make Github API calls. */
20+
const github = new GitHub(OWNER, NAME);
21+
22+
/**
23+
* Class that runs to determine what commands should be run to cherry-pick from master to
24+
* the latest patch branch.
25+
*/
26+
class CherryPickPatchTask {
27+
async run() {
28+
const patchBranchSuggestion = await github.getPatchBranchSuggestion();
29+
const branch = await requestPatchBranch(patchBranchSuggestion);
30+
const sha = await this.getLatestCherryPickedCommitSha(branch);
31+
32+
const commit = await github.getCommit(sha);
33+
const pullRequests = await github.getPatchPullRequests(commit.commit.author.date);
34+
35+
outputResults(pullRequests);
36+
}
37+
38+
/** Returns the commit SHA of the last cherry-picked commit on master. */
39+
async getLatestCherryPickedCommitSha(branch): Promise<string> {
40+
const commits = await github.listCommits(branch);
41+
augmentResponse(commits);
42+
43+
/** Gets the SHA from the string: "(cherry picked from commit 4c6eeb9aba73d3)" */
44+
const regexp = new RegExp('cherry picked from commit (.*[^)])');
45+
const latestShas = commits
46+
.map(d => {
47+
const result = d.commit.message.match(regexp);
48+
return result ? result[1] : null;
49+
})
50+
.filter(d => !!d);
51+
52+
const latestSha = latestShas[0];
53+
if (!latestSha) {
54+
return await requestLatestCherryPickedCommitSha();
55+
} else {
56+
const commit = await github.getCommit(latestSha);
57+
return await verifyLatestCherryPickedCommit(commit);
58+
}
59+
}
60+
}
61+
62+
/** Entry-point for the script. */
63+
if (require.main === module) {
64+
new CherryPickPatchTask().run();
65+
}
66+
67+
// TODO: Remove me.
68+
function augmentResponse(commits: OctokitApi.ReposListCommitsResponse) {
69+
commits.forEach(d => {
70+
d.commit.message = 'blah blah blah (cherry picked from commit' +
71+
' 5c87286bd6245010b6e0b429124c6eeb9aba73d3)';
72+
});
73+
}

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

Whitespace-only changes.

tools/cherry-pick-patch/github.ts

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