Skip to content

Commit

Permalink
chore: add script to pull commits to cherrypick (#16637)
Browse files Browse the repository at this point in the history
* chore: add script to pull commits to cherrypick

* lint
  • Loading branch information
andrewseguin authored and mmalerba committed Aug 8, 2019
1 parent 2994365 commit 917871b
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 38 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"preinstall": "node ./tools/npm/check-npm.js",
"format:ts": "git-clang-format HEAD $(git diff HEAD --name-only | grep -v \"\\.d\\.ts\")",
"format:bazel": "yarn -s bazel:buildifier --lint=fix --mode=fix",
"format": "yarn -s format:ts && yarn -s format:bazel"
"format": "yarn -s format:ts && yarn -s format:bazel",
"cherry-pick-patch": "ts-node --project tools/cherry-pick-patch/ tools/cherry-pick-patch/cherry-pick-patch.ts"
},
"version": "8.1.2",
"requiredAngularVersion": "^8.0.0 || ^9.0.0-0",
Expand Down Expand Up @@ -72,7 +73,7 @@
"@bazel/karma": "0.32.2",
"@bazel/typescript": "0.32.2",
"@firebase/app-types": "^0.3.2",
"@octokit/rest": "^15.9.4",
"@octokit/rest": "^16.28.7",
"@schematics/angular": "^8.0.3",
"@types/browser-sync": "^0.0.42",
"@types/chalk": "^0.4.31",
Expand Down
53 changes: 53 additions & 0 deletions tools/cherry-pick-patch/cherry-pick-patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {GitHub} from './github';
import {outputResults} from './output-results';
import {
requestLatestCherryPickedCommitSha,
requestPatchBranch,
verifyLatestCherryPickedCommit
} from './prompt';


/**
* Task to run the script that attempts to produce cherry-pick commands for the patch branch.
*/
class CherryPickPatchTask {
github = new GitHub();

async run() {
const patchBranchSuggestion = await this.github.getPatchBranchSuggestion();
const branch = await requestPatchBranch(patchBranchSuggestion);
const sha = await this.getLatestCherryPickedCommitSha(branch);

const commit = await this.github.getCommit(sha);
const pullRequests = await this.github.getPatchPullRequestsSince(commit.commit.author.date);

outputResults(pullRequests);
}

/** Returns the commit SHA of the last cherry-picked commit on master. */
async getLatestCherryPickedCommitSha(branch: any): Promise<string> {
const commits = await this.github.listCommits(branch);

/** Gets the SHA from the string: "(cherry picked from commit 4c6eeb9aba73d3)" */
const regexp = new RegExp('cherry picked from commit (.*[^)])');
const latestShas = commits
.map(d => {
const result = d.commit.message.match(regexp);
return result ? result[1] : null;
})
.filter(d => !!d);

const latestSha = latestShas[0];
if (!latestSha) {
return await requestLatestCherryPickedCommitSha();
} else {
const commit = await this.github.getCommit(latestSha);
return await verifyLatestCherryPickedCommit(commit);
}
}
}

/** Entry-point for the script. */
if (require.main === module) {
new CherryPickPatchTask().run();
}
Empty file.
88 changes: 88 additions & 0 deletions tools/cherry-pick-patch/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as OctokitApi from '@octokit/rest';

// TODO: Consider using local git information for the data to avoid worrying about rate limits. */
/** Class to act as an interface to the GitHub API. */
export class GitHub {
// TODO: Use an authentication token to increase rate limits.
/** Octokit API instance that can be used to make Github API calls. */
private _api = new OctokitApi();

/** Owner of the repository to query. */
private _owner = 'angular';

/** Name of the repository to query. */
private _name = 'components';

/**
* Retrieves merged patch-eligible pull requests that have been merged since the date.
* Results are sorted by merge date.
*/
async getPatchPullRequestsSince(dateSince: string): Promise<OctokitApi.PullsGetResponse[]> {
const query = 'base:master is:pr -label:"target: minor" -label:"target: major" is:merged' +
` merged:>${dateSince}`;
const result = await this._search(query);

// Load information for each pull request. Waits for each pull request response until loading
// the next pull request to avoid GitHub's abuse detection (too many calls in a short amount
// of time).
const pullRequests: OctokitApi.PullsGetResponse[] = [];
for (let i = 0; i < result.items.length; i++) {
pullRequests.push(await this.loadPullRequest(result.items[i].number));
}

// Sort by merge date.
pullRequests.sort((a, b) => (a.merged_at < b.merged_at) ? -1 : 1);
return pullRequests;
}

/** Loads the information for the provided pull request number. */
async loadPullRequest(prNumber: number): Promise<OctokitApi.PullsGetResponse> {
const response = await this._api.pulls.get({
owner: this._owner,
repo: this._name,
pull_number: prNumber,
});
return response.data;
}

/** Gets the commit information for the given SHA. */
async getCommit(sha: string): Promise<OctokitApi.ReposGetCommitResponse> {
const response = await this._api.repos.getCommit({
owner: this._owner,
repo: this._name,
ref: sha,
});

return response.data;
}

/** Retrieves the list of latest commits from the branch. */
async listCommits(branch: string): Promise<OctokitApi.ReposListCommitsResponse> {
const response = await this._api.repos.listCommits({
owner: this._owner,
repo: this._name,
sha: branch,
});

return response.data;
}

// TODO: Handle pagination in case there are more than 100 results.
/** Gets a suggestion for the latest patch branch. */
async getPatchBranchSuggestion(): Promise<string> {
const response = await this._api.repos.listBranches({owner: this._owner, repo: this._name});

// Matches branch names that have two digits separated by period and ends with an x
const patchBranches =
response.data.map(branch => branch.name).filter(name => !!/^\d+\.\d+\.x$/g.exec(name));
return patchBranches.pop() || '';
}

// TODO: Handle pagination in case there are more than 100 results.
/** Searches the repository using the provided query. */
private async _search(query: string): Promise<{items: any[]}> {
const scopedQuery = `repo:${this._owner}/${this._name} ${query}`;
const result = await this._api.search.issuesAndPullRequests({per_page: 100, q: scopedQuery});
return result.data;
}
}
34 changes: 34 additions & 0 deletions tools/cherry-pick-patch/output-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {PullsGetResponse} from '@octokit/rest';
import {cyan} from 'chalk';

/** Outputs the information of the pull requests to be cherry-picked and the commands to run. */
export function outputResults(pullRequests: PullsGetResponse[]) {
if (!pullRequests.length) {
console.log('No pull requests need to be cherry-picked');
return;
}

console.log();
console.log(cyan('------------------------'));
console.log(cyan(' Results '));
console.log(cyan('------------------------'));
console.log();

pullRequests.forEach(p => {
const data = [p.number, p.merged_at, p.merge_commit_sha, p.html_url, p.title];
console.log(data.join('\t'));
});

console.log();
console.log(cyan('------------------------'));
console.log(cyan(' Cherry Pick Commands'));
console.log(cyan('------------------------'));

pullRequests.forEach((pr, index) => {
if (index % 5 === 0) {
console.log();
}

console.log(`git cherry-pick -x ${pr.merge_commit_sha};`);
});
}
43 changes: 43 additions & 0 deletions tools/cherry-pick-patch/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {prompt} from 'inquirer';
import * as OctokitApi from '@octokit/rest';

/** Requests the user to provide the name of the patch branch. */
export async function requestPatchBranch(suggestion: string): Promise<string> {
const result = await prompt<{branch: string}>([{
type: 'input',
name: 'branch',
message: `What is the name of the current patch branch?`,
default: suggestion || null,
}]);

return result.branch;
}

/** Confirms the latest cherry-picked commit on master; requests one if not confirmed. */
export async function verifyLatestCherryPickedCommit(commit: OctokitApi.ReposGetCommitResponse) {
console.log(`\nThe last cherry-picked commit on master is "${commit.commit.message}"`);

const result = await prompt<{confirm: boolean}>([{
type: 'confirm',
name: 'confirm',
message: `Is this correct?`,
default: true,
}]);

if (!result.confirm) {
return await requestLatestCherryPickedCommitSha();
} else {
return commit.sha;
}
}

/** Requests the SHA of the latest cherry picked commit on master. */
export async function requestLatestCherryPickedCommitSha(): Promise<string> {
const result = await prompt<{sha: string}>([{
type: 'input',
name: 'sha',
message: `What is the SHA of the latest cherry-picked commit on master?`,
}]);

return result.sha;
}
7 changes: 7 additions & 0 deletions tools/cherry-pick-patch/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"lib": ["es2016"],
"types": ["node"],
"strictNullChecks": true
}
}
Loading

0 comments on commit 917871b

Please sign in to comment.