├── .gitignore ├── README.md ├── config-example.js ├── package.json └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | *.DS_Store 3 | *.npmignore 4 | node_modules 5 | config.js 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | "Hey cool, people are contributing PRs to our projects!" 2 | 3 | "How many PRs aren't from our team?" 4 | 5 | ``` 6 | git clone https://github.com/apostrophecms/count-outside-pull-requests 7 | npm install 8 | cp config-example.js config.js 9 | # edit that jawn, then... 10 | node app 11 | ``` 12 | 13 | "How about in the last quarter?" 14 | 15 | ``` 16 | node app --from=2017-07-01 --to=2017-10-01 17 | ``` 18 | 19 | *The `--to` option is "up to but not including," so this is correct.* 20 | 21 | ## Changelog 22 | 23 | 1.2.0: automatically excludes org members unless `team` is explicitly configured. To use this option effectively your token must have read access to the members list. 24 | 25 | 1.1.0: `orgs` config option to check all repos in those orgs. 26 | -------------------------------------------------------------------------------- /config-example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // You can also give an "orgs" array, which will check *all* 4 | // repos belonging to those orgs. 5 | orgs: [ 'apostrophecms', 'punkave' ], 6 | 7 | // Automatically excludes all members of the above orgs, so we don't count 8 | // internal PRs 9 | 10 | // OR, enumerate your team members by hand 11 | // team: [ 'you', 'someotherperson' ], 12 | 13 | // This must be a github API access token. 14 | // See: https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ 15 | // 16 | // If you want to automatically exclude private members of your 17 | // organization from the list of PR authors, you will need to 18 | // grant the privilege of reading org membership, otherwise 19 | // no privileges are needed. 20 | token: 'abc' 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "count-outside-pull-requests", 3 | "version": "1.2.0", 4 | "description": "Count pull requests that are not from your team across repos", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/apostrophecms/count-outside-pull-requests.git" 12 | }, 13 | "keywords": [ 14 | "pull", 15 | "requests", 16 | "github" 17 | ], 18 | "author": "Apostrophe Technologies, Inc.", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/apostrophecms/count-outside-pull-requests/issues" 22 | }, 23 | "homepage": "https://github.com/apostrophecms/count-outside-pull-requests#readme", 24 | "dependencies": { 25 | "bluebird": "^3.5.1", 26 | "boring": "^0.1.0", 27 | "qs": "^6.5.1", 28 | "request": "^2.83.0", 29 | "request-promise": "^4.2.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.js'); 2 | const Promise = require('bluebird'); 3 | const request = require('request-promise'); 4 | const _ = require('lodash'); 5 | const qs = require('qs'); 6 | const argv = require('boring')(); 7 | const fs = require('fs'); 8 | const results = {}; 9 | let team = []; 10 | let allRepos = []; 11 | 12 | Promise.try(function() { 13 | if (config.team) { 14 | team = config.team; 15 | return true; 16 | } else { 17 | return Promise.each(config.orgs || [], processOrgTeam); 18 | } 19 | }).then(function() { 20 | return Promise.each(config.orgs || [], processOrg).then(function() { 21 | allRepos = allRepos.concat(config.repos || []); 22 | return Promise.each(allRepos, processRepo) 23 | }); 24 | }).then(function() { 25 | let repos = _.keys(results); 26 | repos.sort(function(a, b) { 27 | if (results[a] > results[b]) { 28 | return -1; 29 | } else if (results[b] > results[a]) { 30 | return 1; 31 | } else { 32 | return 0; 33 | } 34 | }); 35 | let grandTotal = 0; 36 | _.each(repos, function(repo) { 37 | const pulls = results[repo]; 38 | console.log('* * * ' + repo + '\n'); 39 | console.log('Total PRs: ' + pulls.length); 40 | grandTotal += pulls.length; 41 | pulls.sort(function(a, b) { 42 | var a = a.user && a.user.login; 43 | var b = b.user && b.user.login; 44 | if (a < b) { 45 | return -1; 46 | } else if (a > b) { 47 | return 1; 48 | } else { 49 | return 0; 50 | } 51 | }); 52 | _.each(pulls, function(pull) { 53 | console.log(pull.user && pull.user.login); 54 | console.log(pull.title); 55 | console.log(pull.created_at); 56 | console.log(pull.html_url); 57 | console.log(); 58 | }); 59 | }); 60 | console.log('\nGRAND TOTAL: ' + grandTotal); 61 | }) 62 | .catch(function(err) { 63 | console.error(err); 64 | process.exit(1); 65 | }); 66 | 67 | function processOrg(org) { 68 | return processOrgPage(org, 1); 69 | } 70 | 71 | function processOrgPage(org, page) { 72 | const params = { 73 | page 74 | }; 75 | let url = 'https://api.github.com/orgs/' + org + '/repos?' + qs.stringify(params); 76 | const headers = { 77 | 'User-Agent': 'count-outside-pull-requests' 78 | }; 79 | if (config.token) { 80 | headers.Authorization = `token ${config.token}`; 81 | } 82 | return request(url, { 83 | json: true, 84 | headers 85 | }) 86 | .then(function(repos) { 87 | allRepos = allRepos.concat(_.map(repos, function(repo) { 88 | return org + '/' + repo.name; 89 | })); 90 | if (repos.length) { 91 | return processOrgPage(org, page + 1); 92 | } 93 | }); 94 | } 95 | 96 | function processRepo(repo) { 97 | return processRepoPage(repo, 1); 98 | } 99 | 100 | function processRepoPage(repo, page) { 101 | const params = { 102 | state: 'all', 103 | page 104 | } 105 | let url = 'https://api.github.com/repos/' + repo + '/pulls?' + qs.stringify(params); 106 | console.log(url); 107 | const headers = { 108 | 'User-Agent': 'count-outside-pull-requests' 109 | }; 110 | if (config.token) { 111 | headers.Authorization = `token ${config.token}`; 112 | } 113 | return request(url, { 114 | json: true, 115 | headers 116 | }) 117 | .then(function(pulls) { 118 | pulls.forEach(function(pull) { 119 | if (argv.from) { 120 | if (pull.created_at < argv.from) { 121 | return; 122 | } 123 | } 124 | if (argv.to) { 125 | if (pull.created_at > argv.to) { 126 | return; 127 | } 128 | } 129 | if (!_.includes(team, pull.user && pull.user.login)) { 130 | results[repo] = results[repo] || []; 131 | results[repo].push(pull); 132 | } 133 | }); 134 | if (pulls.length) { 135 | return processRepoPage(repo, page + 1); 136 | } 137 | return true; 138 | }); 139 | } 140 | 141 | function processOrgTeam(org) { 142 | 143 | return processOrgTeamPage(org, 1); 144 | } 145 | 146 | function processOrgTeamPage(org, page) { 147 | const params = { 148 | page 149 | }; 150 | const headers = { 151 | 'User-Agent': 'count-outside-pull-requests' 152 | }; 153 | if (config.token) { 154 | headers.Authorization = `token ${config.token}`; 155 | } 156 | let url = 'https://api.github.com/orgs/' + org + '/members?' + qs.stringify(params); 157 | return request(url, { 158 | json: true, 159 | headers 160 | }) 161 | .then(function(members) { 162 | team = team.concat(_.map(members, 'login')); 163 | if (members.length) { 164 | return processOrgTeamPage(org, page + 1); 165 | } 166 | }); 167 | } 168 | 169 | 170 | --------------------------------------------------------------------------------