diff --git a/cli.js b/cli.js index 255910c..98a1354 100755 --- a/cli.js +++ b/cli.js @@ -7,20 +7,23 @@ var assign = require('lodash.assign'); var generate = require('./lib/generate'); var markdown = require('./lib/markdown'); -var getUserInfo = require('./lib/github'); +var updateContributors = require('./lib/contributors'); var cwd = process.cwd(); var defaultRCFile = path.join(cwd, '.all-contributorsrc'); var argv = require('yargs') + .help('help') + .alias('h', 'help') .command('generate', 'Generate the list of contributors') .usage('Usage: $0 generate') .command('add', 'add a new contributor') .usage('Usage: $0 add ') .demand(2) - .default('config', defaultRCFile) .default('file', 'README.md') .default('contributorsPerLine', 7) + .default('contributors', []) + .default('config', defaultRCFile) .config('config', function(configPath) { try { return JSON.parse(fs.readFileSync(configPath, 'utf-8')); @@ -31,7 +34,6 @@ var argv = require('yargs') } } }) - .help('help') .argv; argv.file = path.join(cwd, argv.file); @@ -52,19 +54,19 @@ function onError(error) { } } -if (argv[0] === 'generate') { +var command = argv._[0]; + +if (command === 'generate') { startGeneration(argv, onError); -} else if (argv[0] === 'add') { - // Fetch user - argv.username = argv._[1]; - argv.contributions = argv._[2].split(','); - getUserInfo(argv.username, function(error, user) { +} else if (command === 'add') { + var username = argv._[1]; + var contributions = argv._[2]; + // Add/update contributor and save him to the config file + updateContributors(argv, username, contributions, function(error, contributors) { if (error) { - return console.error(error); + return onError(error); } - // TODO - // Add him to the contributors - // Save rc file with updated contributors key + argv.contributors = contributors; startGeneration(argv, onError); }); } diff --git a/lib/configFile.js b/lib/configFile.js new file mode 100644 index 0000000..1c9ca86 --- /dev/null +++ b/lib/configFile.js @@ -0,0 +1,28 @@ +'use strict'; + +var fs = require('fs'); +var _ = require('lodash/fp'); + +function formatCommaFirst(o) { + return JSON.stringify(o, null, 2) + .split(/(,\n\s+)/) + .map(function (e, i) { + return i%2 ? '\n'+e.substring(4)+', ' : e + }) + .join(''); +} + +function readConfig(configPath) { + return JSON.parse(fs.readFileSync(configPath, 'utf-8')); +} + +function writeContributors(configPath, contributors, cb) { + var config = readConfig(configPath); + var content = _.assign(config, { contributors: contributors }); + return fs.writeFile(configPath, formatCommaFirst(content), cb); +} + +module.exports = { + readConfig: readConfig, + writeContributors: writeContributors +} diff --git a/lib/contributors/add.js b/lib/contributors/add.js new file mode 100644 index 0000000..e86f5f0 --- /dev/null +++ b/lib/contributors/add.js @@ -0,0 +1,57 @@ +'use strict'; + +var _ = require('lodash/fp'); + +function matchContribution(type) { + return function(existing) { + return type === existing || type === existing.type; + }; +} + +function uniqueTypes(contribution) { + return contribution.type || contribution; +} + +function formatContributions(options, existing, newTypes) { + var types = newTypes.split(','); + if (options.url) { + return (existing || []).concat(types.map(function(type) { + return { type: type, url: options.url }; + })); + } + return _.uniqBy(uniqueTypes, (existing || []).concat(types)); +} + +function updateContributor(options, contributor, contributions) { + return _.assign(contributor, { + contributions: formatContributions(options, contributor.contributions, contributions) + }); +} + +function updateExistingContributor(options, username, contributions) { + return options.contributors.map(function(contributor, index) { + if (username !== contributor.login) { + return contributor; + } + return updateContributor(options, contributor, contributions); + }); +} + +function addNewContributor(options, username, contributions, infoFetcher, cb) { + infoFetcher(username, function(error, userData) { + if (error) { + return cb(error); + } + var contributor = _.assign(userData, { + contributions: formatContributions(options, [], contributions) + }); + return cb(null, options.contributors.concat(contributor)); + }); +} + +module.exports = function addContributor(options, username, contributions, infoFetcher, cb) { + if (_.find({login: username}, options.contributors)) { + return cb(null, updateExistingContributor(options, username, contributions)); + } + return addNewContributor(options, username, contributions, infoFetcher, cb); +} diff --git a/lib/contributors/add.test.js b/lib/contributors/add.test.js new file mode 100644 index 0000000..cc301d9 --- /dev/null +++ b/lib/contributors/add.test.js @@ -0,0 +1,161 @@ +import test from 'ava'; +import addContributor from './add'; + +function mockInfoFetcher(username, cb) { + return cb(null, { + login: username, + name: 'Some name', + avatar_url: 'www.avatar.url', + html_url: 'www.html.url' + }); +} + +function fixtures() { + const options = { + contributors: [{ + login: 'login1', + name: 'Some name', + avatar_url: 'www.avatar.url', + html_url: 'www.html.url', + contributions: [ + 'code' + ] + }, { + login: 'login2', + name: 'Some name', + avatar_url: 'www.avatar.url', + html_url: 'www.html.url', + contributions: [ + { type: 'blog', url: 'www.blog.url/path' }, + 'code' + ] + }] + }; + return {options}; +} + +test.cb('should callback with error if infoFetcher fails', t => { + t.plan(1); + + const {options} = fixtures(); + const username = 'login3'; + const contributions = ['doc']; + function infoFetcher(username, cb) { + return cb(new Error('infoFetcher error')); + } + + return addContributor(options, username, contributions, infoFetcher, function(error) { + t.is(error.message, 'infoFetcher error'); + t.end(); + }); +}); + +test.cb('should add new contributor at the end of the list of contributors', t => { + t.plan(3); + + const {options} = fixtures(); + const username = 'login3'; + const contributions = 'doc'; + + return addContributor(options, username, contributions, mockInfoFetcher, function(error, contributors) { + t.notOk(error); + t.is(contributors.length, 3); + t.same(contributors[2], { + login: 'login3', + name: 'Some name', + avatar_url: 'www.avatar.url', + html_url: 'www.html.url', + contributions: [ + 'doc' + ] + }); + t.end(); + }); +}); + +test.cb('should add new contributor at the end of the list of contributors with a url link', t => { + t.plan(3); + + const {options} = fixtures(); + const username = 'login3'; + const contributions = 'doc'; + options.url = 'www.foo.bar'; + + return addContributor(options, username, contributions, mockInfoFetcher, function(error, contributors) { + t.notOk(error); + t.is(contributors.length, 3); + t.same(contributors[2], { + login: 'login3', + name: 'Some name', + avatar_url: 'www.avatar.url', + html_url: 'www.html.url', + contributions: [ + { type: 'doc', url: 'www.foo.bar' } + ] + }); + t.end(); + }); +}); + +test.cb(`should not update an existing contributor's contributions where nothing has changed`, t => { + t.plan(2); + + const {options} = fixtures(); + const username = 'login2'; + const contributions = 'blog,code'; + + return addContributor(options, username, contributions, mockInfoFetcher, function(error, contributors) { + t.notOk(error); + t.same(contributors, options.contributors); + t.end(); + }); +}); + +test.cb(`should update an existing contributor's contributions if a new type is added`, t => { + t.plan(3); + + const {options} = fixtures(); + const username = 'login1'; + const contributions = 'bug'; + + return addContributor(options, username, contributions, mockInfoFetcher, function(error, contributors) { + t.notOk(error); + t.is(contributors.length, 2); + t.same(contributors[0], { + login: 'login1', + name: 'Some name', + avatar_url: 'www.avatar.url', + html_url: 'www.html.url', + contributions: [ + 'code', + 'bug' + ] + }); + t.end(); + }); +}); + +test.cb(`should update an existing contributor's contributions if a new type is added with a link`, t => { + t.plan(3); + + const {options} = fixtures(); + const username = 'login1'; + const contributions = 'bug'; + options.url = 'www.foo.bar'; + + return addContributor(options, username, contributions, mockInfoFetcher, function(error, contributors) { + t.notOk(error); + t.is(contributors.length, 2); + t.same(contributors[0], { + login: 'login1', + name: 'Some name', + avatar_url: 'www.avatar.url', + html_url: 'www.html.url', + contributions: [ + 'code', + { type: 'bug', url: 'www.foo.bar' }, + ] + }); + t.end(); + }); +}); diff --git a/lib/github.js b/lib/contributors/github.js similarity index 67% rename from lib/github.js rename to lib/contributors/github.js index 5396495..6a239a8 100644 --- a/lib/github.js +++ b/lib/contributors/github.js @@ -1,5 +1,6 @@ 'use strict'; +var _ = require('lodash/fp'); var request = require('request'); module.exports = function getUserInfo(username, cb) { @@ -12,6 +13,7 @@ module.exports = function getUserInfo(username, cb) { if (error) { return cb(error); } - return cb(null, JSON.parse(res.body)); + var user = JSON.parse(res.body); + return cb(null, _.pick(['login', 'name', 'avatar_url', 'html_url'], user)); }); } diff --git a/lib/contributors/index.js b/lib/contributors/index.js new file mode 100644 index 0000000..3dd5430 --- /dev/null +++ b/lib/contributors/index.js @@ -0,0 +1,16 @@ +'use strict'; + +var add = require('./add'); +var github = require('./github'); +var configFile = require('../configFile'); + +module.exports = function addContributor(options, username, contributions, cb) { + add(options, username, contributions, github, function(error, contributors) { + if (error) { + return cb(error); + } + configFile.writeContributors(options.config, contributors, function(error) { + return cb(error, contributors); + }); + }); +}; diff --git a/package.json b/package.json index c2a859f..c9029e6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "homepage": "https://github.com/jfmengels/all-contributors-cli#readme", "dependencies": { - "lodash": "^4.5.1", + "lodash": "^4.6.1", "lodash.assign": "^4.0.4", "lodash.findindex": "^4.2.0", "lodash.template": "^4.2.1",