blob: 7545d2a910b1bd50e2533586e95f391da7e64c2d [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
*/
var optimist = require('optimist');
var request = require('request');
var apputil = require('./apputil');
var flagutil = require('./flagutil');
var repoutil = require('./apputil');
// Set env variable CORDOVA_GIT_ACCOUNT to <username>:<password> or <api-token> to avoid hitting GitHub rate limits.
var GITHUB_ACCOUNT = process.env.CORDOVA_GIT_ACCOUNT ? process.env.CORDOVA_GIT_ACCOUNT + "@" : "";
var GITHUB_API_URL = "https://" + GITHUB_ACCOUNT + "api.github.com/";
var GITHUB_ORGANIZATION = "apache";
var commentFailed = false;
function addLastCommentInfo(repo, pullRequests, callback) {
var remaining = pullRequests.length;
if (remaining === 0) {
callback();
}
pullRequests.forEach(function(pullRequest) {
var commentsUrl = pullRequest._links.comments && pullRequest._links.comments.href;
var reviewCommentsUrl = pullRequest._links.review_comments && pullRequest._links.review_comments.href;
if (commentsUrl && reviewCommentsUrl) {
// If comments and review_comments URLs are present, use them (more accurate than scraping)
getPullRequestComments(commentsUrl, function (comments) {
getPullRequestComments(reviewCommentsUrl, comments, function (comments) {
// If we have any comments, grab the user name from the most recent one. If not, we'll display the
// owner of the PR (the initial PR comment is not included in the list of comments we get).
comments = comments.sort(function (a, b) {
// For simplicity, we want to end up with the newest comment first, so reverse sort on create date.
return new Date(b.created_at) - new Date(a.created_at);
});
if (comments.length > 0) {
pullRequest.lastUpdatedBy = comments[0] ? comments[0].user.login : pullRequest.user.login;
}
if (--remaining === 0) {
callback();
}
});
});
} else {
// Otherwise, resort to scraping.
request.get({ url: 'https://github.com/apache/' + repo + '/pull/' + pullRequest.number, headers: { 'User-Agent': 'Cordova Coho' }}, function(err, res, payload) {
if (err) {
if (!commentFailed) {
commentFailed = true;
console.warn('Pull request scrape request failed: ' + err);
}
} else {
var m = /[\s\S]*timeline-comment-header[\s\S]*?"author".*?>(.*?)</.exec(payload);
pullRequest.lastUpdatedBy = m && m[1] || '';
}
if (--remaining === 0) {
callback();
}
});
}
});
}
function getPullRequestComments(url, existingComments, callback) {
if (GITHUB_ACCOUNT) {
url = url.replace('https://', 'https://' + GITHUB_ACCOUNT);
}
if (typeof existingComments === 'function') {
callback = existingComments;
existingComments = [];
}
request.get({
url: url,
headers: {'User-Agent': 'Cordova Coho'}
}, function (err, res, payload) {
if (err) {
if (!commentFailed) {
commentFailed = true;
console.warn('Getting pull request comments failed: ' + err);
}
callback(existingComments);
}
var comments = JSON.parse(payload);
if (!comments.forEach) {
// We don't have an array, so something failed
if (!commentFailed) {
commentFailed = true;
console.warn('Getting pull request comments failed: did not return an array');
}
callback(existingComments);
}
callback(existingComments.concat(comments));
});
}
function listGitHubPullRequests(repo, maxAge, hideUser, short, statsOnly, callback) {
var url = GITHUB_API_URL + 'repos/' + GITHUB_ORGANIZATION + '/' + repo + '/pulls';
request.get({ url: url, headers: { 'User-Agent': 'Cordova Coho' }}, function(err, res, pullRequests) {
if (err) {
apputil.fatal('Error getting pull requests from GitHub: ' + err);
} else if (!pullRequests) {
apputil.fatal('Error: GitHub returned no pull requests');
} else if (res.headers['x-ratelimit-remaining'] && res.headers['x-ratelimit-remaining'] == 0) {
var resetEpoch = new Date(res.headers['x-ratelimit-reset'] * 1000);
var expiration = resetEpoch.getHours() + ":" + resetEpoch.getMinutes() + ":" + resetEpoch.getSeconds();
apputil.fatal('Error: GitHub rate limit exceeded, wait till ' + expiration + ' before trying again.\n' +
'See http://developer.github.com/v3/#rate-limiting for details.');
}
pullRequests = JSON.parse(pullRequests);
var origCount = pullRequests.length;
if (pullRequests.message === 'Bad credentials') {
apputil.fatal('Error: GitHub Bad credentials. Check your CORDOVA_GIT_ACCOUNT environment variable which should be set with your Github API token: https://github.com/settings/tokens.',
'CORDOVA_GIT_ACCOUNT used: ' + process.env['CORDOVA_GIT_ACCOUNT']);
}
pullRequests = pullRequests.filter(function(p) {
var updatedDate = new Date(p.updated_at);
var daysAgo = Math.round((new Date() - updatedDate) / (60 * 60 * 24 * 1000));
return daysAgo < maxAge;
});
var countAfterDateFilter = pullRequests.length;
if (hideUser) {
addLastCommentInfo(repo, pullRequests, next);
} else {
next();
}
function next() {
var cbObj = {
"repo": repo,
"fresh-count": 0,
"old-count": 0,
"stale-count": 0,
"total-count": origCount,
"message": null
};
if (hideUser) {
pullRequests = pullRequests.filter(function(p) {
return p.lastUpdatedBy != hideUser;
});
}
var count = pullRequests.length;
cbObj['fresh-count'] = count;
if (!statsOnly) {
pullRequests.sort(function(a,b) {return (a.updated_at > b.updated_at) ? -1 : ((b.updated_at > a.updated_at) ? 1 : 0);} );
}
var countMsg = count + ' Pull Requests';
if (countAfterDateFilter !== origCount || count !== countAfterDateFilter) {
countMsg += ' (plus ';
}
if (countAfterDateFilter !== origCount) {
countMsg += (origCount - countAfterDateFilter) + ' old';
cbObj['old-count'] = (origCount - countAfterDateFilter);
if (count !== countAfterDateFilter) {
countMsg += ', ';
}
}
if (count !== countAfterDateFilter) {
countMsg += (countAfterDateFilter - count) + ' stale';
cbObj['stale-count'] = (countAfterDateFilter - count);
}
if (countAfterDateFilter !== origCount || count !== countAfterDateFilter) {
countMsg += ')';
}
if (!statsOnly) {
console.log('\x1B[31m========= ' + repo + ': ' + countMsg + '. =========\x1B[39m');
}
if (!statsOnly) {
pullRequests.forEach(function(pullRequest) {
var updatedDate = new Date(pullRequest.updated_at);
var daysAgo = Math.round((new Date() - updatedDate) / (60 * 60 * 24 * 1000));
console.log('\x1B[33m-----------------------------------------------------------------------------------------------\x1B[39m');
console.log('PR #' + pullRequest.number + ': ' + pullRequest.user.login + ': ' +
pullRequest.title + ' (\x1B[31m' + (pullRequest.lastUpdatedBy || '<no comments>') + ' ' + daysAgo + ' days ago\x1B[39m)');
console.log('\x1B[33m-----------------------------------------------------------------------------------------------\x1B[39m');
console.log('* ' + pullRequest.html_url);
// console.log('To merge: curl "' + pullRequest.patch_url + '" | git am');
if (!pullRequest.head.repo) {
console.log('NO REPO EXISTS!');
} else {
console.log('To merge: coho merge-pr --pr ' + pullRequest.number);
}
if (pullRequest.body) {
if (short && pullRequest.body.length > 100) {
console.log(pullRequest.body.substring(0, 100) + '...');
} else {
console.log(pullRequest.body);
}
}
console.log('');
});
}
cbObj.message = countMsg;
callback(cbObj);
}
});
}
function *listPullRequestsCommand() {
var opt = flagutil.registerHelpFlag(optimist);
opt = flagutil.registerRepoFlag(opt)
.options('max-age', {
desc: 'Don\'t show pulls older than this (in days)',
type: 'number',
default: 1000
})
.options('hide-user', {
desc: 'Hide PRs where the last comment\'s is by this github user.',
type: 'string'
})
.options('stats-only', {
desc: 'List stats for PRs in the repos specified.',
type: 'bool'
})
.options('sort-ascending', {
desc: 'Used in conjunction with --stats-only. Sort the PRs ascending.',
type: 'bool'
})
.options('json', {
desc: 'Used in conjunction with --stats-only. Output the report in JSON format.',
type: 'bool'
})
.options('short', {
desc: 'Truncates PR body description',
type: 'bool'
});
opt.usage('Reports what GitHub pull requests are open for the given repositories.\n' +
'\n' +
'Example usage: $0 list-pulls --hide-user="agrieve" | tee pulls.list | less -R\n' +
'Example usage: $0 list-pulls --max-age=365 --repo=.\n' +
'Example usage: $0 list-pulls --max-age=365 --repo=. --stats-only --json --sort-ascending --hide-user=cordova-qa | tail -n +2\n' +
'\n' +
'Please note that GitHub rate limiting applies. See http://developer.github.com/v3/#rate-limiting for details.\n' +
'You can also set the CORDOVA_GIT_ACCOUNT environment variable with your Github API key: https://github.com/settings/tokens\n'
);
var argv = opt.argv;
if (argv.h) {
optimist.showHelp();
process.exit(1);
}
var repos = flagutil.computeReposFromFlag(argv.r);
var report = {
"title" : "coho list-pulls report",
// "command" : process.argv,
"timestamp" : new Date().toJSON(),
"max-age": argv['max-age'],
"repos" : []
};
var simple_report = [];
function next(reportObject) {
if (reportObject && argv['stats-only']) {
if (argv.json) {
report.repos.push(reportObject);
} else {
simple_report.push(reportObject);
}
}
if (repos.length) {
var repo = repos.shift();
listGitHubPullRequests(repo.repoName, argv['max-age'], argv['hide-user'], argv.short, argv['stats-only'], next);
} else if (argv['stats-only']){ // done
function compareFunc(a, b) {
if (a['fresh-count'] < b['fresh-count']) {
return argv['sort-ascending']? -1 : 1;
}
if (a['fresh-count'] > b['fresh-count']) {
return argv['sort-ascending']? 1 : -1;
}
return 0;
};
if (argv.json) {
report.repos.sort(compareFunc);
console.log(JSON.stringify(report, null, 4));
} else {
simple_report.sort(compareFunc);
simple_report.forEach(function(report) {
console.log(report.repo + ' --> ' + report.message);
});
}
}
}
var url = 'https://github.com/pulls?utf8=%E2%9C%93&q=is%3Aopen+is%3Apr';
repos.forEach(function(repo) {
url += '+repo%3Aapache%2F' + repo.repoName;
});
if (!(argv['stats-only'] && argv['json'])) {
console.log(url);
}
next();
}
module.exports = listPullRequestsCommand;