Skip to content
This repository has been archived by the owner on Jan 23, 2025. It is now read-only.

feat: split the approve command #164

Merged
merged 1 commit into from
Oct 3, 2018
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
187 changes: 14 additions & 173 deletions src/approve-prs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,190 +12,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @fileoverview A quick'n'dirty console UI to approve a bunch of pull requests.
* Usage: `node approve-prs.js [regex]` -- will go through open PRs with title
* matching `regex`, one by one. Without a regex, will go through all open PRs.
*/

'use strict';

import axios from 'axios';
import {GitHub, GitHubRepository, PullRequest} from './lib/github';
import {question} from './lib/question';
import {GitHubRepository, PullRequest} from './lib/github';
import * as meow from 'meow';
import {getConfig} from './lib/config';

/**
* Downloads and prints patch file (well, actually, any file) to a console.
* @param {string} patchUrl URL to download.
*/
async function showPatch(patchUrl: string) {
const axiosResult = await axios.get(patchUrl);
const patch = axiosResult.data;
console.log(patch);
}

/**
* Process one pull request: ask the user to approve, show patch, or skip it.
* @param {GitHubRepository} repository GitHub repository for this pull request.
* @param {Object} pr Pull request object, as returned by GitHub API.
* @returns boolean True if successfully processed.
*/
async function processPullRequest(
repository: GitHubRepository, pr: PullRequest,
auto: boolean): Promise<boolean> {
const title = pr.title;
const htmlUrl = pr.html_url;
const patchUrl = pr.patch_url;
const author = pr.user.login;
const baseSha = pr.base.sha;
const ref = pr.head.ref;

console.log(` [${author}] ${htmlUrl}: ${title}`);
import {process} from './lib/prIterator';

let latestCommit: {[index: string]: string};
async function processMethod(repository: GitHubRepository, pr: PullRequest) {
console.log(` [${pr.user.login}] ${pr.html_url}: ${pr.title}`);
try {
latestCommit = await repository.getLatestCommitToMaster();
await repository.approvePullRequest(pr);
console.log(' approved!');
} catch (err) {
console.warn(
' cannot get sha of latest commit to master, skipping:',
err.toString());
` error trying to approve PR ${pr.html_url}:`, err.toString());
return false;
}
const latestMasterSha = latestCommit['sha'];

if (latestMasterSha !== baseSha) {
for (;;) {
let response: string;
if (auto) {
response = 'u';
} else {
response = await question(
'PR branch is out of date. What to do? [u]pdate branch, show [p]atch, [s]kip: ');
}

if (response === 'u') {
try {
await repository.updateBranch(ref, 'master');
console.log(
'You might not be able to merge immediately because CI tasks will take some time.');
break;
} catch (err) {
console.warn(
` cannot update branch for PR ${htmlUrl}, skipping:`,
err.toString());
return false;
}
} else if (response === 'p') {
await showPatch(patchUrl);
continue;
} else if (response === 's') {
console.log(' skipped');
return false;
}
}
}

for (;;) {
let response: string;
if (auto) {
response = 'a';
} else {
response = await question(
'What to do? [a]pprove and merge, show [p]atch, [s]kip: ');
}
if (response === 'a') {
try {
await repository.approvePullRequest(pr);
console.log(' approved!');
} catch (err) {
console.warn(
` error trying to approve PR ${htmlUrl}:`, err.toString());
return false;
}
try {
await repository.mergePullRequest(pr);
console.log(' merged!');
} catch (err) {
console.warn(
` error trying to merge PR ${htmlUrl}:`, err.toString());
return false;
}
try {
await repository.deleteBranch(ref);
console.log(' branch deleted!');
} catch (err) {
console.warn(
` error trying to delete branch ${ref}:`, err.toString());
return false;
}
break;
} else if (response === 'p') {
await showPatch(patchUrl);
continue;
} else if (response === 's') {
console.log(' skipped');
break;
}
}

return true;
}

/**
* Main function. Iterates all open pull request in the repositories of the
* given organization matching given filters. Organization, filters, and GitHub
* token should be given in the configuration file.
* @param {string[]} args Command line arguments.
*/
export async function main(cli: meow.Result) {
if (cli.input.length < 2 || !cli.input[1]) {
console.log(`Usage: repo approve [regex] [--auto]`);
console.log(
'Will show all open PRs with title matching regex and allow to approve them.');
return;
}

const config = await getConfig();
const github = new GitHub(config);
const regex = new RegExp(cli.input[1] || '.*');
const auto = cli.flags.auto;
const repos = await github.getRepositories();
const successful: string[] = [];
const failed: string[] = [];
for (const repository of repos) {
console.log(repository.name);
let prs;
try {
prs = await repository.listPullRequests();
} catch (err) {
console.warn(' cannot list open pull requests:', err.toString());
continue;
}

for (const pr of prs) {
const title = pr.title!;
if (title.match(regex)) {
const result =
await processPullRequest(repository, pr as PullRequest, auto);
if (result) {
successful.push(pr.html_url);
} else {
failed.push(pr.html_url);
}
}
}
}

console.log(
`Successfully approved and merged: ${successful.length} pull request(s)`);
for (const pr of successful) {
console.log(` ${pr}`);
}

console.log(`Unable to merge: ${failed.length} pull requests(s)`);
for (const pr of failed) {
console.log(` ${pr}`);
}
export async function approve(cli: meow.Result) {
return process(cli, {
commandName: 'approve',
commandDesc:
'Will show all open PRs with title matching regex and approve them.',
processMethod
});
}
16 changes: 13 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
'use strict';

import {main as apply} from './apply-change';
import {main as approve} from './approve-prs';
import {main as reject} from './reject-prs';
import {approve} from './approve-prs';
import {reject} from './reject-prs';
import {update} from './update-prs';
import {merge} from './merge-prs';
import {main as check} from './repo-check';
import {sync, exec} from './sync';
import * as meow from 'meow';
Expand All @@ -18,7 +20,9 @@ const cli = meow(
$ repo <command>

Examples
$ repo approve /regex/ [--auto]
$ repo approve /regex/
$ repo update /regex/
$ repo merge /regex/
$ repo reject /regex/
$ repo apply --branch branch --message message --comment comment [--reviewers username[,username...]] [--silent] command
$ repo check
Expand Down Expand Up @@ -63,6 +67,12 @@ switch (cli.input[0]) {
case 'reject':
p = reject(cli);
break;
case 'update':
p = update(cli);
break;
case 'merge':
p = merge(cli);
break;
case 'apply':
p = apply(cli);
break;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ export interface PullRequest {
patch_url: string;
user: User;
base: {sha: string;};
head: {ref: string;};
head: {ref: string; label: string;};
}

export interface Repository {
Expand Down
85 changes: 85 additions & 0 deletions src/lib/prIterator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @fileoverview A quick'n'dirty console UI to approve a bunch of pull requests.
* Usage: `node approve-prs.js [regex]` -- will go through open PRs with title
* matching `regex`, one by one. Without a regex, will go through all open PRs.
*/

'use strict';

import {GitHub, GitHubRepository, PullRequest} from './github';
import * as meow from 'meow';
import {getConfig} from './config';

export interface PRIteratorOptions {
commandName: string;
commandDesc: string;
processMethod:
(repository: GitHubRepository, pr: PullRequest) => Promise<boolean>;
}

/**
* Main function. Iterates all open pull request in the repositories of the
* given organization matching given filters. Organization, filters, and GitHub
* token should be given in the configuration file.
* @param {string[]} args Command line arguments.
*/
export async function process(cli: meow.Result, options: PRIteratorOptions) {
if (cli.input.length < 2 || !cli.input[1]) {
console.log(`Usage: repo ${options.commandName} [regex]`);
console.log(options.commandDesc);
return;
}

const config = await getConfig();
const github = new GitHub(config);
const regex = new RegExp(cli.input[1] || '.*');
const repos = await github.getRepositories();
const successful: string[] = [];
const failed: string[] = [];
for (const repository of repos) {
console.log(repository.name);
let prs;
try {
prs = await repository.listPullRequests();
} catch (err) {
console.warn(' cannot list open pull requests:', err.toString());
continue;
}

for (const pr of prs) {
const title = pr.title!;
if (title.match(regex)) {
const result = await options.processMethod(repository, pr);
if (result) {
successful.push(pr.html_url);
} else {
failed.push(pr.html_url);
}
}
}
}

console.log(`Successfully processed: ${successful.length} pull request(s)`);
for (const pr of successful) {
console.log(` ${pr}`);
}

console.log(`Unable to processed: ${failed.length} pull requests(s)`);
for (const pr of failed) {
console.log(` ${pr}`);
}
}
Loading