├── .gitignore ├── LICENSE.md ├── README.md ├── new-contributors.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015 Rod Vagg 5 | --------------------------- 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | new-contributors 2 | ================ 3 | 4 | **Generate a list of recent (possible) new contributors to a GitHub project** 5 | 6 | _(Extracted from http://github.com/rvagg/iojs-tools)_ 7 | 8 | ## Usage: `new-contributors [--days|-d ] [org/repo] 9 | 10 | The default number of days of history to inspect is `7`, this can be changed with the `--days` (or `-d`) argument. 11 | 12 | The default organisation and repository pair is `node/nodejs` but this can be overridden by supplying the pair as an argument, e.g: `new-contributors Level/level`. 13 | 14 | `new-contributors` will load the git history of the repository in the current working directory, collecting email addresses of contributors up to `` ago. It will also attempt to load a `.mailmap` file as it existed `` ago, if it exists. Pull requests created since `` ago are then analysed and the git email addresses of each of the contributors are compared to the existing contributors as per the git log and and `.mailmap` entries. Any addresses that are new indicate a possible new contributor. The output consists of a list of pull requests and the names and addresses of the authors. 15 | 16 | ## Example 17 | 18 | ``` 19 | $ new-contributors --days 4 20 | Loaded 4 days old /Users/rvagg/git/nodejs/node/.mailmap with 159 entries... 21 | Found 884 email addresses in git log up to 4 days ago for /Users/rvagg/git/nodejs/node... 22 | Checking 21 pull requests for nodejs/node... 23 | 24 | New contributors for the last 4 days: 25 | 26 | NUMBER CONTACT TITLE URL 27 | #4269 Martin von Gagern Fix deprecation message for ErrnoException https://github.com/nodejs/node/pull/4269 28 | #4263 Hideki Yamamura doc: fix improper http.get sample code in http.markdown https://github.com/nodejs/node/pull/4263 29 | #4234 Vitor Cortez doc: clarify explanation of first stream section https://github.com/nodejs/node/pull/4234 30 | #4231 Vladimir Krivosheev v8 remote debug: export BreakEvent, BreakPoint and CompileEvent https://github.com/nodejs/node/pull/4231 31 | ``` 32 | 33 | 34 | ## License 35 | 36 | **new-contributors** is Copyright (c) 2015 Rod Vagg [@rvagg](https://twitter.com/rvagg) and licenced under the MIT licence. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE.md file for more details. -------------------------------------------------------------------------------- /new-contributors.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const ghauth = require('ghauth') 6 | , ghpulls = require('ghpulls') 7 | , hyperquest = require('hyperquest') 8 | , bl = require('bl') 9 | , map = require('map-async') 10 | , decodeMime = require('mimelib').decodeMimeWord 11 | , gitexec = require('gitexec') 12 | , split2 = require('split2') 13 | , listStream = require('list-stream') 14 | , after = require('after') 15 | , columnify = require('columnify') 16 | , argv = require('minimist')(process.argv.slice(2)) 17 | 18 | const defaultDays = 7 19 | , defaultOrg = 'nodejs' 20 | , defaultRepo = 'node' 21 | , days = Number(argv.days) || Number(argv.d) || defaultDays 22 | , afterDate = new Date(Date.now() - 1000 * 60 * 60 * 24 * days) 23 | , org = (argv._[0] && argv._[0].split('/')[0]) || defaultOrg 24 | , repo = (argv._[0] && argv._[0].split('/')[1]) || defaultRepo 25 | , authOptions = { configName: 'iojs-tools' } 26 | , gitEmailCmd = `git log --format="%aE" --until='${days} days ago' | sort | uniq` 27 | , gitDaysAgoCmd = `git log --since="${days} days ago" --format=%H | tail -1` 28 | , gitMailmapCmd = `git show {{ref}}:.mailmap` 29 | , dir = process.cwd() 30 | 31 | 32 | const done = after(3, onDataComplete) 33 | 34 | let gitEmails 35 | , pullRequests 36 | , mailmap 37 | 38 | 39 | gitexec.execCollect(dir, 'git remote -v', (err, remotes) => { 40 | if (!err && !new RegExp(`github\\.com[/:]${org}/${repo}\.git`).test(remotes)) 41 | console.warn(`WARNING: could not find ${org}/${repo} in the list of remotes for this git repository, perhaps you should supply an 'org/repo' argument?`) 42 | }) 43 | 44 | 45 | gitexec.exec(dir, gitEmailCmd) 46 | .pipe(split2()) 47 | .pipe(listStream((err, emails) => { 48 | if (err) 49 | return done(err) 50 | 51 | emails = emails.map((email) => email.toString()) 52 | gitEmails = emails 53 | console.log(`Found ${gitEmails.length} email addresses in git log up to ${days} days ago for ${dir}...`) 54 | done() 55 | })) 56 | 57 | 58 | gitexec.execCollect(dir, gitDaysAgoCmd, (err, ref) => { 59 | if (!ref) { 60 | console.log(`Could not find a ${dir}/.mailmap from ${days} ago, ignoring...`) 61 | return done() 62 | } 63 | 64 | ref = ref.replace(/[\r\n\s]+/g, '') 65 | 66 | gitexec.execCollect(dir, gitMailmapCmd.replace(/\{\{ref\}\}/g, ref), (err, _mailmap) => { 67 | if (err) { 68 | console.log(`Could not find a ${dir}/.mailmap from ${days} ago, ignoring...`) 69 | return done() 70 | } 71 | 72 | mailmap = _mailmap 73 | 74 | console.log(`Loaded ${dir}/.mailmap from ${days} ago with ${mailmap.split(/[\n\r]+/).length} entries...`) 75 | 76 | done() 77 | }) 78 | }) 79 | 80 | 81 | getPullRequestData((err, list) => { 82 | if (err) 83 | return done(err) 84 | 85 | pullRequests = list 86 | done() 87 | }) 88 | 89 | 90 | function getPullRequestData (callback) { 91 | function processList (err, list) { 92 | if (err) 93 | throw err 94 | 95 | list = list.filter(Boolean).map((pr) => { 96 | return { 97 | name : pr.from_name 98 | , email : pr.from_email 99 | , contact : `${pr.from_name} <${pr.from_email}>` 100 | , url : pr.html_url 101 | , title : pr.title 102 | , number : `#${pr.number}` 103 | } 104 | }) 105 | 106 | callback(null, list) 107 | } 108 | 109 | ghauth(authOptions, (err, authData) => { 110 | if (err) 111 | throw err 112 | 113 | ghpulls.list(authData, org, repo, { state: 'all', afterDate }, (err, list) => { 114 | if (err) 115 | throw err 116 | 117 | console.log(`Checking ${list.length} pull requests for ${org}/${repo}...`) 118 | map(list, collectAuthor, processList) 119 | }) 120 | }) 121 | } 122 | 123 | function collectAuthor (pull, callback) { 124 | const url = `https://patch-diff.githubusercontent.com/raw/${org}/${repo}/pull/${pull.number}.patch` 125 | 126 | hyperquest.get(url).pipe(bl((err, data) => { 127 | if (err) 128 | return callback(err) 129 | 130 | data = data.toString().replace(/\n\s+/g, ' ') 131 | 132 | const from = data.toString().match(/^From: (.+) <([^>]+)>$/m) 133 | if (!from) 134 | return callback() 135 | //return callback(new Error(`No 'From:' in patch for #${pull.number}`)) 136 | 137 | pull.from_name = from[1].split(/\s/).map((w) => decodeMime(w)).join(' ') 138 | pull.from_email = from[2] 139 | 140 | callback(null, pull) 141 | })) 142 | } 143 | 144 | 145 | function onDataComplete (err) { 146 | if (err) 147 | throw err 148 | 149 | pullRequests = pullRequests.filter((pr) => { 150 | if (mailmap && mailmap.indexOf(`<${pr.email}>`) > -1) 151 | return false 152 | 153 | if (gitEmails.indexOf(pr.email) > -1) 154 | return false 155 | 156 | return true 157 | }) 158 | 159 | let emails = [] 160 | pullRequests.reverse().forEach((pr) => { 161 | let count = emails.filter((e) => e == pr.email).length 162 | if (count) 163 | pr.number += ` (${count + 1})` 164 | emails.push(pr.email) 165 | }) 166 | 167 | console.log(`\nNew contributors for the last ${days} day${days == 1 ? '' : 's:'}\n`) 168 | console.log(columnify(pullRequests, { 169 | columns: [ 'number', 'contact', 'title', 'url' ] 170 | })) 171 | } 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new-contributors", 3 | "version": "1.1.0", 4 | "description": "Check a repository for new contributors", 5 | "main": "new-contributors.js", 6 | "author": "Rod (http://r.va.gg/)", 7 | "license": "MIT", 8 | "dependencies": { 9 | "after": "~0.8.1", 10 | "bl": "~1.0.0", 11 | "columnify": "~1.5.2", 12 | "ghauth": "~3.0.0", 13 | "ghpulls": "~1.0.2", 14 | "gitexec": "~1.0.0", 15 | "hyperquest": "~1.2.0", 16 | "list-stream": "~1.0.0", 17 | "map-async": "~0.1.1", 18 | "mimelib": "~0.2.19", 19 | "minimist": "~1.2.0", 20 | "split2": "~1.0.0" 21 | }, 22 | "preferGlobal": true, 23 | "bin": { 24 | "new-contributors": "./new-contributors.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/rvagg/new-contributors.git" 29 | } 30 | } 31 | --------------------------------------------------------------------------------