├── .gitignore ├── circle.yml ├── package.json ├── LICENSE ├── github.js ├── test ├── fixtures │ └── aarlaud-snyk └── test.js ├── README.md ├── contributors.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:8.8.0 6 | steps: 7 | - run: 8 | name: "Setup Snyk" 9 | command: | 10 | sudo npm install -g snyk 11 | - checkout 12 | - run: 13 | name: "Install deps" 14 | command: | 15 | npm install 16 | - run: 17 | name: "Run Tests" 18 | command: | 19 | npm test 20 | - run: 21 | name: "Snyk Test" 22 | command: | 23 | snyk test --org=aarlaud-snyk-dev 24 | - run: 25 | name: "Monitor" 26 | command: | 27 | snyk monitor --org=aarlaud-snyk-dev -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-stats", 3 | "version": "1.0.0", 4 | "description": "Github repo stats extractor for Snyk's purposes", 5 | "main": "index.js", 6 | "bin": { 7 | "github-stats": "./index.js" 8 | }, 9 | "scripts": { 10 | "test": "mocha", 11 | "snyktest": "snyk test --org=aarlaud-snyk" 12 | }, 13 | "author": "Antoine@snyk.io", 14 | "license": "MIT", 15 | "dependencies": { 16 | "chalk": "^2.3.0", 17 | "commander": "^2.11.0", 18 | "figlet": "^1.2.0", 19 | "github-base": "^1.0.0" 20 | }, 21 | "devDependencies": { 22 | "chai": "^4.2.0", 23 | "eslint": "^5.6.1", 24 | "eslint-config-standard": "^12.0.0", 25 | "eslint-plugin-import": "^2.14.0", 26 | "eslint-plugin-node": "^7.0.1", 27 | "eslint-plugin-promise": "^4.0.1", 28 | "eslint-plugin-standard": "^4.0.0", 29 | "mocha": "^5.2.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Antoine @ Snyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /github.js: -------------------------------------------------------------------------------- 1 | var GitHub = require('github-base') 2 | var chalk = require('chalk') 3 | 4 | const interpretResponseCode = (statusCode) => { 5 | var response = '' 6 | switch (statusCode) { 7 | case 200: 8 | case 204: 9 | response = 'OK' 10 | break 11 | case 202: 12 | response = 'Github received listing request and is processing data. Please try again in a moment' 13 | break 14 | case 404: 15 | response = 'Organization cannot be found.' 16 | break 17 | case 403: 18 | response = 'Access Denied' 19 | break 20 | default: 21 | response = 'Unexpected response code: ' + statusCode 22 | } 23 | return response 24 | } 25 | 26 | 27 | const authenticate = (options) => { 28 | var githubHandler 29 | var apiurl = 'https://api.github.com' 30 | if (options.apiurl) { 31 | if (options.apiurl.substring(0, 8) !== 'https://' || !options.apiurl.includes('/api/v3')) { 32 | console.log('The api url should look like https://mygheinstanceurl.mycompany.com/api/v3') 33 | process.exit(1) 34 | } 35 | // console.log(options.apiurl.includes('/api/v3')); 36 | // if(options.apiurl.substring(0,7)) 37 | apiurl = options.apiurl 38 | } 39 | if (options.token) { 40 | githubHandler = new GitHub({ 41 | token: options.token, 42 | apiurl: apiurl 43 | }) 44 | } else if (options.username && options.password) { 45 | githubHandler = new GitHub({ 46 | username: options.username, 47 | password: options.password, 48 | apiurl: apiurl 49 | }) 50 | } else { 51 | console.error(chalk.red('Invalid input ! Must provide token or username/password')) 52 | process.exit(1) 53 | } 54 | return githubHandler 55 | } 56 | 57 | const getGithubOrgList = (githubHandler, orgName, privateReposOnly) => { 58 | return new Promise((resolve, reject) => { 59 | var url = '/orgs/' + orgName + '/repos?type=all' 60 | if (privateReposOnly) url = '/orgs/' + orgName + '/repos?type=private' 61 | githubHandler.paged(url) 62 | .then((res) => { 63 | var aggregatedPagesRecords = []; 64 | for(var i=0;i { 75 | reject(err); 76 | }); 77 | 78 | 79 | }) 80 | } 81 | 82 | module.exports = { authenticate, getGithubOrgList } 83 | -------------------------------------------------------------------------------- /test/fixtures/aarlaud-snyk: -------------------------------------------------------------------------------- 1 | [{"name":"goof","forked":true,"contributorsList":[]},{"name":"gitbucket","forked":true,"contributorsList":[]},{"name":"mqttclientdashboard-akamai-fastpurge-msod","forked":true,"contributorsList":[]},{"name":"FirehoseNYC","forked":true,"contributorsList":[]},{"name":"ruby-rails-sample","forked":true,"contributorsList":[]},{"name":"github-stats","forked":false,"contributorsList":[{"name":"aarlaud","# of commits":3}]},{"name":"snyk-to-html","forked":true,"contributorsList":[]},{"name":"snyk-jar-scan","forked":false,"contributorsList":[]},{"name":"java-goof","forked":true,"contributorsList":[]},{"name":"npm-with-only-dev-dependencies","forked":true,"contributorsList":[]},{"name":"trash","forked":false,"contributorsList":[]},{"name":"sg-case-test","forked":false,"contributorsList":[]},{"name":"npm-check-updates","forked":true,"contributorsList":[]},{"name":"snyk-chrome-extension","forked":false,"contributorsList":[]},{"name":"snyk-filter","forked":false,"contributorsList":[]},{"name":"maven-multi-file-fix","forked":true,"contributorsList":[]},{"name":"synapse","forked":true,"contributorsList":[]},{"name":"angularSnykScanner","forked":false,"contributorsList":[]},{"name":"gitlab-stats","forked":false,"contributorsList":[]},{"name":"openldap","forked":false,"contributorsList":[]},{"name":"testgradle","forked":false,"contributorsList":[{"name":"aarlaud","# of commits":2}]},{"name":"maven-depends-on-private","forked":true,"contributorsList":[{"fork":true,"name":"darscan","# of commits":2}]},{"name":"java-buildpack","forked":true,"contributorsList":[{"fork":true,"name":"AH7","# of commits":1},{"fork":true,"name":"aarlaud","# of commits":14},{"fork":true,"name":"nebhale","# of commits":1}]},{"name":"nuxeo","forked":true,"contributorsList":[{"fork":true,"name":"fabiensaulnier","# of commits":1},{"fork":true,"name":"artpick","# of commits":2},{"fork":true,"name":"covolution","# of commits":1},{"fork":true,"name":"pierre-gautier","# of commits":14},{"fork":true,"name":"yjulienne-nuxeo","# of commits":1},{"fork":true,"name":"nelsonsilva","# of commits":3},{"fork":true,"name":"nuxeojenkins","# of commits":1},{"fork":true,"name":"funshodavid","# of commits":16},{"fork":true,"name":"mcedica","# of commits":1},{"fork":true,"name":"kevinleturc","# of commits":19},{"fork":true,"name":"akervern","# of commits":1},{"fork":true,"name":"tmartins","# of commits":2},{"fork":true,"name":"ataillefer","# of commits":7},{"fork":true,"name":"guirenard","# of commits":14},{"fork":true,"name":"bdelbosc","# of commits":28},{"fork":true,"name":"troger","# of commits":5},{"fork":true,"name":"nxmatic","# of commits":3},{"fork":true,"name":"jcarsique","# of commits":1},{"fork":true,"name":"efge","# of commits":30},{"fork":true,"name":"atchertchian","# of commits":3}]},{"name":"training-app","forked":true,"contributorsList":[]},{"name":"NodeGoat","forked":true,"contributorsList":[{"fork":true,"name":"lirantal","# of commits":3}]}] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github list and stats extractor (both Github.com and Github Enterprise) 2 | ## Simple CLI node app to extract details about github organization and repositories 3 | 4 | [![Known Vulnerabilities](https://snyk.io/test/github/aarlaud-snyk/github-stats/badge.svg)](https://snyk.io/test/github/aarlaud-snyk/github-stats) 5 | [![CircleCI](https://circleci.com/gh/aarlaud-snyk/github-stats.svg?style=svg)](https://circleci.com/gh/aarlaud-snyk/github-stats) 6 | 7 | 8 | ##### Packages: Node JS CLI app using commander, chalk, and github-base 9 | 10 | ### Installation 11 | 1. git clone https://github.com/aarlaud-snyk/github-stats 12 | 2. npm install 13 | 14 | #### Prerequisites 15 | - Node 8 (ES6 support for Promises) 16 | - Be member of the organization for private repositories 17 | - **full repo scope granted** to personal access token (how to: https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line or click on this https://github.com/settings/tokens if logged in). 18 | - Github credentials 19 | - If using Github Enterprise, you'll need your api endpoint. Usually looks like the base url you know appended with /api/v3, i.e https://my-ghe-instance.mycompany.com/api/v3. 20 | 21 | ### Usage 22 | - node index.js repoList \ -t \ 23 | - node index.js repoContributorCount \ \ -t \ 24 | - node index.js orgContributorCount \ -t \ 25 | 26 | Example: node index.js orgContributorCount snyk -t 27 | 28 | #### Useful flags 29 | - Use -p or --private to restrict to private repos only (repoList and orgContributorCount only) 30 | - use --apiurl to set the url of your Github Enterprise instance __**API Endpoint**__ (i.e --apiurl=https://my-ghe-instance.company.com/api/v3) 31 | - if using proxy, exporting the http_proxy settings should do the trick. Google search the details of how to set that up, pretty straightforward. 32 | 33 | ## You might need multiple runs to get the results.__Github does not return results immediately__. So it is very likely not to return any results at first. Try again after a short moment. If you have a lot of repos in the organization, you might need to run it multiple times until the total number is extracted (details about [Github's word about caching](https://developer.github.com/v3/repos/statistics/)) 34 | 35 | #### Commands 36 | - repoList: List all repositories under an organization. Can filter on private repos only (--private). 37 | - repoContributorCount: List the number of active contributors for a specific repositories 38 | - orgContributorCount: List the number of active contributors for an entire organization.. Can filter on private repos only (--private). 39 | 40 | ##### An active contributor to a repo is someone who has committed code at least once in the last 90 days. 41 | 42 | 43 | # Untested 44 | 1. username and password attempts, mainly because I have 2FA enabled so need a token. Do yourself a favor, please enable 2FA regardless 45 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var expect = require('chai').expect 3 | 4 | var fs = require('fs') 5 | var path = require('path') 6 | var filePath = path.join(__dirname, './fixtures/') 7 | describe('Non connected functions', function () { 8 | describe('Testing fixtures', function () { 9 | it('should have an array with 26 items', function () { 10 | var data = JSON.parse(fs.readFileSync(filePath + 'aarlaud-snyk', 'utf8')) 11 | assert.equal(data.length, 26) 12 | }) 13 | 14 | it('should have an aarlaud with 3 commits in github-stats', function (done) { 15 | fs.readFile(filePath + 'aarlaud-snyk', 'utf8', function (err, contents) { 16 | var data = JSON.parse(contents) 17 | if (err) { 18 | done(err) 19 | } 20 | for (var i = 0; i < data.length; i++) { 21 | if (data[i].name === 'github-stats') { 22 | assert.equal(data[i].contributorsList[0]['# of commits'], 3) 23 | done() 24 | } 25 | } 26 | }) 27 | }) 28 | }) 29 | describe('Testing consolidateContributorsList()', function () { 30 | var convert = require('../contributors.js') 31 | it('should return 1 active contributors for repos', function (done) { 32 | convert.consolidateContributorsList(filePath, 'aarlaud-snyk') 33 | .then((result) => { 34 | expect(result).to.have.property('list') 35 | expect(result['list']).to.have.property('aarlaud').and.to.have.deep.property('# of commits', 5) 36 | done() 37 | }) 38 | }) 39 | it('should return 25 active contributors for the forked repos', function (done) { 40 | convert.consolidateContributorsList(filePath, 'aarlaud-snyk') 41 | .then((result) => { 42 | expect(result).to.have.property('forkedList') 43 | expect(result['forkedList']).to.have.property('aarlaud').and.to.have.deep.property('# of commits', 14) 44 | 45 | done() 46 | }) 47 | }) 48 | }) 49 | }) 50 | 51 | describe('Connected functions', function () { 52 | describe('Testing Github Authentication', function () { 53 | const githubUtils = require('../github.js') 54 | 55 | it('should authenticate with token - fail for unknown repos', function (done) { 56 | var options = { 'token': process.env.GITHUB_TOKEN } 57 | var githubHandler = githubUtils.authenticate(options) 58 | 59 | githubHandler.get('/repos/doowb/fooobarbaz') 60 | .then(() => { 61 | fail() 62 | }) 63 | .catch((err) => { 64 | assert.equal(err.message, "Not Found") 65 | done() 66 | }); 67 | 68 | 69 | }) 70 | 71 | it('should authenticate with token - works for known repos', function (done) { 72 | var options = { 'token': process.env.GITHUB_TOKEN } 73 | var githubHandler = githubUtils.authenticate(options) 74 | 75 | githubHandler.get('/repos/snyk/snyk') 76 | .then((data) => { 77 | done() 78 | }) 79 | .catch((err) => { 80 | fail() 81 | }) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /contributors.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var fs = require('fs') 3 | var path = require('path') 4 | 5 | var nbOfDays = 90 6 | var roundedNbOfWeeks = Math.floor(nbOfDays / 7) 7 | 8 | const processLists = (organization, data = null) => { 9 | consolidateContributorsList(organization, data) 10 | .then(lists => displayResults(lists['list'], lists['forkedList'])) 11 | .catch((err) => { throw new Error(err) }) 12 | } 13 | 14 | const consolidateContributorsList = (filePath, organization, data = null) => { 15 | return new Promise((resolve, reject) => { 16 | var contributorsList = [] 17 | var forkContributorsList = [] 18 | fs.readFile(filePath + organization, 'utf8', function (err, contents) { 19 | var data = JSON.parse(contents) 20 | if (err) { 21 | throw new Error(err.message) 22 | reject(err) 23 | } 24 | for (var i = 0; i < data.length; i++) { 25 | for (var j = 0; j < data[i].contributorsList.length; j++) { 26 | if (data[i].forked) { 27 | if (data[i].contributorsList[j].name in forkContributorsList) { 28 | let commitCount = forkContributorsList[data[i].contributorsList[j].name]['# of commits'] 29 | forkContributorsList[data[i].contributorsList[j].name] = { '# of commits': commitCount + data[i].contributorsList[j]['# of commits'] } 30 | } else { 31 | forkContributorsList[data[i].contributorsList[j].name] = { '# of commits': data[i].contributorsList[j]['# of commits'] } 32 | } 33 | } else { 34 | if (data[i].contributorsList[j].name in contributorsList) { 35 | let commitCount = contributorsList[data[i].contributorsList[j].name]['# of commits'] 36 | contributorsList[data[i].contributorsList[j].name] = { '# of commits': commitCount + data[i].contributorsList[j]['# of commits'] } 37 | } else { 38 | contributorsList[data[i].contributorsList[j].name] = { '# of commits': data[i].contributorsList[j]['# of commits'] } 39 | } 40 | } 41 | } 42 | } 43 | resolve({ 'list': contributorsList, 'forkedList': forkContributorsList }) 44 | }) 45 | }) 46 | } 47 | 48 | const displayResults = (contributorsList, forkcontributorsList) => { 49 | var contributorsCount = 0 50 | for (var x in contributorsList) { contributorsCount++ } 51 | console.log(chalk.red('\nTotal active contributors with commit in the last ' + nbOfDays + ' days (rounded at ' + roundedNbOfWeeks + ' weeks) = ' + contributorsCount + '\n')) 52 | 53 | if (contributorsCount > 0) { 54 | console.log(chalk.blue('Contributors List:')) 55 | console.log(contributorsList) 56 | console.log('\n') 57 | } 58 | 59 | var forkContributorsCount = 0 60 | for (x in forkcontributorsList) { forkContributorsCount++ } 61 | console.log(chalk.red('\nTotal forked repo active contributors with commit in the last ' + nbOfDays + ' days (rounded at ' + roundedNbOfWeeks + ' weeks) = ' + forkContributorsCount + '\n')) 62 | 63 | if (contributorsCount > 0) { 64 | console.log(chalk.blue('Fork Contributors List:')) 65 | console.log(forkcontributorsList) 66 | console.log('\n') 67 | } 68 | } 69 | 70 | module.exports = { processLists, consolidateContributorsList } 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const INTER_CALLS_DELAY = 1000 5 | 6 | var program = require('commander') 7 | const chalk = require('chalk') 8 | const figlet = require('figlet') 9 | var fs = require('fs') 10 | var path = require('path') 11 | var contributors = require('./contributors.js') 12 | var githubUtils = require('./github'); 13 | var nbOfDays = 90 14 | var roundedNbOfWeeks = Math.floor(nbOfDays / 7) 15 | 16 | var filePath = path.join(__dirname, '/tmp/') 17 | var organization = '' 18 | const EventEmitter = require('events').EventEmitter 19 | const eventEmitter = new EventEmitter() 20 | 21 | 22 | 23 | // Adding global Exception tracing 24 | process.on('uncaughtException', function onUncaughtException (err) { 25 | console.log('uncaught Exception', err) 26 | }) 27 | process.on('unhandledRejection', function onUnhandledRejection (err) { 28 | console.log('unhandled Rejection', err) 29 | }) 30 | 31 | 32 | 33 | const getGithubRepoStats = (githubHandler, orgName, repoName) => { 34 | return new Promise((resolve, reject) => { 35 | githubHandler.paged('/repos/' + orgName + '/' + repoName + '/stats/contributors') 36 | .then((res) => { 37 | var aggregatedPagesRecords = []; 38 | for(var i=0;i { 51 | reject(err); 52 | }); 53 | 54 | 55 | 56 | }) 57 | } 58 | 59 | const getGithubRepoSummaryStats = (githubHandler, orgName, repoName, isForked) => { 60 | return new Promise((resolve, reject) => { 61 | getGithubRepoStats(githubHandler, orgName, repoName) 62 | .then((data) => { 63 | var contributorsList = [] 64 | 65 | for (var i = 0; i < data.length; i++) { 66 | var nbOfWeeks = roundedNbOfWeeks 67 | 68 | if(!data[i].weeks){ 69 | continue; 70 | } 71 | 72 | if (data[i].weeks.length < roundedNbOfWeeks) { 73 | nbOfWeeks = data[i].weeks.length 74 | } 75 | 76 | var nbOfCommits = 0 77 | for (var j = data[i].weeks.length - nbOfWeeks; j < data[i].weeks.length; j++) { 78 | // weeksList.push(res[i].weeks[j].w+"-#commits "+res[i].weeks[j].c); 79 | if (data[i].weeks[j].c > 0) { 80 | nbOfCommits += data[i].weeks[j].c 81 | } 82 | } 83 | if (nbOfCommits > 0 && isForked === false) { 84 | contributorsList.push({ 'name': data[i].author.login, '# of commits': nbOfCommits }) 85 | } else if (nbOfCommits > 0 && isForked === true) { 86 | contributorsList.push({ 'fork': true, 'name': data[i].author.login, '# of commits': nbOfCommits }) 87 | } 88 | } 89 | 90 | eventEmitter.emit('promiseCompleted', repoName, contributorsList) 91 | resolve(contributorsList) 92 | }) 93 | .catch((error) => { 94 | reject(error) 95 | }) 96 | }) 97 | } 98 | 99 | const registerEventListeners = (promiseArray) => { 100 | var lastMessage 101 | // register a listener for the 'randomString' event 102 | eventEmitter.on('promiseCompleted', function (repoName, list) { 103 | console.log(list.length + ' contributors for \t' + repoName) 104 | // repoStatsList.push(list); 105 | fs.readFile(filePath + organization, 'utf8', function (err, contents) { 106 | if (err) { 107 | throw new Error(err.message) 108 | } 109 | var repoListArray = JSON.parse(contents) 110 | var newContent = [] 111 | for (var i = 0; i < repoListArray.length; i++) { 112 | if (repoListArray[i].name === repoName) { 113 | // console.log({"name":repoListArray[i].name, "forked":repoListArray[i].forked, "contributorsList":list}); 114 | newContent.push({ 'name': repoListArray[i].name, 'forked': repoListArray[i].forked, 'contributorsList': list }) 115 | } else { 116 | newContent.push(repoListArray[i]) 117 | } 118 | } 119 | 120 | fs.writeFile(filePath + organization, JSON.stringify(newContent), function (err) { 121 | if (err) { 122 | throw new Error(err.message) 123 | } 124 | setTimeout(() => { promiseProcess(promiseArray) }, INTER_CALLS_DELAY) 125 | }) 126 | }) 127 | }) 128 | 129 | eventEmitter.on('allPromisesCompleted', function () { 130 | if (lastMessage) { 131 | console.log(chalk.red(lastMessage)) 132 | } else { 133 | // consolidateContributorsList(repoStatsList); 134 | contributors.processLists(filePath, organization) 135 | } 136 | }) 137 | 138 | eventEmitter.on('setLastMessage', (message) => { 139 | lastMessage = message 140 | }) 141 | 142 | // eventEmitter.on('promiseFailed', function (repoName, list, error) { 143 | // console.error("Promise failed"); 144 | // //TODO: Handle retry with proper backoff X-retry value or exponential if no retry 145 | // console.error(error); 146 | // }); 147 | } 148 | 149 | const promiseProcess = (promiseArray) => { 150 | if (promiseArray.length > 0) { 151 | promiseArray[0]() 152 | .then(() => promiseArray.shift()) 153 | .catch((error) => { 154 | if (error && error.statusCode === 202) { 155 | console.log(error.error) 156 | eventEmitter.emit('setLastMessage', '\nGithub is processing data, please try again in a moment.\nIt may take multiple runs to go through all the repos.\nPlease rerun until you see the contributors count displayed') 157 | promiseArray.shift() 158 | promiseProcess(promiseArray) 159 | } else if (error && error.statusCode === 403) { 160 | // TODO: if X-Retry header value retry then, otherwise exponential backoff 161 | console.error('403!') 162 | console.error(error) 163 | } else { 164 | console.error(error) 165 | } 166 | 167 | // eventEmitter.emit('promiseFailed', repoName, error) 168 | }) 169 | } else if (promiseArray.length === 0) { 170 | eventEmitter.emit('allPromisesCompleted') 171 | } 172 | } 173 | 174 | 175 | 176 | const introText = () => { 177 | figlet.text('SNYK', { 178 | font: 'Star Wars', 179 | horizontalLayout: 'default', 180 | verticalLayout: 'default' 181 | }, function (err, data) { 182 | if (err) { 183 | console.log('Something went wrong...') 184 | console.dir(err) 185 | return 186 | } 187 | console.log(data) 188 | console.log('\n') 189 | console.log('Snyk tool for counting active contributors') 190 | console.log('Respecting Github API Rate limiting best practices, so be patient :)') 191 | }) 192 | } 193 | 194 | program 195 | .version('1.0.0') 196 | .description('Snyk\'s Github contributors counter (active in the last 3 months)') 197 | .usage(' [options] \n options: -t (2FA setup) or -u -pwd --private for commands on private repos only \n --apiurl ') 198 | 199 | program 200 | .command('repoList ') 201 | .description('List all repos under an organization') 202 | .option('-p, --private', 'private repos only') 203 | .option('-t, --token [GHToken]', 'Running command with Personal Github Token (for 2FA setup)') 204 | .option('-u, --username [username]', 'username (use Token if you set 2FA on Github)') 205 | .option('-pwd, --password [password]', 'password') 206 | .option('-r, --raw', 'Raw output') 207 | .option('-apiurl, --apiurl [apiurl]', 'API url if not https://api.Github.com') 208 | .action((org, options) => { 209 | if (!options.raw) { 210 | introText() 211 | } 212 | var github = githubUtils.authenticate(options) 213 | githubUtils.getGithubOrgList(github, org, options.private) 214 | .then((data) => { 215 | if (!options.raw) { 216 | if (options.private) console.log(chalk.red('\nPrivate Repos Only')) 217 | console.log(chalk.blue('\nTotal # of repos = ' + data.length)) 218 | console.log(chalk.blue('\nRepo list:')) 219 | } 220 | var forkedRepos = [] 221 | for (var i = 0; i < data.length; i++) { 222 | if (data[i].fork) { 223 | forkedRepos.push(data[i].name) 224 | } else { 225 | console.log(data[i].name) 226 | } 227 | } 228 | if (!options.raw) { 229 | console.log(chalk.blue('\nForked Repo list:')) 230 | } 231 | for (i = 0; i < forkedRepos.length; i++) { 232 | console.log(forkedRepos[i]) 233 | } 234 | 235 | console.log('\n') 236 | }) 237 | .catch((error) => { 238 | console.error(error) 239 | }) 240 | }) 241 | 242 | program 243 | .command('repoContributorCount [org] [repo]') 244 | .description('Count number of active contributors to Github repo') 245 | .option('-t, --token [GHToken]', 'Running command with Personal Github Token (for 2FA setup)') 246 | .option('-u, --username [username]', 'username (use Token if you set 2FA on Github)') 247 | .option('-pwd, --password [password]', 'password') 248 | .option('-r, --raw', 'raw output') 249 | .option('-apiurl, --apiurl [apiurl]', 'API url if not https://api.Github.com') 250 | .action((org, repo, options) => { 251 | if (!options.raw) introText() 252 | var github = githubUtils.authenticate(options) 253 | getGithubRepoStats(github, org, repo) 254 | .then((data) => { 255 | var rawCount = data.length 256 | var contributorsList = [] 257 | console.log(data); 258 | for (var i = 0; i < data.length; i++) { 259 | var nbOfWeeks = roundedNbOfWeeks 260 | if (data[i].weeks.length < roundedNbOfWeeks) { 261 | nbOfWeeks = data[i].weeks.length 262 | } 263 | 264 | var nbOfCommits = 0 265 | for (var j = data[i].weeks.length - nbOfWeeks; j < data[i].weeks.length; j++) { 266 | // weeksList.push(res[i].weeks[j].w+"-#commits "+res[i].weeks[j].c); 267 | if (data[i].weeks[j].c > 0) { 268 | nbOfCommits += data[i].weeks[j].c 269 | } 270 | } 271 | if (nbOfCommits > 0) { 272 | if (options.raw) { 273 | contributorsList.push(data[i].author.login) 274 | } else { 275 | contributorsList.push({ 'name': data[i].author.login, '# of commits': nbOfCommits }) 276 | } 277 | } 278 | } 279 | 280 | if (!options.raw) { 281 | console.log(chalk.red('\nTotal active contributors in the last ' + nbOfDays + ' days = ' + contributorsList.length)) 282 | console.log(chalk.blue('\nTotal contributors since first commit = ' + rawCount)) 283 | console.log(chalk.blue('\nDetails for the last ' + nbOfDays + ' days (rounded at ' + roundedNbOfWeeks + ' weeks): ')) 284 | console.log(contributorsList) 285 | console.log('\n') 286 | } else { 287 | console.log(contributorsList) 288 | console.log('\n') 289 | } 290 | }) 291 | .catch((error) => { 292 | console.error(error) 293 | }) 294 | }) 295 | 296 | program 297 | .command('orgContributorCount [org]') 298 | .description('Count number of active contributors to Github repo across an entire organization') 299 | .option('-t, --token [GHToken]', 'Running command with Personal Github Token (for 2FA setup)') 300 | .option('-u, --username [username]', 'username (use Token if you set 2FA on Github)') 301 | .option('-pwd, --password [password]', 'password') 302 | .option('-p, --private', 'private repos only') 303 | .option('-apiurl, --apiurl [apiurl]', 'API url if not https://api.github.com') 304 | .action((org, options) => { 305 | introText() 306 | 307 | var github = githubUtils.authenticate(options) 308 | var promiseArray = [] 309 | organization = org 310 | 311 | if (!fs.existsSync(filePath)) { 312 | fs.mkdirSync(filePath) 313 | } 314 | if (fs.existsSync(filePath + organization)) { 315 | console.log(chalk.red('\nWorking off file repoList in tmp folder. Delete file to restart counting from scratch')) 316 | fs.readFile(filePath + organization, 'utf8', function (err, contents) { 317 | if (err) { 318 | throw new Error(err.message) 319 | } 320 | var repoListArray = JSON.parse(contents) 321 | var repoToBeProcessed = [] 322 | for (var i = 0; i < repoListArray.length; i++) { 323 | if (!repoListArray[i].hasOwnProperty('contributorsList')) { 324 | repoToBeProcessed.push(repoListArray[i]) 325 | } 326 | } 327 | 328 | var repoArray = repoToBeProcessed.map(repo => () => 329 | getGithubRepoSummaryStats(github, org, repo.name, repo.forked) 330 | ) 331 | registerEventListeners(repoArray) 332 | promiseProcess(repoArray) 333 | // console.log("done !"); 334 | }) 335 | } else { 336 | githubUtils.getGithubOrgList(github, org, options.private) 337 | .then((data) => { 338 | if (options.private) console.log(chalk.red('\nPrivate Repos Only')) 339 | console.log(chalk.blue('\nTotal # of repos = ' + data.length)) 340 | var repoArray = data.map(repo => { return { 'name': repo.name, 'forked': repo.fork } }) 341 | fs.writeFile(filePath + organization, JSON.stringify(repoArray), function (err) { 342 | if (err) { 343 | return console.log(err) 344 | } 345 | }) 346 | 347 | promiseArray = data.map(repo => () => 348 | getGithubRepoSummaryStats(github, org, repo.name, repo.fork) 349 | ) 350 | registerEventListeners(promiseArray) 351 | promiseProcess(promiseArray) 352 | }) 353 | .catch((err) => { console.error(err) }) 354 | } 355 | }) 356 | 357 | program.parse(process.argv) 358 | 359 | if (program.args.length === 0) program.help() 360 | 361 | const interpretResponseCode = (statusCode) => { 362 | var response = '' 363 | switch (statusCode) { 364 | case 200: 365 | case 204: 366 | response = 'OK' 367 | break 368 | case 202: 369 | response = 'Github received listing request and is processing data. Please try again in a moment' 370 | break 371 | case 404: 372 | response = 'Organization cannot be found.' 373 | break 374 | case 403: 375 | response = 'Access Denied' 376 | break 377 | default: 378 | response = 'Unexpected response code: ' + statusCode 379 | } 380 | return response 381 | } 382 | 383 | 384 | --------------------------------------------------------------------------------