diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..1f24ca5
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,6 @@
+**/.*
+**/node_modules
+**/dist
+**/assets
+**/build
+**/test
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..22132c8
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,163 @@
+{
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true,
+ "mocha": true
+ },
+ "plugins": [
+ "mocha"
+ ],
+ // https://github.com/feross/eslint-config-standard
+ "rules": {
+ "accessor-pairs": 2,
+ "block-scoped-var": 0,
+ "brace-style": [2, "1tbs", { "allowSingleLine": true }],
+ "camelcase": 0,
+ "comma-dangle": [2, "never"],
+ "comma-spacing": [2, { "before": false, "after": true }],
+ "comma-style": [2, "last"],
+ "complexity": 0,
+ "consistent-return": 0,
+ "consistent-this": 0,
+ "curly": [2, "multi-line"],
+ "default-case": 0,
+ "dot-location": [2, "property"],
+ "dot-notation": 0,
+ "eol-last": 2,
+ "eqeqeq": [2, "allow-null"],
+ "func-names": 0,
+ "func-style": 0,
+ "generator-star-spacing": [2, "both"],
+ "guard-for-in": 0,
+ "handle-callback-err": [2, "^(err|error|anySpecificError)$" ],
+ "indent": [2, 2],
+ "key-spacing": [2, { "beforeColon": false, "afterColon": true }],
+ "linebreak-style": 0,
+ "max-depth": 0,
+ "max-len": 0,
+ "max-nested-callbacks": 0,
+ "max-params": 0,
+ "max-statements": 0,
+ "new-cap": [2, { "newIsCap": true, "capIsNew": false }],
+ "new-parens": 2,
+ "no-alert": 0,
+ "no-array-constructor": 2,
+ "no-bitwise": 0,
+ "no-caller": 2,
+ "no-catch-shadow": 0,
+ "no-cond-assign": 2,
+ "no-console": 0,
+ "no-constant-condition": 0,
+ "no-continue": 0,
+ "no-control-regex": 2,
+ "no-debugger": 2,
+ "no-delete-var": 2,
+ "no-div-regex": 0,
+ "no-dupe-args": 2,
+ "no-dupe-keys": 2,
+ "no-duplicate-case": 2,
+ "no-else-return": 0,
+ "no-empty": 0,
+ "no-empty-character-class": 2,
+ "no-eq-null": 0,
+ "no-eval": 2,
+ "no-ex-assign": 2,
+ "no-extend-native": 2,
+ "no-extra-bind": 2,
+ "no-extra-boolean-cast": 2,
+ "no-extra-semi": 0,
+ "no-extra-strict": 0,
+ "no-fallthrough": 2,
+ "no-floating-decimal": 2,
+ "no-func-assign": 2,
+ "no-implied-eval": 2,
+ "no-inline-comments": 0,
+ "no-inner-declarations": [2, "functions"],
+ "no-invalid-regexp": 2,
+ "no-irregular-whitespace": 2,
+ "no-iterator": 2,
+ "no-label-var": 2,
+ "no-labels": 2,
+ "no-lone-blocks": 2,
+ "no-lonely-if": 0,
+ "no-loop-func": 0,
+ "no-mixed-requires": 0,
+ "no-mixed-spaces-and-tabs": [2, false],
+ "no-multi-spaces": 2,
+ "no-multi-str": 2,
+ "no-multiple-empty-lines": [2, { "max": 1 }],
+ "no-native-reassign": 2,
+ "no-negated-in-lhs": 2,
+ "no-nested-ternary": 0,
+ "no-new": 0,
+ "no-new-func": 2,
+ "no-new-object": 2,
+ "no-new-require": 2,
+ "no-new-wrappers": 2,
+ "no-obj-calls": 2,
+ "no-octal": 2,
+ "no-octal-escape": 2,
+ "no-path-concat": 0,
+ "no-plusplus": 0,
+ "no-process-env": 0,
+ "no-process-exit": 0,
+ "no-proto": 2,
+ "no-redeclare": 2,
+ "no-regex-spaces": 2,
+ "no-reserved-keys": 0,
+ "no-restricted-modules": 0,
+ "no-return-assign": 2,
+ "no-script-url": 0,
+ "no-self-compare": 2,
+ "no-sequences": 2,
+ "no-shadow": 0,
+ "no-shadow-restricted-names": 2,
+ "no-spaced-func": 2,
+ "no-sparse-arrays": 2,
+ "no-sync": 0,
+ "no-ternary": 0,
+ "no-throw-literal": 2,
+ "no-trailing-spaces": 2,
+ "no-undef": 2,
+ "no-undef-init": 2,
+ "no-undefined": 0,
+ "no-underscore-dangle": 0,
+ "no-unneeded-ternary": 2,
+ "no-unreachable": 2,
+ "no-unused-expressions": 0,
+ "no-unused-vars": [2, { "vars": "all", "args": "none" }],
+ "no-use-before-define": 0,
+ "no-var": 0,
+ "no-void": 0,
+ "no-warning-comments": 0,
+ "no-with": 2,
+ "no-extra-parens": 0,
+ "object-curly-spacing": 0,
+ "one-var": [2, { "initialized": "never" }],
+ "operator-assignment": 0,
+ "operator-linebreak": [2, "after"],
+ "padded-blocks": 0,
+ "quote-props": 0,
+ "quotes": [1, "single", "avoid-escape"],
+ "radix": 2,
+ "semi": [2, "always"],
+ "semi-spacing": 0,
+ "sort-vars": 0,
+ "keyword-spacing": [2],
+ "space-before-blocks": [2, "always"],
+ "space-before-function-paren": 0,
+ "space-in-parens": [2, "never"],
+ "space-infix-ops": 2,
+ "space-unary-ops": [2, { "words": true, "nonwords": false }],
+ "spaced-comment": [2, "always"],
+ "strict": 0,
+ "use-isnan": 2,
+ "valid-jsdoc": 0,
+ "valid-typeof": 2,
+ "vars-on-top": 0,
+ "wrap-iife": [2, "any"],
+ "wrap-regex": 0,
+ "yoda": [2, "never"]
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..20137e4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+.DS_Store
+node_modules
+npm-debug.log
+.idea/*
+Thumbs.db
+coverage/
+build/
+*.gz
+*.sw*
+*.un~
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..5e5be8f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+language: node_js
+sudo: false
+node_js:
+ - 8
+script:
+ - npm i
+ - npm run ci
+after_script:
+ - npm install coveralls@2 && cat ./coverage/lcov.info | coveralls
diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644
index 0000000..e69de29
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9eb43c5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 xdf
+
+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:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..224cead
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+# git-contributor
+
+[![NPM version][npm-image]][npm-url]
+[![build status][travis-image]][travis-url]
+[![Test coverage][coveralls-image]][coveralls-url]
+[![node version][node-image]][node-url]
+[![npm download][download-image]][download-url]
+
+[npm-image]: https://img.shields.io/npm/v/git-contributor.svg?style=flat-square
+[npm-url]: https://npmjs.org/package/git-contributor
+[travis-image]: https://img.shields.io/travis/xudafeng/git-contributor.svg?style=flat-square
+[travis-url]: https://travis-ci.org/xudafeng/git-contributor
+[coveralls-image]: https://img.shields.io/coveralls/xudafeng/git-contributor.svg?style=flat-square
+[coveralls-url]: https://coveralls.io/r/xudafeng/git-contributor?branch=master
+[node-image]: https://img.shields.io/badge/node.js-%3E=_8-green.svg?style=flat-square
+[node-url]: http://nodejs.org/download/
+[download-image]: https://img.shields.io/npm/dm/git-contributor.svg?style=flat-square
+[download-url]: https://npmjs.org/package/git-contributor
+
+> git-contributor
+
+## Spec
+
+- The listings show recently contributors.
+- More on the way.
+
+## Installment
+
+```bash
+$ npm i git-contributor --save-dev
+```
+
+## License
+
+The MIT License (MIT)
diff --git a/bin/git-contributor b/bin/git-contributor
new file mode 100755
index 0000000..48ac294
--- /dev/null
+++ b/bin/git-contributor
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+
+'use strict';
+
+const fs = require('fs');
+const _ = require('xutil');
+const path = require('path');
+const program = require('commander');
+
+const gen = require('../lib/git-contributor');
+
+const pkg = require('../package.json');
+
+program
+ .option('-m, --markdown', 'auto parse and update README.md')
+ .option('-p, --print', 'render markdown file with pdf style')
+ .option('-v, --versions', 'output version infomation');
+
+const options = Object.assign({
+ markdown: true,
+ print: true
+}, program);
+
+gen.getAuthor()
+ .then(list => {
+ const res = gen.genMarkDown(list);
+ if (options.markdown) {
+ const readmeFile = path.join(process.cwd(), 'README.md');
+ if (_.isExistedFile(readmeFile)) {
+ let readmeContent = fs.readFileSync(readmeFile, 'utf8');
+ const reg = new RegExp(`${res.startToken}[^]*${res.endToken}`);
+ if (reg.test(readmeContent)) {
+ readmeContent = readmeContent.replace(reg, res.content);
+ } else {
+ readmeContent += res.content;
+ }
+ fs.writeFileSync(readmeFile, readmeContent);
+ }
+ }
+ if (options.print) {
+ console.log(res.content);
+ }
+ })
+ .catch(e => {
+ console.log(e);
+ });
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..e254aac
--- /dev/null
+++ b/index.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./lib/git-contributor');
diff --git a/lib/git-contributor.js b/lib/git-contributor.js
new file mode 100644
index 0000000..627beaf
--- /dev/null
+++ b/lib/git-contributor.js
@@ -0,0 +1,108 @@
+'use strict';
+
+const _ = require('xutil');
+const {
+ execSync
+} = require('child_process');
+const path = require('path');
+const Promise = require('bluebird');
+const request = require('request-promise');
+
+const pkg = require('../package');
+
+const gitLog2MailList = () => {
+ const logs = execSync('git log --pretty="%an <%ae>"').toString();
+ const list = logs.split(/\n/g).reverse();
+ return _.uniq(list)
+ .filter(item => item.length)
+ .map(item => {
+ return item.split(/\s+/g)[1];
+ });
+};
+
+const getInfoFromGithub = maillist => {
+ const api = 'https://api.github.com/search/users?q=';
+ const tasks = maillist.map(email => {
+ const options = {
+ uri: `${api}${encodeURIComponent(email + ' in:email type:user')}`,
+ json: true,
+ headers: {
+ 'user-agent': Date.now()
+ }
+ };
+ if (process.env.OAUTH_TOKEN) {
+ options.headers['Authorization'] = `token ${process.env.OAUTH_TOKEN}`;
+ }
+ return request(options);
+ });
+
+ return Promise.all(tasks).then(list => {
+ return list.map(item => {
+ if (item && item.total_count === 1) {
+ return item.items[0];
+ }
+ });
+ });
+};
+
+const format = list => {
+ return list.filter(item => item).map(item => {
+ return {
+ login: item.login,
+ avatar_url: item.avatar_url,
+ html_url: item.html_url
+ };
+ });
+};
+
+exports.getAuthor = async (options = {}) => {
+ const cwd = path.resolve(options.cwd || process.cwd());
+ const dotGitDir = path.join(cwd, '.git');
+
+ if (!_.isExistedDir(dotGitDir)) {
+ return [];
+ }
+ const mailList = gitLog2MailList();
+ const infoList = await getInfoFromGithub(_.uniq(mailList));
+ return format(infoList);
+};
+
+exports.genMarkDown = list => {
+ const startToken = '';
+ const endToken = '';
+ const content1 = [''];
+ const lineMax = 6;
+ const content1Data = list.splice(0, lineMax);
+ content1Data.map((item, key) => {
+ content1.push(`[
${item.login}](${item.html_url})
`);
+ });
+ const content2 = [''];
+ list.map((item, key) => {
+ content2.push(`[
${item.login}](${item.html_url})
`);
+ });
+ const genLine = (number) => {
+ const r = [];
+ while(number--) {
+ r.push(' :---: ');
+ }
+ return r;
+ };
+ const res = [
+ startToken,
+ '',
+ '## Contributors',
+ '',
+ content1.join('|'),
+ `|${genLine(content1Data.length).join('|')}|`,
+ content2.join('|'),
+ '',
+ `This project follows the git-contributor [spec](${pkg.repository.url}), welcome to join in and feel free to contribute.`,
+ endToken
+ ];
+
+ return {
+ content: res.join('\n'),
+ startToken,
+ endToken,
+ };
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bbd1b59
--- /dev/null
+++ b/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "git-contributor",
+ "version": "1.0.0",
+ "description": "git-contributor",
+ "keywords": [
+ "contributor"
+ ],
+ "bin": {
+ "git-contributor": "./bin/git-contributor"
+ },
+ "main": "index.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/xudafeng/git-contributor.git"
+ },
+ "dependencies": {
+ "bluebird": "^3.5.1",
+ "commander": "^2.15.1",
+ "request": "^2.85.0",
+ "request-promise": "^4.2.2",
+ "xutil": "^1.0.11"
+ },
+ "devDependencies": {
+ "co-mocha": "*",
+ "eslint": "*",
+ "eslint-plugin-mocha": "^4.11.0",
+ "mocha": "*",
+ "istanbul": "*",
+ "pre-commit": "*"
+ },
+ "scripts": {
+ "ci": "npm run lint && npm run test",
+ "test": "istanbul cover `npm bin`/_mocha",
+ "lint": "eslint . --fix"
+ },
+ "pre-commit": [
+ "lint"
+ ],
+ "license": "MIT"
+}
diff --git a/test/git-contributor.test.js b/test/git-contributor.test.js
new file mode 100644
index 0000000..62cf6b5
--- /dev/null
+++ b/test/git-contributor.test.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const assert = require('assert');
+
+const gitcontributor = require('..');
+
+describe('test', () => {
+ it('should be ok', () => {
+ });
+});
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 0000000..5ada47b
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1 @@
+--reporter spec