chore: upgrade and migrate tooling stuff (#73)

* codemod

* move files

* install kcd-scripts and set up some stuff

* make everything work

* update md files

* change a few things
This commit is contained in:
Kent C. Dodds 2017-11-22 11:09:06 -07:00 committed by GitHub
parent 3d998c4c7c
commit 482a8ab956
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 4502 additions and 7518 deletions

View file

@ -1,13 +0,0 @@
#root = true
[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View file

@ -1,19 +0,0 @@
{
"env": {
"jasmine": true,
"node": true,
"mocha": true,
"browser": true,
"builtin": true
},
"extends": [
"eslint:recommended",
"plugin:ava/recommended"
],
"plugins": [
"ava"
],
"rules": {
"no-unused-vars": [2, {"vars": "all", "args": "none"}]
}
}

43
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,43 @@
<!--
Thanks for your interest in the project. I appreciate bugs filed and PRs submitted!
Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).
Please fill out this template with all the relevant information so we can
understand what's going on and fix the issue.
I'll probably ask you to submit the fix (after giving some direction). If you've
never done that before, that's great! Check this free short video tutorial to
learn how: http://kcd.im/pull-request
-->
- `all-contributors-cli` version:
- `node` version:
- `npm` (or `yarn`) version:
Relevant code or config
```javascript
```
What you did:
What happened:
<!-- Please provide the full error message/screenshots/anything -->
Reproduction repository:
<!--
If possible, please create a repository that reproduces the issue with the
minimal amount of code possible.
-->
Problem description:
Suggested solution:

35
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,35 @@
<!--
Thanks for your interest in the project. Bugs filed and PRs submitted are appreciated!
Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).
Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).
If you're new to contributing to open source projects, you might find this free
video course helpful: http://kcd.im/pull-request
Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!
-->
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:
<!-- Why are these changes necessary? -->
**Why**:
<!-- How were these changes implemented? -->
**How**:
<!-- Have you done all of these things? -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Documentation
- [ ] Tests
- [ ] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
- [ ] Added myself to contributors table <!-- this is optional, see the contributing guidelines for instructions -->
<!-- feel free to add additional comments -->

14
.gitignore vendored
View file

@ -1,2 +1,12 @@
node_modules/
.nyc_output
node_modules
coverage
dist
.opt-in
.opt-out
.DS_Store
.eslintcache
# these cause more harm than good
# when working with contributors
package-lock.json
yarn.lock

View file

@ -1,3 +1,4 @@
sudo: false
language: node_js
cache:
directories:
@ -5,13 +6,9 @@ cache:
notifications:
email: false
node_js:
- '6'
- '5'
- '4'
script:
- npm test
after_success:
- npm run semantic-release
- '8'
script: npm run validate
after_success: kcd-scripts travis-after-success
branches:
only:
- master

189
README.md
View file

@ -1,25 +1,65 @@
# all-contributors-cli
<h1 align="center">
all-contributors-cli 🤖
</h1>
<p align="center" style="font-size: 1.2rem;">Automate acknowledging contributors to your open source projects</p>
<hr />
[![Build Status][build-badge]][build]
[![Code Coverage][coverage-badge]][coverage]
[![version][version-badge]][package] [![downloads][downloads-badge]][downloads]
[![MIT License][license-badge]][license]
[![version](https://img.shields.io/npm/v/all-contributors-cli.svg)](http://npm.im/all-contributors-cli)
[![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors)
[![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc]
[![Watch on GitHub][github-watch-badge]][github-watch]
[![Star on GitHub][github-star-badge]][github-star]
[![Tweet][twitter-badge]][twitter]
This is a tool to help automate adding contributor acknowledgements according to the [all-contributors](https://github.com/kentcdodds/all-contributors) specification.
## The problem
You want to implement the [All Contributors][all-contributors] spec, but don't
want to maintain the table by hand
## This solution
This is a tool to help automate adding contributor acknowledgements according to
the [all-contributors](https://github.com/kentcdodds/all-contributors)
specification.
## Table of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Inspiration](#inspiration)
- [Other Solutions](#other-solutions)
- [Contributors](#contributors)
- [LICENSE](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Installation
You can install it via `npm`:
```console
npm install all-contributors-cli -g
This module is distributed via [npm][npm] which is bundled with [node][node] and
should be installed as one of your project's `devDependencies`:
```
Then init the project using `init` and answer a few questions:
```console
all-contributors init
```
Once initialized, you don't need to have `all-contributors-cli` installed globally. You can instead save it as a devDependency of your project and add it to your `package.json` scripts:
```console
npm install --save-dev all-contributors-cli
```
Then init the project using `init` and answer a few questions:
```console
./node_modules/.bin/all-contributors init
```
Then you can add these scripts to your `package.json`:
```json
{
"scripts": {
@ -28,7 +68,9 @@ npm install --save-dev all-contributors-cli
}
}
```
and use them via `npm run`:
```console
npm run contributors:add -- jfmengels doc
npm run contributors:generate
@ -38,7 +80,8 @@ npm run contributors:generate
### Generating the contributors list
Use `generate` to generate the contributors list and inject it into your contributors file. Contributors will be read from your configuration file.
Use `generate` to generate the contributors list and inject it into your
contributors file. Contributors will be read from your configuration file.
```console
all-contributors generate
@ -46,7 +89,9 @@ all-contributors generate
### Add/update contributors
Use `add` to add new contributors to your project, or add new ways in which they have contributed. They will be added to your configuration file, and the contributors file will be updated just as if you used the `generate` command.
Use `add` to add new contributors to your project, or add new ways in which they
have contributed. They will be added to your configuration file, and the
contributors file will be updated just as if you used the `generate` command.
```console
# Add new contributor <username>, who made a contribution of type <contribution>
@ -54,67 +99,111 @@ all-contributors add <username> <contribution>
# Example:
all-contributors add jfmengels code,doc
```
Where `username` is the user's GitHub username, and `contribution` is a `,`-separated list of ways to contribute, from the following list ([see the specs](https://github.com/kentcdodds/all-contributors#emoji-key)):
- blog: [📝](# "Blogposts")
- bug: [🐛](# "Bug reports")
- code: [💻](# "Code")
- design: [🎨](# "Design")
- doc: [📖](# "Documentation")
- eventOrganizing: [📋](# "Event Organizing")
- example: [💡](# "Examples")
- financial: [💵](# "Financial")
- fundingFinding: [🔍](# "Funding Finding")
- ideas: [🤔](# "Ideas, Planning, & Feedback")
- infra: [🚇](# "Infrastructure (Hosting, Build-Tools, etc)")
- plugin: [🔌](# "Plugin/utility libraries")
- question: [💬](# "Answering Questions")
- review: [👀](# "Reviewed Pull Requests")
- talk: [📢](# "Talks")
- test: [⚠️](# "Tests")
- tool: [🔧](# "Tools")
- translation: [🌍](# "Translation")
- tutorial: [](# "Tutorials")
- video: [📹](# "Videos")
Where `username` is the user's GitHub username, and `contribution` is a
`,`-separated list of ways to contribute, from the following list
([see the specs](https://github.com/kentcdodds/all-contributors#emoji-key)):
* blog: [📝](# "Blogposts")
* bug: [🐛](# "Bug reports")
* code: [💻](# "Code")
* design: [🎨](# "Design")
* doc: [📖](# "Documentation")
* eventOrganizing: [📋](# "Event Organizing")
* example: [💡](# "Examples")
* financial: [💵](# "Financial")
* fundingFinding: [🔍](# "Funding Finding")
* ideas: [🤔](# "Ideas, Planning, & Feedback")
* infra: [🚇](# "Infrastructure (Hosting, Build-Tools, etc)")
* plugin: [🔌](# "Plugin/utility libraries")
* question: [💬](# "Answering Questions")
* review: [👀](# "Reviewed Pull Requests")
* talk: [📢](# "Talks")
* test: [⚠️](# "Tests")
* tool: [🔧](# "Tools")
* translation: [🌍](# "Translation")
* tutorial: [](# "Tutorials")
* video: [📹](# "Videos")
### Check for missing contributors
Use `check` to compare contributors from GitHub with the ones credited in your `.all-contributorsrc` file, in order to make sure that credit is given where it's due.
Use `check` to compare contributors from GitHub with the ones credited in your
`.all-contributorsrc` file, in order to make sure that credit is given where
it's due.
```console
all-contributors check
```
> Due to GitHub API restrictions, this command only works for projects with less than 500 contributors.
> Due to GitHub API restrictions, this command only works for projects with less
> than 500 contributors.
## Configuration
You can configure the project by updating the `.all-contributorsrc` JSON file. The data used to generate the contributors list will be stored in there, and you can configure how you want `all-contributors-cli` to generate the list.
You can configure the project by updating the `.all-contributorsrc` JSON file.
The data used to generate the contributors list will be stored in there, and you
can configure how you want `all-contributors-cli` to generate the list.
These are the keys you can specify:
- `files`: Array of files to update. Default: `['README.md']`
- `projectOwner`: Name of the user the project is hosted by. Example: `jfmengels/all-contributors-cli` --> `jfmengels`. Mandatory.
- `projectName`: Name of the project. Example: `jfmengels/all-contributors-cli` --> `all-contributors-cli`. Mandatory.
- `types`: Specify custom symbols or link templates for contribution types. Can override the documented types.
- `imageSize`: Size (in px) of the user's avatar. Default: `100`.
- `contributorsPerLine`: Maximum number of columns for the contributors table. Default: `7`.
- `contributorTemplate`: Define your own template to generate the contributor list.
- `badgeTemplate`: Define your own template to generate the badge.
* `files`: Array of files to update. Default: `['README.md']`
* `projectOwner`: Name of the user the project is hosted by. Example:
`jfmengels/all-contributors-cli` --> `jfmengels`. Mandatory.
* `projectName`: Name of the project. Example: `jfmengels/all-contributors-cli`
--> `all-contributors-cli`. Mandatory.
* `types`: Specify custom symbols or link templates for contribution types. Can
override the documented types.
* `imageSize`: Size (in px) of the user's avatar. Default: `100`.
* `contributorsPerLine`: Maximum number of columns for the contributors table.
Default: `7`.
* `contributorTemplate`: Define your own template to generate the contributor
list.
* `badgeTemplate`: Define your own template to generate the badge.
## Contributors
Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
Thanks goes to these wonderful people
([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore -->
| [<img src="https://avatars.githubusercontent.com/u/3869412?v=3" width="100px;"/><br /><sub><b>Jeroen Engels</b></sub>](https://github.com/jfmengels)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jfmengels "Code") [📖](https://github.com/jfmengels/all-contributors-cli/commits?author=jfmengels "Documentation") [⚠️](https://github.com/jfmengels/all-contributors-cli/commits?author=jfmengels "Tests") | [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](http://kentcdodds.com/)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=kentcdodds "Documentation") [💻](https://github.com/jfmengels/all-contributors-cli/commits?author=kentcdodds "Code") | [<img src="https://avatars.githubusercontent.com/u/14871650?v=3" width="100px;"/><br /><sub><b>João Guimarães</b></sub>](https://github.com/jccguimaraes)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jccguimaraes "Code") | [<img src="https://avatars.githubusercontent.com/u/1282980?v=3" width="100px;"/><br /><sub><b>Ben Briggs</b></sub>](http://beneb.info)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=ben-eb "Code") | [<img src="https://avatars.githubusercontent.com/u/22768990?v=3" width="100px;"/><br /><sub><b>Itai Steinherz</b></sub>](https://github.com/itaisteinherz)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=itaisteinherz "Documentation") [💻](https://github.com/jfmengels/all-contributors-cli/commits?author=itaisteinherz "Code") | [<img src="https://avatars.githubusercontent.com/u/5701162?v=3" width="100px;"/><br /><sub><b>Alex Jover</b></sub>](https://github.com/alexjoverm)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=alexjoverm "Code") [📖](https://github.com/jfmengels/all-contributors-cli/commits?author=alexjoverm "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/8212?v=3" width="100px;"/><br /><sub><b>Jerod Santo</b></sub>](https://jerodsanto.net)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jerodsanto "Code") |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars1.githubusercontent.com/u/574871?v=3" width="100px;"/><br /><sub><b>Kevin Jalbert</b></sub>](https://github.com/kevinjalbert)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=kevinjalbert "Code") | [<img src="https://avatars3.githubusercontent.com/u/5038030?v=4" width="100px;"/><br /><sub><b>tunnckoCore</b></sub>](https://i.am.charlike.online)<br />[🔧](#tool-charlike "Tools") | [<img src="https://avatars2.githubusercontent.com/u/304450?v=4" width="100px;"/><br /><sub><b>Mehdi Achour</b></sub>](https://machour.idk.tn/)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=machour "Code") | [<img src="https://avatars1.githubusercontent.com/u/8344688?v=4" width="100px;"/><br /><sub><b>Roy Revelt</b></sub>](https://codsen.com)<br />[🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3Arevelt "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/422331?v=4" width="100px;"/><br /><sub><b>Chris Vickery</b></sub>](https://github.com/chrisinajar)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=chrisinajar "Code") | [<img src="https://avatars2.githubusercontent.com/u/1026002?v=4" width="100px;"/><br /><sub><b>Bryce Reynolds</b></sub>](https://github.com/brycereynolds)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=brycereynolds "Code") | [<img src="https://avatars3.githubusercontent.com/u/2322305?v=4" width="100px;"/><br /><sub><b>James, please</b></sub>](http://www.jmeas.com)<br />[🤔](#ideas-jmeas "Ideas, Planning, & Feedback") [💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jmeas "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/1057324?v=4" width="100px;"/><br /><sub><b>Spyros Ioakeimidis</b></sub>](http://www.spyros.io)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=spirosikmd "Code") | [<img src="https://avatars3.githubusercontent.com/u/12335761?v=4" width="100px;"/><br /><sub><b>Fernando Costa</b></sub>](https://github.com/fadc80)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=fadc80 "Code") | [<img src="https://avatars0.githubusercontent.com/u/197404?v=4" width="100px;"/><br /><sub><b>snipe</b></sub>](https://snipe.net)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=snipe "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/997157?v=4" width="100px;"/><br /><sub><b>Gant Laborde</b></sub>](http://gantlaborde.com/)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=GantMan "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification.
Contributions of any kind are welcome!
This project follows the
[all-contributors](https://github.com/kentcdodds/all-contributors)
specification. Contributions of any kind are welcome!
## LICENSE
MIT
[npm]: https://www.npmjs.com/
[node]: https://nodejs.org
[build-badge]: https://img.shields.io/travis/jfmengels/all-contributors-cli.svg?style=flat-square
[build]: https://travis-ci.org/jfmengels/all-contributors-cli
[coverage-badge]: https://img.shields.io/codecov/c/github/jfmengels/all-contributors-cli.svg?style=flat-square
[coverage]: https://codecov.io/github/jfmengels/all-contributors-cli
[version-badge]: https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square
[package]: https://www.npmjs.com/package/all-contributors-cli
[downloads-badge]: https://img.shields.io/npm/dm/all-contributors-cli.svg?style=flat-square
[downloads]: http://www.npmtrends.com/all-contributors-cli
[license-badge]: https://img.shields.io/npm/l/all-contributors-cli.svg?style=flat-square
[license]: https://github.com/jfmengels/all-contributors-cli/blob/master/other/LICENSE
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[prs]: http://makeapullrequest.com
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
[coc]: https://github.com/jfmengels/all-contributors-cli/blob/master/other/CODE_OF_CONDUCT.md
[github-watch-badge]: https://img.shields.io/github/watchers/jfmengels/all-contributors-cli.svg?style=social
[github-watch]: https://github.com/jfmengels/all-contributors-cli/watchers
[github-star-badge]: https://img.shields.io/github/stars/jfmengels/all-contributors-cli.svg?style=social
[github-star]: https://github.com/jfmengels/all-contributors-cli/stargazers
[twitter]: https://twitter.com/intent/tweet?text=Check%20out%20all-contributors-cli!%20https://github.com/jfmengels/all-contributors-cli%20%F0%9F%91%8D
[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/jfmengels/all-contributors-cli.svg?style=social
[emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
[all-contributors]: https://github.com/kentcdodds/all-contributors

155
cli.js
View file

@ -1,155 +0,0 @@
#!/usr/bin/env node
/* eslint-disable no-console */
'use strict';
var path = require('path');
var yargs = require('yargs');
var chalk = require('chalk');
var inquirer = require('inquirer');
var init = require('./lib/init');
var generate = require('./lib/generate');
var util = require('./lib/util');
var updateContributors = require('./lib/contributors');
var cwd = process.cwd();
var defaultRCFile = path.join(cwd, '.all-contributorsrc');
var argv = yargs
.help('help')
.alias('h', 'help')
.alias('v', 'version')
.version()
.command('generate', 'Generate the list of contributors')
.usage('Usage: $0 generate')
.command('add', 'add a new contributor')
.usage('Usage: $0 add <username> <contribution>')
.command('init', 'Prepare the project to be used with this tool')
.usage('Usage: $0 init')
.command('check', 'Compares contributors from GitHub with the ones credited in .all-contributorsrc')
.usage('Usage: $0 check')
.boolean('commit')
.default('files', ['README.md'])
.default('contributorsPerLine', 7)
.default('contributors', [])
.default('config', defaultRCFile)
.config('config', function (configPath) {
try {
return util.configFile.readConfig(configPath);
} catch (error) {
if (configPath !== defaultRCFile) {
onError(error);
}
}
})
.argv;
function startGeneration(argv) {
return Promise.all(
argv.files.map(file => {
const filePath = path.join(cwd, file);
return util.markdown.read(filePath)
.then(fileContent => {
var newFileContent = generate(argv, argv.contributors, fileContent);
return util.markdown.write(filePath, newFileContent);
});
})
);
}
function addContribution(argv) {
var username = argv._[1];
var contributions = argv._[2];
// Add or update contributor in the config file
return updateContributors(argv, username, contributions)
.then(data => {
argv.contributors = data.contributors;
return startGeneration(argv)
.then(() => {
if (argv.commit) {
return util.git.commit(argv, data);
}
});
});
}
function checkContributors() {
var configData = util.configFile.readConfig(argv.config);
return util.check(configData.projectOwner, configData.projectName)
.then(ghContributors => {
var knownContributions = configData.contributors.reduce((obj, item) => {
obj[item.login] = item.contributions;
return obj;
}, {});
var knownContributors = configData.contributors.map(contributor => contributor.login);
var missingInConfig = ghContributors.filter(login => !knownContributors.includes(login));
var missingFromGithub = knownContributors.filter(login => {
return !ghContributors.includes(login) && (
knownContributions[login].includes('code') ||
knownContributions[login].includes('test')
);
});
if (missingInConfig.length) {
process.stdout.write(chalk.bold('Missing contributors in .all-contributorsrc:\n'));
process.stdout.write(` ${missingInConfig.join(', ')}\n`);
}
if (missingFromGithub.length) {
process.stdout.write(chalk.bold('Unknown contributors found in .all-contributorsrc:\n'));
process.stdout.write(` ${missingFromGithub.join(', ')}\n`);
}
});
}
function onError(error) {
if (error) {
console.error(error.message);
process.exit(1);
}
process.exit(0);
}
function promptForCommand(argv) {
var questions = [{
type: 'list',
name: 'command',
message: 'What do you want to do?',
choices: [{
name: 'Add a new contributor or add a new contribution type',
value: 'add'
}, {
name: 'Re-generate the contributors list',
value: 'generate'
}, {
name: 'Compare contributors from GitHub with the credited ones',
value: 'check'
}],
when: !argv._[0],
default: 0
}];
return inquirer.prompt(questions)
.then(answers => {
return answers.command || argv._[0];
});
}
promptForCommand(argv)
.then(command => {
switch (command) {
case 'init':
return init();
case 'generate':
return startGeneration(argv);
case 'add':
return addContribution(argv);
case 'check':
return checkContributors();
default:
throw new Error(`Unknown command ${command}`);
}
})
.catch(onError);

12
jest.config.js Normal file
View file

@ -0,0 +1,12 @@
const jestConfig = require('kcd-scripts/jest')
module.exports = Object.assign(jestConfig, {
coverageThreshold: {
global: {
branches: 50,
functions: 40,
lines: 50,
statements: 50,
},
},
})

View file

@ -1,54 +0,0 @@
'use strict';
var _ = require('lodash/fp');
function uniqueTypes(contribution) {
return contribution.type || contribution;
}
function formatContributions(options, existing, types) {
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) {
if (username.toLowerCase() !== contributor.login.toLowerCase()) {
return contributor;
}
return updateContributor(options, contributor, contributions);
});
}
function addNewContributor(options, username, contributions, infoFetcher) {
return infoFetcher(username)
.then(userData => {
var contributor = _.assign(userData, {
contributions: formatContributions(options, [], contributions)
});
return options.contributors.concat(contributor);
});
}
module.exports = function addContributor(options, username, contributions, infoFetcher) {
// case insensitive find
var exists = _.find(function (contributor) {
return contributor.login.toLowerCase() === username.toLowerCase();
}, options.contributors);
if (exists) {
return Promise.resolve(updateExistingContributor(options, username, contributions));
}
return addNewContributor(options, username, contributions, infoFetcher);
};

View file

@ -1,189 +0,0 @@
import test from 'ava';
import addContributor from './add';
function mockInfoFetcher(username) {
return Promise.resolve({
login: username,
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url'
});
}
function fixtures() {
const options = {
contributors: [{
login: 'login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
'code'
]
}, {
login: 'login2',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
{type: 'blog', url: 'www.blog.url/path'},
'code'
]
}]
};
return {options};
}
function caseFixtures() {
const options = {
contributors: [{
login: 'Login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
'code'
]
}]
};
return {options};
}
test('should callback with error if infoFetcher fails', t => {
const {options} = fixtures();
const username = 'login3';
const contributions = ['doc'];
function infoFetcher() {
return Promise.reject(new Error('infoFetcher error'));
}
return t.throws(
addContributor(options, username, contributions, infoFetcher),
'infoFetcher error'
);
});
test('should add new contributor at the end of the list of contributors', t => {
const {options} = fixtures();
const username = 'login3';
const contributions = ['doc'];
return addContributor(options, username, contributions, mockInfoFetcher)
.then(contributors => {
t.is(contributors.length, 3);
t.deepEqual(contributors[2], {
login: 'login3',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
'doc'
]
});
});
});
test('should add new contributor at the end of the list of contributors with a url link', t => {
const {options} = fixtures();
const username = 'login3';
const contributions = ['doc'];
options.url = 'www.foo.bar';
return addContributor(options, username, contributions, mockInfoFetcher)
.then(contributors => {
t.is(contributors.length, 3);
t.deepEqual(contributors[2], {
login: 'login3',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
{type: 'doc', url: 'www.foo.bar'}
]
});
});
});
test(`should not update an existing contributor's contributions where nothing has changed`, t => {
const {options} = fixtures();
const username = 'login2';
const contributions = ['blog', 'code'];
return addContributor(options, username, contributions, mockInfoFetcher)
.then(contributors => {
t.deepEqual(contributors, options.contributors);
});
});
test(`should not update an existing contributor's contributions where nothing has changed but the casing`, t => {
const {options} = caseFixtures();
const username = 'login1';
const contributions = ['code'];
return addContributor(options, username, contributions, mockInfoFetcher)
.then(contributors => {
t.deepEqual(contributors, options.contributors);
});
});
test(`should update an existing contributor's contributions if a new type is added`, t => {
const {options} = fixtures();
const username = 'login1';
const contributions = ['bug'];
return addContributor(options, username, contributions, mockInfoFetcher)
.then(contributors => {
t.is(contributors.length, 2);
t.deepEqual(contributors[0], {
login: 'login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
'code',
'bug'
]
});
});
});
test(`should update an existing contributor's contributions if a new type is added with different username case`, t => {
const {options} = caseFixtures();
const username = 'login1';
const contributions = ['bug'];
return addContributor(options, username, contributions, mockInfoFetcher)
.then(contributors => {
t.is(contributors.length, 1);
t.deepEqual(contributors[0], {
login: 'Login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
'code',
'bug'
]
});
});
});
test(`should update an existing contributor's contributions if a new type is added with a link`, t => {
const {options} = fixtures();
const username = 'login1';
const contributions = ['bug'];
options.url = 'www.foo.bar';
return addContributor(options, username, contributions, mockInfoFetcher)
.then(contributors => {
t.is(contributors.length, 2);
t.deepEqual(contributors[0], {
login: 'login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [
'code',
{type: 'bug', url: 'www.foo.bar'}
]
});
});
});

View file

@ -1,31 +0,0 @@
'use strict';
var pify = require('pify');
var request = pify(require('request'));
module.exports = function getUserInfo(username) {
return request.get({
url: 'https://api.github.com/users/' + username,
headers: {
'User-Agent': 'request'
}
})
.then(res => {
var body = JSON.parse(res.body);
var profile = body.blog || body.html_url;
// Github throwing specific errors as 200...
if (!profile && body.message) {
throw new Error(body.message);
}
profile = profile.startsWith('http') ? profile : 'http://' + profile;
return {
login: body.login,
name: body.name || username,
avatar_url: body.avatar_url,
profile
};
});
};

View file

@ -1,70 +0,0 @@
import test from 'ava';
import nock from 'nock';
import getUserInfo from './github';
test('should handle errors', t => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.replyWithError(404);
return t.throws(getUserInfo('nodisplayname'));
});
test('should handle github errors', t => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
message: 'API rate limit exceeded for 0.0.0.0. (But here\'s the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)',
documentation_url: 'https://developer.github.com/v3/#rate-limiting'
});
return t.throws(getUserInfo('nodisplayname'));
});
test('should fill in the name when null is returned', t => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
login: 'nodisplayname',
name: null,
avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400',
html_url: 'https://github.com/nodisplayname'
});
return getUserInfo('nodisplayname')
.then(info => {
t.is(info.name, 'nodisplayname');
});
});
test('should fill in the name when an empty string is returned', t => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
login: 'nodisplayname',
name: '',
avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400',
html_url: 'https://github.com/nodisplayname'
});
return getUserInfo('nodisplayname')
.then(info => {
t.is(info.name, 'nodisplayname');
});
});
test('should append http when no absolute link is provided', t => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
login: 'nodisplayname',
name: '',
avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400',
html_url: 'www.github.com/nodisplayname'
});
return getUserInfo('nodisplayname')
.then(info => {
t.is(info.profile, 'http://www.github.com/nodisplayname');
});
});

View file

@ -1,33 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var util = require('../util');
var add = require('./add');
var github = require('./github');
var prompt = require('./prompt');
function isNewContributor(contributorList, username) {
return !_.find({login: username}, contributorList);
}
module.exports = function addContributor(options, username, contributions) {
const answersP = prompt(options, username, contributions);
const contributorsP = answersP
.then(answers => add(options, answers.username, answers.contributions, github));
const writeContributorsP = contributorsP.then(
contributors => util.configFile.writeContributors(options.config, contributors)
);
return Promise.all([answersP, contributorsP, writeContributorsP])
.then(res => {
const answers = res[0];
const contributors = res[1];
return {
username: answers.username,
contributions: answers.contributions,
contributors: contributors,
newContributor: isNewContributor(options.contributors, answers.username)
};
});
};

View file

@ -1,64 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var inquirer = require('inquirer');
var util = require('../util');
var contributionChoices = _.flow(
util.contributionTypes,
_.toPairs,
_.sortBy(function (pair) {
return pair[1].description;
}),
_.map(function (pair) {
return {
name: pair[1].symbol + ' ' + pair[1].description,
value: pair[0]
};
})
);
function getQuestions(options, username, contributions) {
return [{
type: 'input',
name: 'username',
message: 'What is the contributor\'s GitHub username?',
when: !username
}, {
type: 'checkbox',
name: 'contributions',
message: 'What are the contribution types?',
when: !contributions,
default: function (answers) {
// default values for contributions when updating existing users
answers.username = answers.username || username;
return options.contributors
.filter((entry) => entry.login.toLowerCase() === answers.username.toLowerCase())
.reduce((allEntries, entry) => allEntries.concat(entry.contributions), []);
},
choices: contributionChoices(options),
validate: function (input, answers) {
answers.username = answers.username || username;
var previousContributions = options.contributors
.filter((entry) => entry.login.toLowerCase() === answers.username.toLowerCase())
.reduce((allEntries, entry) => allEntries.concat(entry.contributions), []);
if (!input.length) {
return 'Use space to select at least one contribution type.';
} else if (_.isEqual(input, previousContributions)) {
return 'Nothing changed, use space to select contribution types.';
}
return true;
}
}];
}
module.exports = function prompt(options, username, contributions) {
var defaults = {
username: username,
contributions: contributions && contributions.split(',')
};
var questions = getQuestions(options, username, contributions);
return inquirer.prompt(questions)
.then(_.assign(defaults));
};

View file

@ -1,11 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var defaultTemplate = '[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)';
module.exports = function formatBadge(options, contributors) {
return _.template(options.badgeTemplate || defaultTemplate)({
contributors: contributors
});
};

View file

@ -1,23 +0,0 @@
import test from 'ava';
import _ from 'lodash/fp';
import formatBadge from './format-badge';
test('should return badge with the number of contributors', t => {
const options = {};
const expected8 =
'[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors)';
const expected16 =
'[![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors)';
t.is(formatBadge(options, _.times(_.constant({}), 8)), expected8);
t.is(formatBadge(options, _.times(_.constant({}), 16)), expected16);
});
test('should be able to specify custom badge template', t => {
const options = {
badgeTemplate: 'We have <%= contributors.length %> contributors'
};
t.is(formatBadge(options, _.times(_.constant({}), 8)), 'We have 8 contributors');
t.is(formatBadge(options, _.times(_.constant({}), 16)), 'We have 16 contributors');
});

View file

@ -1,35 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var util = require('../util');
var linkTemplate = _.template('[<%= symbol %>](<%= url %> "<%= description %>")');
function getType(options, contribution) {
var types = util.contributionTypes(options);
return types[contribution.type || contribution];
}
module.exports = function formatContribution(options, contributor, contribution) {
var type = getType(options, contribution);
if (!type) {
throw new Error('Unknown contribution type ' + contribution + ' for contributor ' + contributor.login);
}
var templateData = {
symbol: type.symbol,
description: type.description,
contributor: contributor,
options: options
};
var url = `#${contribution}-${contributor.login}`;
if (contribution.url) {
url = contribution.url;
} else if (type.link) {
url = _.template(type.link)(templateData);
}
return linkTemplate(_.assign({url: url}, templateData));
};

View file

@ -1,124 +0,0 @@
import test from 'ava';
import contributors from './fixtures/contributors.json';
import formatContributionType from './format-contribution-type';
const fixtures = () => {
const options = {
projectOwner: 'jfmengels',
projectName: 'all-contributors-cli',
imageSize: 100
};
return {options};
};
test('should return corresponding symbol', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
t.is(formatContributionType(options, contributor, 'tool'), '[🔧](#tool-kentcdodds "Tools")');
t.is(formatContributionType(options, contributor, 'question'), '[💬](#question-kentcdodds "Answering Questions")');
});
test('should return link to commits', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
const expectedLink = 'https://github.com/jfmengels/all-contributors-cli/commits?author=kentcdodds';
t.is(formatContributionType(options, contributor, 'code'), '[💻](' + expectedLink + ' "Code")');
t.is(formatContributionType(options, contributor, 'doc'), '[📖](' + expectedLink + ' "Documentation")');
t.is(formatContributionType(options, contributor, 'test'), '[⚠️](' + expectedLink + ' "Tests")');
});
test('should return link to issues', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
const expected = '[🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3Akentcdodds "Bug reports")';
t.is(formatContributionType(options, contributor, 'bug'), expected);
});
test('should make any symbol into a link if contribution is an object', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
const contribution = {
type: 'tool',
url: 'www.foo.bar'
};
t.is(formatContributionType(options, contributor, contribution), '[🔧](www.foo.bar "Tools")');
});
test('should override url for given types', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
const contribution = {
type: 'code',
url: 'www.foo.bar'
};
t.is(formatContributionType(options, contributor, contribution), '[💻](www.foo.bar "Code")');
});
test('should be able to add types to the symbol list', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
options.types = {
cheerful: {symbol: ':smiley:'}
};
t.is(formatContributionType(options, contributor, 'cheerful'), '[:smiley:](#cheerful-kentcdodds "")');
t.is(formatContributionType(options, contributor, {
type: 'cheerful',
url: 'www.foo.bar'
}), '[:smiley:](www.foo.bar "")');
});
test('should be able to add types with template to the symbol list', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
options.types = {
web: {
symbol: ':web:',
link: 'www.<%= contributor.login %>.com'
}
};
t.is(formatContributionType(options, contributor, 'web'), '[:web:](www.kentcdodds.com "")');
});
test('should be able to override existing types', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
options.types = {
code: {symbol: ':smiley:'}
};
t.is(formatContributionType(options, contributor, 'code'), '[:smiley:](#code-kentcdodds "")');
t.is(formatContributionType(options, contributor, {
type: 'code',
url: 'www.foo.bar'
}), '[:smiley:](www.foo.bar "")');
});
test('should be able to override existing templates', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
options.types = {
code: {
symbol: ':web:',
link: 'www.<%= contributor.login %>.com'
}
};
t.is(formatContributionType(options, contributor, 'code'), '[:web:](www.kentcdodds.com "")');
t.is(formatContributionType(options, contributor, {
type: 'code',
url: 'www.foo.bar'
}), '[:web:](www.foo.bar "")');
});
test('should throw a helpful error on unknown type', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
t.throws(() => formatContributionType(options, contributor, 'docs'), 'Unknown contribution type docs for contributor kentcdodds');
});

View file

@ -1,38 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var formatContributionType = require('./format-contribution-type');
var avatarTemplate = _.template('<img src="<%= contributor.avatar_url %>" width="<%= options.imageSize %>px;"/>');
var avatarBlockTemplate = _.template('[<%= avatar %><br /><sub><b><%= name %></b></sub>](<%= contributor.profile %>)');
var contributorTemplate = _.template('<%= avatarBlock %><br /><%= contributions %>');
var defaultImageSize = 100;
function defaultTemplate(templateData) {
var avatar = avatarTemplate(templateData);
var avatarBlock = avatarBlockTemplate(_.assign({
name: escapeName(templateData.contributor.name),
avatar: avatar
}, templateData));
return contributorTemplate(_.assign({avatarBlock: avatarBlock}, templateData));
}
function escapeName(name) {
return name.replace(new RegExp('\\|', 'g'), '&#124;');
}
module.exports = function formatContributor(options, contributor) {
var formatter = _.partial(formatContributionType, [options, contributor]);
var contributions = contributor.contributions
.map(formatter)
.join(' ');
var templateData = {
contributions: contributions,
contributor: contributor,
options: _.assign({imageSize: defaultImageSize}, options)
};
var customTemplate = options.contributorTemplate && _.template(options.contributorTemplate);
return (customTemplate || defaultTemplate)(templateData);
};

View file

@ -1,60 +0,0 @@
import test from 'ava';
import _ from 'lodash/fp';
import formatContributor from './format-contributor';
import contributors from './fixtures/contributors.json';
function fixtures() {
const options = {
projectOwner: 'jfmengels',
projectName: 'all-contributors-cli',
imageSize: 150
};
return {options};
}
test('should format a simple contributor', t => {
const contributor = _.assign(contributors.kentcdodds, {contributions: ['review']});
const {options} = fixtures();
const expected = '[<img src="https://avatars1.githubusercontent.com/u/1500684" width="150px;"/><br /><sub><b>Kent C. Dodds</b></sub>](http://kentcdodds.com)<br />[👀](#review-kentcdodds "Reviewed Pull Requests")';
t.is(formatContributor(options, contributor), expected);
});
test('should format contributor with complex contribution types', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
const expected = '[<img src="https://avatars1.githubusercontent.com/u/1500684" width="150px;"/><br /><sub><b>Kent C. Dodds</b></sub>](http://kentcdodds.com)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=kentcdodds "Documentation") [👀](#review-kentcdodds "Reviewed Pull Requests") [💬](#question-kentcdodds "Answering Questions")';
t.is(formatContributor(options, contributor), expected);
});
test('should format contributor using custom template', t => {
const contributor = contributors.kentcdodds;
const {options} = fixtures();
options.contributorTemplate = '<%= contributor.name %> is awesome!';
const expected = 'Kent C. Dodds is awesome!';
t.is(formatContributor(options, contributor), expected);
});
test('should default image size to 100', t => {
const contributor = _.assign(contributors.kentcdodds, {contributions: ['review']});
const {options} = fixtures();
delete options.imageSize;
const expected = '[<img src="https://avatars1.githubusercontent.com/u/1500684" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](http://kentcdodds.com)<br />[👀](#review-kentcdodds "Reviewed Pull Requests")';
t.is(formatContributor(options, contributor), expected);
});
test('should format contributor with pipes in their name', t => {
const contributor = contributors.pipey;
const {options} = fixtures();
const expected = '[<img src="https://avatars1.githubusercontent.com/u/1500684" width="150px;"/><br /><sub><b>Who &#124; Needs &#124; Pipes?</b></sub>](http://github.com/chrisinajar)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=pipey "Documentation")';
t.is(formatContributor(options, contributor), expected);
});

View file

@ -1,72 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var injectContentBetween = require('../util').markdown.injectContentBetween;
var formatBadge = require('./format-badge');
var formatContributor = require('./format-contributor');
var badgeRegex = /\[!\[All Contributors\]\([a-zA-Z0-9\-\.\/_:\?=]+\)\]\(#\w+\)/;
function injectListBetweenTags(newContent) {
return function (previousContent) {
var tagToLookFor = '<!-- ALL-CONTRIBUTORS-LIST:';
var closingTag = '-->';
var startOfOpeningTagIndex = previousContent.indexOf(tagToLookFor + 'START');
var endOfOpeningTagIndex = previousContent.indexOf(closingTag, startOfOpeningTagIndex);
var startOfClosingTagIndex = previousContent.indexOf(tagToLookFor + 'END', endOfOpeningTagIndex);
if (startOfOpeningTagIndex === -1 || endOfOpeningTagIndex === -1 || startOfClosingTagIndex === -1) {
return previousContent;
}
return previousContent.slice(0, endOfOpeningTagIndex + closingTag.length) +
newContent +
previousContent.slice(startOfClosingTagIndex);
};
}
function formatLine(contributors) {
return '| ' + contributors.join(' | ') + ' |';
}
function createColumnLine(options, contributors) {
var nbColumns = Math.min(options.contributorsPerLine, contributors.length);
return _.repeat(nbColumns, '| :---: ') + '|';
}
function generateContributorsList(options, contributors) {
return _.flow(
_.map(function formatEveryContributor(contributor) {
return formatContributor(options, contributor);
}),
_.chunk(options.contributorsPerLine),
_.map(formatLine),
function insertColumns(lines) {
var columnLine = createColumnLine(options, contributors);
return injectContentBetween(lines, columnLine, 1, 1);
},
_.join('\n'),
function (newContent) {
return '\n' + newContent + '\n';
}
)(contributors);
}
function replaceBadge(newContent) {
return function (previousContent) {
var regexResult = badgeRegex.exec(previousContent);
if (!regexResult) {
return previousContent;
}
return previousContent.slice(0, regexResult.index) +
newContent +
previousContent.slice(regexResult.index + regexResult[0].length);
};
}
module.exports = function generate(options, contributors, fileContent) {
var contributorsList = contributors.length === 0 ? '\n' : generateContributorsList(options, contributors);
var badge = formatBadge(options, contributors);
return _.flow(
injectListBetweenTags(contributorsList),
replaceBadge(badge)
)(fileContent);
};

View file

@ -1,36 +0,0 @@
import test from 'ava';
import {addBadge} from './init-content';
test('should insert badge under title', t => {
const content = [
'# project',
'',
'Description',
'',
'Foo bar'
].join('\n');
const expected = [
'# project',
'[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)',
'',
'Description',
'',
'Foo bar'
].join('\n');
const result = addBadge(content);
t.is(result, expected);
});
test('should add badge if content is empty', t => {
const content = '';
const expected = [
'',
'[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)'
].join('\n');
const result = addBadge(content);
t.is(result, expected);
});

View file

@ -1,25 +0,0 @@
'use strict';
var util = require('../util');
var prompt = require('./prompt');
var initContent = require('./init-content');
var configFile = util.configFile;
var markdown = util.markdown;
function injectInFile(file, fn) {
return markdown.read(file)
.then(content => markdown.write(file, fn(content)));
}
module.exports = function init() {
return prompt()
.then(result => {
return configFile.writeConfig('.all-contributorsrc', result.config)
.then(() => injectInFile(result.contributorFile, initContent.addContributorsList))
.then(() => {
if (result.badgeFile) {
return injectInFile(result.badgeFile, initContent.addBadge);
}
});
});
};

View file

@ -1,49 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var injectContentBetween = require('../util').markdown.injectContentBetween;
var badgeContent = '[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)';
var headerContent = 'Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):';
var listContent = '<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --><!-- ALL-CONTRIBUTORS-LIST:END -->';
var footerContent = 'This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!';
function addBadge(lines) {
return injectContentBetween(lines, badgeContent, 1, 1);
}
function splitAndRejoin(fn) {
return _.flow(
_.split('\n'),
fn,
_.join('\n')
);
}
var findContributorsSection = _.findIndex(function isContributorsSection(str) {
return str
.toLowerCase()
.indexOf('# contributors') === 1;
});
function addContributorsList(lines) {
var insertionLine = findContributorsSection(lines);
if (insertionLine === -1) {
return lines
.concat([
'## Contributors',
'',
headerContent,
'',
listContent,
'',
footerContent
]);
}
return injectContentBetween(lines, listContent, insertionLine + 2, insertionLine + 2);
}
module.exports = {
addBadge: splitAndRejoin(addBadge),
addContributorsList: splitAndRejoin(addContributorsList)
};

View file

@ -1,75 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var inquirer = require('inquirer');
var git = require('../util').git;
var questions = [{
type: 'input',
name: 'projectName',
message: 'What\'s the name of the repository?'
}, {
type: 'input',
name: 'projectOwner',
message: 'Who is the owner of the repository?'
}, {
type: 'input',
name: 'contributorFile',
message: 'In which file should contributors be listed?',
default: 'README.md'
}, {
type: 'confirm',
name: 'needBadge',
message: 'Do you want a badge tallying the number of contributors?'
}, {
type: 'input',
name: 'badgeFile',
message: 'In which file should the badge be shown?',
when: function (answers) {
return answers.needBadge;
},
default: function (answers) {
return answers.contributorFile;
}
}, {
type: 'input',
name: 'imageSize',
message: 'How big should the avatars be? (in px)',
filter: parseInt,
default: 100
}, {
type: 'confirm',
name: 'commit',
message: 'Do you want this badge to auto-commit when contributors are added?',
default: true
}];
var uniqueFiles = _.flow(
_.compact,
_.uniq
);
module.exports = function prompt() {
return git.getRepoInfo()
.then(repoInfo => {
if (repoInfo) {
questions[0].default = repoInfo.projectName;
questions[1].default = repoInfo.projectOwner;
}
return inquirer.prompt(questions);
})
.then(answers => {
return {
config: {
projectName: answers.projectName,
projectOwner: answers.projectOwner,
files: uniqueFiles([answers.contributorFile, answers.badgeFile]),
imageSize: answers.imageSize,
commit: answers.commit,
contributors: []
},
contributorFile: answers.contributorFile,
badgeFile: answers.badgeFile
};
});
};

View file

@ -1,45 +0,0 @@
'use strict';
var pify = require('pify');
var request = pify(require('request'));
function getNextLink(link) {
if (!link) {
return null;
}
var nextLink = link.split(',').find(s => s.includes('rel="next"'));
if (!nextLink) {
return null;
}
return nextLink.split(';')[0].slice(1, -1);
}
function getContributorsPage(url) {
return request.get({
url: url,
headers: {
'User-Agent': 'request'
}
})
.then(res => {
var body = JSON.parse(res.body);
var contributorsIds = body.map(contributor => contributor.login);
var nextLink = getNextLink(res.headers.link);
if (nextLink) {
return getContributorsPage(nextLink).then(nextContributors => {
return contributorsIds.concat(nextContributors);
});
}
return contributorsIds;
});
}
module.exports = function getContributorsFromGithub(owner, name) {
var url = `https://api.github.com/repos/${owner}/${name}/contributors?per_page=100`;
return getContributorsPage(url);
};

View file

@ -1,46 +0,0 @@
import test from 'ava';
import nock from 'nock';
var check = require('./check');
import allContributorsCliResponse from './fixtures/all-contributors.response.json';
import allContributorsCliTransformed from './fixtures/all-contributors.transformed.json';
import reactNativeResponse1 from './fixtures/react-native.response.1.json';
import reactNativeResponse2 from './fixtures/react-native.response.2.json';
import reactNativeResponse3 from './fixtures/react-native.response.3.json';
import reactNativeResponse4 from './fixtures/react-native.response.4.json';
import reactNativeTransformed from './fixtures/react-native.transformed.json';
test.before(() => {
nock('https://api.github.com')
.persist()
.get('/repos/jfmengels/all-contributors-cli/contributors?per_page=100')
.reply(200, allContributorsCliResponse)
.get('/repos/facebook/react-native/contributors?per_page=100')
.reply(200, reactNativeResponse1, {
Link: '<https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last"'
})
.get('/repositories/29028775/contributors?per_page=100&page=2')
.reply(200, reactNativeResponse2, {
Link: '<https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="prev"'
})
.get('/repositories/29028775/contributors?per_page=100&page=3')
.reply(200, reactNativeResponse3, {
Link: '<https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="prev"'
})
.get('/repositories/29028775/contributors?per_page=100&page=4')
.reply(200, reactNativeResponse4, {
Link: '<https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="prev"'
});
});
test('Handle a single results page correctly', async t => {
const transformed = await check('jfmengels', 'all-contributors-cli');
t.deepEqual(transformed, allContributorsCliTransformed);
});
test('Handle multiple results pages correctly', async t => {
const transformed = await check('facebook', 'react-native');
t.deepEqual(transformed, reactNativeTransformed);
});

View file

@ -1,37 +0,0 @@
'use strict';
var fs = require('fs');
var pify = require('pify');
var _ = require('lodash/fp');
function readConfig(configPath) {
try {
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error('Configuration file not found: ' + configPath);
}
throw error;
}
}
function writeConfig(configPath, content) {
return pify(fs.writeFile)(configPath, JSON.stringify(content, null, 2) + '\n');
}
function writeContributors(configPath, contributors) {
var config;
try {
config = readConfig(configPath);
} catch (error) {
return Promise.reject(error);
}
var content = _.assign(config, {contributors: contributors});
return writeConfig(configPath, content);
}
module.exports = {
readConfig: readConfig,
writeConfig: writeConfig,
writeContributors: writeContributors
};

View file

@ -1,13 +0,0 @@
import test from 'ava';
import configFile from './config-file.js';
const absentFile = './abc';
const expected = 'Configuration file not found: ' + absentFile;
test('Reading an absent configuration file throws a helpful error', t => {
t.throws(() => configFile.readConfig(absentFile), expected);
});
test('Writing contributors in an absent configuration file throws a helpful error', t => {
t.throws(configFile.writeContributors(absentFile, []), expected);
});

View file

@ -1,97 +0,0 @@
'use strict';
var _ = require('lodash/fp');
var linkToCommits = 'https://github.com/<%= options.projectOwner %>/<%= options.projectName %>/commits?author=<%= contributor.login %>';
var linkToIssues = 'https://github.com/<%= options.projectOwner %>/<%= options.projectName %>/issues?q=author%3A<%= contributor.login %>';
var defaultTypes = {
blog: {
symbol: '📝',
description: 'Blogposts'
},
bug: {
symbol: '🐛',
description: 'Bug reports',
link: linkToIssues
},
code: {
symbol: '💻',
description: 'Code',
link: linkToCommits
},
design: {
symbol: '🎨',
description: 'Design'
},
doc: {
symbol: '📖',
description: 'Documentation',
link: linkToCommits
},
eventOrganizing: {
symbol: '📋',
description: 'Event Organizing'
},
example: {
symbol: '💡',
description: 'Examples'
},
financial: {
symbol: '💵',
description: 'Financial'
},
fundingFinding: {
symbol: '🔍',
description: 'Funding Finding'
},
ideas: {
symbol: '🤔',
description: 'Ideas, Planning, & Feedback'
},
infra: {
symbol: '🚇',
description: 'Infrastructure (Hosting, Build-Tools, etc)'
},
plugin: {
symbol: '🔌',
description: 'Plugin/utility libraries'
},
question: {
symbol: '💬',
description: 'Answering Questions'
},
review: {
symbol: '👀',
description: 'Reviewed Pull Requests'
},
talk: {
symbol: '📢',
description: 'Talks'
},
test: {
symbol: '⚠️',
description: 'Tests',
link: linkToCommits
},
tool: {
symbol: '🔧',
description: 'Tools'
},
translation: {
symbol: '🌍',
description: 'Translation'
},
tutorial: {
symbol: '✅',
description: 'Tutorials'
},
video: {
symbol: '📹',
description: 'Videos'
}
};
module.exports = function (options) {
return _.assign(defaultTypes, options.types);
};

View file

@ -1,61 +0,0 @@
'use strict';
var path = require('path');
var spawn = require('child_process').spawn;
var _ = require('lodash/fp');
var pify = require('pify');
var commitTemplate = '<%= (newContributor ? "Add" : "Update") %> @<%= username %> as a contributor';
var getRemoteOriginData = pify(cb => {
var output = '';
var git = spawn('git', 'config --get remote.origin.url'.split(' '));
git.stdout.on('data', function (data) {
output += data;
});
git.stderr.on('data', cb);
git.on('close', function () {
cb(null, output);
});
});
function parse(originUrl) {
var result = /:(\w+)\/([A-Za-z0-9-_]+)/.exec(originUrl);
if (!result) {
return null;
}
return {
projectOwner: result[1],
projectName: result[2]
};
}
function getRepoInfo() {
return getRemoteOriginData()
.then(parse);
}
var spawnGitCommand = pify((args, cb) => {
var git = spawn('git', args);
git.stderr.on('data', cb);
git.on('close', cb);
});
function commit(options, data) {
var files = options.files.concat(options.config);
var absolutePathFiles = files.map(file => {
return path.resolve(process.cwd(), file);
});
return spawnGitCommand(['add'].concat(absolutePathFiles))
.then(() => {
var commitMessage = _.template(options.commitTemplate || commitTemplate)(data);
return spawnGitCommand(['commit', '-m', commitMessage]);
});
}
module.exports = {
commit: commit,
getRepoInfo: getRepoInfo
};

View file

@ -1,26 +0,0 @@
'use strict';
var fs = require('fs');
var pify = require('pify');
function read(filePath) {
return pify(fs.readFile)(filePath, 'utf8');
}
function write(filePath, content) {
return pify(fs.writeFile)(filePath, content);
}
function injectContentBetween(lines, content, startIndex, endIndex) {
return [].concat(
lines.slice(0, startIndex),
content,
lines.slice(endIndex)
);
}
module.exports = {
read: read,
write: write,
injectContentBetween: injectContentBetween
};

87
other/CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,87 @@
# Contributor Covenant Code of Conduct
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Our Pledge](#our-pledge)
- [Our Standards](#our-standards)
- [Our Responsibilities](#our-responsibilities)
- [Scope](#scope)
- [Enforcement](#enforcement)
- [Attribution](#attribution)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at kent+coc@doddsfamily.us. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

75
other/MAINTAINING.md Normal file
View file

@ -0,0 +1,75 @@
# Maintaining
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Code of Conduct](#code-of-conduct)
- [Issues](#issues)
- [Pull Requests](#pull-requests)
- [Release](#release)
- [Thanks!](#thanks)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
This is documentation for maintainers of this project.
## Code of Conduct
Please review, understand, and be an example of it. Violations of the code of conduct are
taken seriously, even (especially) for maintainers.
## Issues
We want to support and build the community. We do that best by helping people learn to solve
their own problems. We have an issue template and hopefully most folks follow it. If it's
not clear what the issue is, invite them to create a minimal reproduction of what they're trying
to accomplish or the bug they think they've found.
Once it's determined that a code change is necessary, point people to
[makeapullrequest.com](http://makeapullrequest.com) and invite them to make a pull request.
If they're the one who needs the feature, they're the one who can build it. If they need
some hand holding and you have time to lend a hand, please do so. It's an investment into
another human being, and an investment into a potential maintainer.
Remember that this is open source, so the code is not yours, it's ours. If someone needs a change
in the codebase, you don't have to make it happen yourself. Commit as much time to the project
as you want/need to. Nobody can ask any more of you than that.
## Pull Requests
As a maintainer, you're fine to make your branches on the main repo or on your own fork. Either
way is fine.
When we receive a pull request, a travis build is kicked off automatically (see the `.travis.yml`
for what runs in the travis build). We avoid merging anything that breaks the travis build.
Please review PRs and focus on the code rather than the individual. You never know when this is
someone's first ever PR and we want their experience to be as positive as possible, so be
uplifting and constructive.
When you merge the pull request, 99% of the time you should use the
[Squash and merge](https://help.github.com/articles/merging-a-pull-request/) feature. This keeps
our git history clean, but more importantly, this allows us to make any necessary changes to the
commit message so we release what we want to release. See the next section on Releases for more
about that.
## Release
Our releases are automatic. They happen whenever code lands into `master`. A travis build gets
kicked off and if it's successful, a tool called
[`semantic-release`](https://github.com/semantic-release/semantic-release) is used to
automatically publish a new release to npm as well as a changelog to GitHub. It is only able to
determine the version and whether a release is necessary by the git commit messages. With this
in mind, **please brush up on [the commit message convention][commit] which drives our releases.**
> One important note about this: Please make sure that commit messages do NOT contain the words
> "BREAKING CHANGE" in them unless we want to push a major version. I've been burned by this
> more than once where someone will include "BREAKING CHANGE: None" and it will end up releasing
> a new major version. Not a huge deal honestly, but kind of annoying...
## Thanks!
Thank you so much for helping to maintain this project!
[commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md

47
other/manual-releases.md Normal file
View file

@ -0,0 +1,47 @@
# manual-releases
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
This project has an automated release set up. So things are only released when there are
useful changes in the code that justify a release. But sometimes things get messed up one way or another
and we need to trigger the release ourselves. When this happens, simply bump the number below and commit
that with the following commit message based on your needs:
**Major**
```
fix(release): manually release a major version
There was an issue with a major release, so this manual-releases.md
change is to release a new major version.
Reference: #<the number of a relevant pull request, issue, or commit>
BREAKING CHANGE: <mention any relevant breaking changes (this is what triggers the major version change so don't skip this!)>
```
**Minor**
```
feat(release): manually release a minor version
There was an issue with a minor release, so this manual-releases.md
change is to release a new minor version.
Reference: #<the number of a relevant pull request, issue, or commit>
```
**Patch**
```
fix(release): manually release a patch version
There was an issue with a patch release, so this manual-releases.md
change is to release a new patch version.
Reference: #<the number of a relevant pull request, issue, or commit>
```
The number of times we've had to do a manual release is: 0

View file

@ -3,24 +3,25 @@
"version": "0.0.0-semantically-released",
"description": "Tool to easily add recognition for new contributors",
"bin": {
"all-contributors": "cli.js"
"all-contributors": "dist/cli.js"
},
"files": ["dist"],
"engines": {
"node": ">=4"
},
"scripts": {
"all-contributors": "./cli.js",
"test": "xo && nyc ava",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
"add-contributor": "kcd-scripts contributors add",
"build": "kcd-scripts build",
"lint": "kcd-scripts lint",
"test": "kcd-scripts test",
"validate": "kcd-scripts validate",
"precommit": "kcd-scripts precommit"
},
"repository": {
"type": "git",
"url": "https://github.com/jfmengels/all-contributors-cli.git"
},
"keywords": [
"all-contributors",
"contributors"
],
"keywords": ["all-contributors", "contributors"],
"author": "Jeroen Engels <jfm.engels@gmail.com>",
"license": "MIT",
"bugs": {
@ -30,39 +31,25 @@
"dependencies": {
"async": "^2.0.0-rc.1",
"chalk": "^2.3.0",
"inquirer": "^3.0.1",
"inquirer": "^4.0.0",
"lodash": "^4.11.2",
"pify": "^2.3.0",
"pify": "^3.0.0",
"request": "^2.72.0",
"yargs": "^4.7.0"
"yargs": "^10.0.3"
},
"devDependencies": {
"ava": "^0.14.0",
"nock": "^8.0.0",
"nyc": "^6.4.2",
"semantic-release": "^6.3.2",
"xo": "^0.15.0"
"kcd-scripts": "^0.29.0",
"nock": "^9.1.0"
},
"ava": {
"files": [
"lib/**/*.test.js"
]
},
"files": [
"cli.js",
"lib",
"!lib/**/*.test.js",
"!lib/**/fixtures"
],
"xo": {
"space": 2,
"eslintIgnore": ["node_modules", "coverage", "dist"],
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
"rules": {
"camelcase": [
2,
{
"properties": "never"
}
]
"camelcase": "off",
"no-process-exit": "off",
"import/extensions": "off",
"func-names": "off",
"consistent-return": "off"
}
}
}

2
prettier.config.js Normal file
View file

@ -0,0 +1,2 @@
// this is really only here for editor integrations
module.exports = require('kcd-scripts/dist/config/prettierrc')

168
src/cli.js Executable file
View file

@ -0,0 +1,168 @@
#!/usr/bin/env node
/* eslint-disable no-console */
const path = require('path')
const yargs = require('yargs')
const chalk = require('chalk')
const inquirer = require('inquirer')
const init = require('./lib/init')
const generate = require('./lib/generate')
const util = require('./lib/util')
const updateContributors = require('./lib/contributors')
const cwd = process.cwd()
const defaultRCFile = path.join(cwd, '.all-contributorsrc')
const yargv = yargs
.help('help')
.alias('h', 'help')
.alias('v', 'version')
.version()
.command('generate', 'Generate the list of contributors')
.usage('Usage: $0 generate')
.command('add', 'add a new contributor')
.usage('Usage: $0 add <username> <contribution>')
.command('init', 'Prepare the project to be used with this tool')
.usage('Usage: $0 init')
.command(
'check',
'Compares contributors from GitHub with the ones credited in .all-contributorsrc',
)
.usage('Usage: $0 check')
.boolean('commit')
.default('files', ['README.md'])
.default('contributorsPerLine', 7)
.default('contributors', [])
.default('config', defaultRCFile)
.config('config', configPath => {
try {
return util.configFile.readConfig(configPath)
} catch (error) {
if (configPath !== defaultRCFile) {
onError(error)
}
}
}).argv
function startGeneration(argv) {
return Promise.all(
argv.files.map(file => {
const filePath = path.join(cwd, file)
return util.markdown.read(filePath).then(fileContent => {
const newFileContent = generate(argv, argv.contributors, fileContent)
return util.markdown.write(filePath, newFileContent)
})
}),
)
}
function addContribution(argv) {
const username = argv._[1]
const contributions = argv._[2]
// Add or update contributor in the config file
return updateContributors(argv, username, contributions).then(data => {
argv.contributors = data.contributors
return startGeneration(argv).then(() => {
if (argv.commit) {
return util.git.commit(argv, data)
}
})
})
}
function checkContributors(argv) {
const configData = util.configFile.readConfig(argv.config)
return util
.check(configData.projectOwner, configData.projectName)
.then(ghContributors => {
const knownContributions = configData.contributors.reduce((obj, item) => {
obj[item.login] = item.contributions
return obj
}, {})
const knownContributors = configData.contributors.map(
contributor => contributor.login,
)
const missingInConfig = ghContributors.filter(
login => !knownContributors.includes(login),
)
const missingFromGithub = knownContributors.filter(login => {
return (
!ghContributors.includes(login) &&
(knownContributions[login].includes('code') ||
knownContributions[login].includes('test'))
)
})
if (missingInConfig.length) {
process.stdout.write(
chalk.bold('Missing contributors in .all-contributorsrc:\n'),
)
process.stdout.write(` ${missingInConfig.join(', ')}\n`)
}
if (missingFromGithub.length) {
process.stdout.write(
chalk.bold('Unknown contributors found in .all-contributorsrc:\n'),
)
process.stdout.write(`${missingFromGithub.join(', ')}\n`)
}
})
}
function onError(error) {
if (error) {
console.error(error.message)
process.exit(1)
}
process.exit(0)
}
function promptForCommand(argv) {
const questions = [
{
type: 'list',
name: 'command',
message: 'What do you want to do?',
choices: [
{
name: 'Add a new contributor or add a new contribution type',
value: 'add',
},
{
name: 'Re-generate the contributors list',
value: 'generate',
},
{
name: 'Compare contributors from GitHub with the credited ones',
value: 'check',
},
],
when: !argv._[0],
default: 0,
},
]
return inquirer.prompt(questions).then(answers => {
return answers.command || argv._[0]
})
}
promptForCommand(yargv)
.then(command => {
switch (command) {
case 'init':
return init()
case 'generate':
return startGeneration(yargv)
case 'add':
return addContribution(yargv)
case 'check':
return checkContributors(yargv)
default:
throw new Error(`Unknown command ${command}`)
}
})
.catch(onError)

View file

@ -0,0 +1,184 @@
import addContributor from '../add'
function mockInfoFetcher(username) {
return Promise.resolve({
login: username,
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
})
}
function fixtures() {
const options = {
contributors: [
{
login: 'login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: ['code'],
},
{
login: 'login2',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [{type: 'blog', url: 'www.blog.url/path'}, 'code'],
},
],
}
return {options}
}
function caseFixtures() {
const options = {
contributors: [
{
login: 'Login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: ['code'],
},
],
}
return {options}
}
test('callback with error if infoFetcher fails', async () => {
const {options} = fixtures()
const username = 'login3'
const contributions = ['doc']
const error = new Error('infoFetcher error')
function infoFetcher() {
return Promise.reject(error)
}
const resolvedError = await addContributor(
options,
username,
contributions,
infoFetcher,
).catch(e => e)
expect(resolvedError).toBe(error)
})
test('add new contributor at the end of the list of contributors', () => {
const {options} = fixtures()
const username = 'login3'
const contributions = ['doc']
return addContributor(options, username, contributions, mockInfoFetcher).then(
contributors => {
expect(contributors.length).toBe(3)
expect(contributors[2]).toEqual({
login: 'login3',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: ['doc'],
})
},
)
})
test('add new contributor at the end of the list of contributors with a url link', () => {
const {options} = fixtures()
const username = 'login3'
const contributions = ['doc']
options.url = 'www.foo.bar'
return addContributor(options, username, contributions, mockInfoFetcher).then(
contributors => {
expect(contributors.length).toBe(3)
expect(contributors[2]).toEqual({
login: 'login3',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: [{type: 'doc', url: 'www.foo.bar'}],
})
},
)
})
test(`should not update an existing contributor's contributions where nothing has changed`, () => {
const {options} = fixtures()
const username = 'login2'
const contributions = ['blog', 'code']
return addContributor(options, username, contributions, mockInfoFetcher).then(
contributors => {
expect(contributors).toEqual(options.contributors)
},
)
})
test(`should not update an existing contributor's contributions where nothing has changed but the casing`, () => {
const {options} = caseFixtures()
const username = 'login1'
const contributions = ['code']
return addContributor(options, username, contributions, mockInfoFetcher).then(
contributors => {
expect(contributors).toEqual(options.contributors)
},
)
})
test(`should update an existing contributor's contributions if a new type is added`, () => {
const {options} = fixtures()
const username = 'login1'
const contributions = ['bug']
return addContributor(options, username, contributions, mockInfoFetcher).then(
contributors => {
expect(contributors.length).toBe(2)
expect(contributors[0]).toEqual({
login: 'login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: ['code', 'bug'],
})
},
)
})
test(`should update an existing contributor's contributions if a new type is added with different username case`, () => {
const {options} = caseFixtures()
const username = 'login1'
const contributions = ['bug']
return addContributor(options, username, contributions, mockInfoFetcher).then(
contributors => {
expect(contributors.length).toBe(1)
expect(contributors[0]).toEqual({
login: 'Login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: ['code', 'bug'],
})
},
)
})
test(`should update an existing contributor's contributions if a new type is added with a link`, () => {
const {options} = fixtures()
const username = 'login1'
const contributions = ['bug']
options.url = 'www.foo.bar'
return addContributor(options, username, contributions, mockInfoFetcher).then(
contributors => {
expect(contributors.length).toBe(2)
expect(contributors[0]).toEqual({
login: 'login1',
name: 'Some name',
avatar_url: 'www.avatar.url',
profile: 'www.profile.url',
contributions: ['code', {type: 'bug', url: 'www.foo.bar'}],
})
},
)
})

View file

@ -0,0 +1,69 @@
import nock from 'nock'
import getUserInfo from '../github'
async function rejects(promise) {
const error = await promise.catch(e => e)
expect(error).toBeTruthy()
}
test('handle errors', async () => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.replyWithError(404)
await rejects(getUserInfo('nodisplayname'))
})
test('handle github errors', async () => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
message:
"API rate limit exceeded for 0.0.0.0. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
documentation_url: 'https://developer.github.com/v3/#rate-limiting',
})
await rejects(getUserInfo('nodisplayname'))
})
test('fill in the name when null is returned', async () => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
login: 'nodisplayname',
name: null,
avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400',
html_url: 'https://github.com/nodisplayname',
})
const info = await getUserInfo('nodisplayname')
expect(info.name).toBe('nodisplayname')
})
test('fill in the name when an empty string is returned', async () => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
login: 'nodisplayname',
name: '',
avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400',
html_url: 'https://github.com/nodisplayname',
})
const info = await getUserInfo('nodisplayname')
expect(info.name).toBe('nodisplayname')
})
test('append http when no absolute link is provided', async () => {
nock('https://api.github.com')
.get('/users/nodisplayname')
.reply(200, {
login: 'nodisplayname',
name: '',
avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400',
html_url: 'www.github.com/nodisplayname',
})
const info = await getUserInfo('nodisplayname')
expect(info.profile).toBe('http://www.github.com/nodisplayname')
})

63
src/contributors/add.js Normal file
View file

@ -0,0 +1,63 @@
const _ = require('lodash/fp')
function uniqueTypes(contribution) {
return contribution.type || contribution
}
function formatContributions(options, existing, types) {
if (options.url) {
return (existing || []).concat(
types.map(type => {
return {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(contributor => {
if (username.toLowerCase() !== contributor.login.toLowerCase()) {
return contributor
}
return updateContributor(options, contributor, contributions)
})
}
function addNewContributor(options, username, contributions, infoFetcher) {
return infoFetcher(username).then(userData => {
const contributor = _.assign(userData, {
contributions: formatContributions(options, [], contributions),
})
return options.contributors.concat(contributor)
})
}
module.exports = function addContributor(
options,
username,
contributions,
infoFetcher,
) {
// case insensitive find
const exists = _.find(contributor => {
return contributor.login.toLowerCase() === username.toLowerCase()
}, options.contributors)
if (exists) {
return Promise.resolve(
updateExistingContributor(options, username, contributions),
)
}
return addNewContributor(options, username, contributions, infoFetcher)
}

View file

@ -0,0 +1,30 @@
const pify = require('pify')
const request = pify(require('request'))
module.exports = function getUserInfo(username) {
return request
.get({
url: `https://api.github.com/users/${username}`,
headers: {
'User-Agent': 'request',
},
})
.then(res => {
const body = JSON.parse(res.body)
let profile = body.blog || body.html_url
// Github throwing specific errors as 200...
if (!profile && body.message) {
throw new Error(body.message)
}
profile = profile.startsWith('http') ? profile : `http://${profile}`
return {
login: body.login,
name: body.name || username,
avatar_url: body.avatar_url,
profile,
}
})
}

36
src/contributors/index.js Normal file
View file

@ -0,0 +1,36 @@
const _ = require('lodash/fp')
const util = require('../util')
const add = require('./add')
const github = require('./github')
const prompt = require('./prompt')
function isNewContributor(contributorList, username) {
return !_.find({login: username}, contributorList)
}
module.exports = function addContributor(options, username, contributions) {
const answersP = prompt(options, username, contributions)
const contributorsP = answersP.then(answers =>
add(options, answers.username, answers.contributions, github),
)
const writeContributorsP = contributorsP.then(contributors =>
util.configFile.writeContributors(options.config, contributors),
)
return Promise.all([answersP, contributorsP, writeContributorsP]).then(
res => {
const answers = res[0]
const contributors = res[1]
return {
username: answers.username,
contributions: answers.contributions,
contributors,
newContributor: isNewContributor(
options.contributors,
answers.username,
),
}
},
)
}

View file

@ -0,0 +1,76 @@
const _ = require('lodash/fp')
const inquirer = require('inquirer')
const util = require('../util')
const contributionChoices = _.flow(
util.contributionTypes,
_.toPairs,
_.sortBy(pair => {
return pair[1].description
}),
_.map(pair => {
return {
name: `${pair[1].symbol} ${pair[1].description}`,
value: pair[0],
}
}),
)
function getQuestions(options, username, contributions) {
return [
{
type: 'input',
name: 'username',
message: "What is the contributor's GitHub username?",
when: !username,
},
{
type: 'checkbox',
name: 'contributions',
message: 'What are the contribution types?',
when: !contributions,
default: function(answers) {
// default values for contributions when updating existing users
answers.username = answers.username || username
return options.contributors
.filter(
entry =>
entry.login.toLowerCase() === answers.username.toLowerCase(),
)
.reduce(
(allEntries, entry) => allEntries.concat(entry.contributions),
[],
)
},
choices: contributionChoices(options),
validate: function(input, answers) {
answers.username = answers.username || username
const previousContributions = options.contributors
.filter(
entry =>
entry.login.toLowerCase() === answers.username.toLowerCase(),
)
.reduce(
(allEntries, entry) => allEntries.concat(entry.contributions),
[],
)
if (!input.length) {
return 'Use space to select at least one contribution type.'
} else if (_.isEqual(input, previousContributions)) {
return 'Nothing changed, use space to select contribution types.'
}
return true
},
},
]
}
module.exports = function prompt(options, username, contributions) {
const defaults = {
username,
contributions: contributions && contributions.split(','),
}
const questions = getQuestions(options, username, contributions)
return inquirer.prompt(questions).then(_.assign(defaults))
}

View file

@ -4,28 +4,20 @@
"name": "Kent C. Dodds",
"profile": "http://kentcdodds.com",
"avatar_url": "https://avatars1.githubusercontent.com/u/1500684",
"contributions": [
"doc",
"review",
"question"
]
"contributions": ["doc", "review", "question"]
},
"bogas04": {
"login": "bogas04",
"name": "Divjot Singh",
"profile": "http://bogas04.github.io",
"avatar_url": "https://avatars1.githubusercontent.com/u/6177621",
"contributions": [
"review"
]
"contributions": ["review"]
},
"pipey": {
"login": "pipey",
"name": "Who | Needs | Pipes?",
"profile": "http://github.com/chrisinajar",
"avatar_url": "https://avatars1.githubusercontent.com/u/1500684",
"contributions": [
"doc"
]
"contributions": ["doc"]
}
}

View file

@ -0,0 +1,26 @@
import _ from 'lodash/fp'
import formatBadge from '../format-badge'
test('return badge with the number of contributors', () => {
const options = {}
const expected8 =
'[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors)'
const expected16 =
'[![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors)'
expect(formatBadge(options, _.times(_.constant({}), 8))).toBe(expected8)
expect(formatBadge(options, _.times(_.constant({}), 16))).toBe(expected16)
})
test('be able to specify custom badge template', () => {
const options = {
badgeTemplate: 'We have <%= contributors.length %> contributors',
}
expect(formatBadge(options, _.times(_.constant({}), 8))).toBe(
'We have 8 contributors',
)
expect(formatBadge(options, _.times(_.constant({}), 16))).toBe(
'We have 16 contributors',
)
})

View file

@ -0,0 +1,155 @@
import formatContributionType from '../format-contribution-type'
import contributors from './fixtures/contributors.json'
const fixtures = () => {
const options = {
projectOwner: 'jfmengels',
projectName: 'all-contributors-cli',
imageSize: 100,
}
return {options}
}
test('return corresponding symbol', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
expect(formatContributionType(options, contributor, 'tool')).toBe(
'[🔧](#tool-kentcdodds "Tools")',
)
expect(formatContributionType(options, contributor, 'question')).toBe(
'[💬](#question-kentcdodds "Answering Questions")',
)
})
test('return link to commits', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
const expectedLink =
'https://github.com/jfmengels/all-contributors-cli/commits?author=kentcdodds'
expect(formatContributionType(options, contributor, 'code')).toBe(
`[💻](${expectedLink} "Code")`,
)
expect(formatContributionType(options, contributor, 'doc')).toBe(
`[📖](${expectedLink} "Documentation")`,
)
expect(formatContributionType(options, contributor, 'test')).toBe(
`[⚠️](${expectedLink} "Tests")`,
)
})
test('return link to issues', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
const expected =
'[🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3Akentcdodds "Bug reports")'
expect(formatContributionType(options, contributor, 'bug')).toBe(expected)
})
test('make any symbol into a link if contribution is an object', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
const contribution = {
type: 'tool',
url: 'www.foo.bar',
}
expect(formatContributionType(options, contributor, contribution)).toBe(
'[🔧](www.foo.bar "Tools")',
)
})
test('override url for given types', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
const contribution = {
type: 'code',
url: 'www.foo.bar',
}
expect(formatContributionType(options, contributor, contribution)).toBe(
'[💻](www.foo.bar "Code")',
)
})
test('be able to add types to the symbol list', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
options.types = {
cheerful: {symbol: ':smiley:'},
}
expect(formatContributionType(options, contributor, 'cheerful')).toBe(
'[:smiley:](#cheerful-kentcdodds "")',
)
expect(
formatContributionType(options, contributor, {
type: 'cheerful',
url: 'www.foo.bar',
}),
).toBe('[:smiley:](www.foo.bar "")')
})
test('be able to add types with template to the symbol list', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
options.types = {
web: {
symbol: ':web:',
link: 'www.<%= contributor.login %>.com',
},
}
expect(formatContributionType(options, contributor, 'web')).toBe(
'[:web:](www.kentcdodds.com "")',
)
})
test('be able to override existing types', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
options.types = {
code: {symbol: ':smiley:'},
}
expect(formatContributionType(options, contributor, 'code')).toBe(
'[:smiley:](#code-kentcdodds "")',
)
expect(
formatContributionType(options, contributor, {
type: 'code',
url: 'www.foo.bar',
}),
).toBe('[:smiley:](www.foo.bar "")')
})
test('be able to override existing templates', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
options.types = {
code: {
symbol: ':web:',
link: 'www.<%= contributor.login %>.com',
},
}
expect(formatContributionType(options, contributor, 'code')).toBe(
'[:web:](www.kentcdodds.com "")',
)
expect(
formatContributionType(options, contributor, {
type: 'code',
url: 'www.foo.bar',
}),
).toBe('[:web:](www.foo.bar "")')
})
test('throw a helpful error on unknown type', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
expect(() =>
formatContributionType(options, contributor, 'docs'),
).toThrowError('Unknown contribution type docs for contributor kentcdodds')
})

View file

@ -0,0 +1,67 @@
import _ from 'lodash/fp'
import formatContributor from '../format-contributor'
import contributors from './fixtures/contributors.json'
function fixtures() {
const options = {
projectOwner: 'jfmengels',
projectName: 'all-contributors-cli',
imageSize: 150,
}
return {options}
}
test('format a simple contributor', () => {
const contributor = _.assign(contributors.kentcdodds, {
contributions: ['review'],
})
const {options} = fixtures()
const expected =
'[<img src="https://avatars1.githubusercontent.com/u/1500684" width="150px;"/><br /><sub><b>Kent C. Dodds</b></sub>](http://kentcdodds.com)<br />[👀](#review-kentcdodds "Reviewed Pull Requests")'
expect(formatContributor(options, contributor)).toBe(expected)
})
test('format contributor with complex contribution types', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
const expected =
'[<img src="https://avatars1.githubusercontent.com/u/1500684" width="150px;"/><br /><sub><b>Kent C. Dodds</b></sub>](http://kentcdodds.com)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=kentcdodds "Documentation") [👀](#review-kentcdodds "Reviewed Pull Requests") [💬](#question-kentcdodds "Answering Questions")'
expect(formatContributor(options, contributor)).toBe(expected)
})
test('format contributor using custom template', () => {
const contributor = contributors.kentcdodds
const {options} = fixtures()
options.contributorTemplate = '<%= contributor.name %> is awesome!'
const expected = 'Kent C. Dodds is awesome!'
expect(formatContributor(options, contributor)).toBe(expected)
})
test('default image size to 100', () => {
const contributor = _.assign(contributors.kentcdodds, {
contributions: ['review'],
})
const {options} = fixtures()
delete options.imageSize
const expected =
'[<img src="https://avatars1.githubusercontent.com/u/1500684" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](http://kentcdodds.com)<br />[👀](#review-kentcdodds "Reviewed Pull Requests")'
expect(formatContributor(options, contributor)).toBe(expected)
})
test('format contributor with pipes in their name', () => {
const contributor = contributors.pipey
const {options} = fixtures()
const expected =
'[<img src="https://avatars1.githubusercontent.com/u/1500684" width="150px;"/><br /><sub><b>Who &#124; Needs &#124; Pipes?</b></sub>](http://github.com/chrisinajar)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=pipey "Documentation")'
expect(formatContributor(options, contributor)).toBe(expected)
})

View file

@ -1,6 +1,5 @@
import test from 'ava';
import contributors from './fixtures/contributors.json';
import generate from './';
import generate from '../'
import contributors from './fixtures/contributors.json'
function fixtures() {
const options = {
@ -8,17 +7,17 @@ function fixtures() {
projectName: 'all-contributors',
imageSize: 100,
contributorsPerLine: 5,
contributors: contributors,
contributorTemplate: '<%= contributor.name %> is awesome!'
};
contributors,
contributorTemplate: '<%= contributor.name %> is awesome!',
}
const jfmengels = {
login: 'jfmengels',
name: 'Jeroen Engels',
html_url: 'https://github.com/jfmengels',
avatar_url: 'https://avatars.githubusercontent.com/u/3869412?v=3',
contributions: ['doc']
};
contributions: ['doc'],
}
const content = [
'# project',
@ -29,16 +28,16 @@ function fixtures() {
'These people contributed to the project:',
'<!-- ALL-CONTRIBUTORS-LIST:START -->FOO BAR BAZ<!-- ALL-CONTRIBUTORS-LIST:END -->',
'',
'Thanks a lot everyone!'
].join('\n');
'Thanks a lot everyone!',
].join('\n')
return {options, jfmengels, content};
return {options, jfmengels, content}
}
test('should replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of contributors', t => {
const {kentcdodds, bogas04} = contributors;
const {options, jfmengels, content} = fixtures();
const contributorList = [kentcdodds, bogas04, jfmengels];
test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of contributors', () => {
const {kentcdodds, bogas04} = contributors
const {options, jfmengels, content} = fixtures()
const contributorList = [kentcdodds, bogas04, jfmengels]
const expected = [
'# project',
'',
@ -51,18 +50,26 @@ test('should replace the content between the ALL-CONTRIBUTORS-LIST tags by a tab
'| :---: | :---: | :---: |',
'<!-- ALL-CONTRIBUTORS-LIST:END -->',
'',
'Thanks a lot everyone!'
].join('\n');
'Thanks a lot everyone!',
].join('\n')
const result = generate(options, contributorList, content);
const result = generate(options, contributorList, content)
t.is(result, expected);
});
expect(result).toBe(expected)
})
test('should split contributors into multiples lines when there are too many', t => {
const {kentcdodds} = contributors;
const {options, content} = fixtures();
const contributorList = [kentcdodds, kentcdodds, kentcdodds, kentcdodds, kentcdodds, kentcdodds, kentcdodds];
test('split contributors into multiples lines when there are too many', () => {
const {kentcdodds} = contributors
const {options, content} = fixtures()
const contributorList = [
kentcdodds,
kentcdodds,
kentcdodds,
kentcdodds,
kentcdodds,
kentcdodds,
kentcdodds,
]
const expected = [
'# project',
'',
@ -76,34 +83,30 @@ test('should split contributors into multiples lines when there are too many', t
'| Kent C. Dodds is awesome! | Kent C. Dodds is awesome! |',
'<!-- ALL-CONTRIBUTORS-LIST:END -->',
'',
'Thanks a lot everyone!'
].join('\n');
'Thanks a lot everyone!',
].join('\n')
const result = generate(options, contributorList, content);
const result = generate(options, contributorList, content)
t.is(result, expected);
});
expect(result).toBe(expected)
})
test('should not inject anything if there is no tags to inject content in', t => {
const {kentcdodds} = contributors;
const {options} = fixtures();
const contributorList = [kentcdodds];
const content = [
'# project',
'',
'Description',
'',
'License: MIT'
].join('\n');
test('not inject anything if there is no tags to inject content in', () => {
const {kentcdodds} = contributors
const {options} = fixtures()
const contributorList = [kentcdodds]
const content = ['# project', '', 'Description', '', 'License: MIT'].join(
'\n',
)
const result = generate(options, contributorList, content);
t.is(result, content);
});
const result = generate(options, contributorList, content)
expect(result).toBe(content)
})
test('should not inject anything if start tag is malformed', t => {
const {kentcdodds} = contributors;
const {options} = fixtures();
const contributorList = [kentcdodds];
test('not inject anything if start tag is malformed', () => {
const {kentcdodds} = contributors
const {options} = fixtures()
const contributorList = [kentcdodds]
const content = [
'# project',
'',
@ -111,17 +114,17 @@ test('should not inject anything if start tag is malformed', t => {
'<!-- ALL-CONTRIBUTORS-LIST:SSSSSSSTART -->',
'<!-- ALL-CONTRIBUTORS-LIST:END -->',
'',
'License: MIT'
].join('\n');
'License: MIT',
].join('\n')
const result = generate(options, contributorList, content);
t.is(result, content);
});
const result = generate(options, contributorList, content)
expect(result).toBe(content)
})
test('should not inject anything if end tag is malformed', t => {
const {kentcdodds} = contributors;
const {options} = fixtures();
const contributorList = [kentcdodds];
test('not inject anything if end tag is malformed', () => {
const {kentcdodds} = contributors
const {options} = fixtures()
const contributorList = [kentcdodds]
const content = [
'# project',
'',
@ -129,16 +132,16 @@ test('should not inject anything if end tag is malformed', t => {
'<!-- ALL-CONTRIBUTORS-LIST:START -->',
'<!-- ALL-CONTRIBUTORS-LIST:EEEEEEEND -->',
'',
'License: MIT'
].join('\n');
'License: MIT',
].join('\n')
const result = generate(options, contributorList, content);
t.is(result, content);
});
const result = generate(options, contributorList, content)
expect(result).toBe(content)
})
test('should inject nothing if there are no contributors', t => {
const {options, content} = fixtures();
const contributorList = [];
test('inject nothing if there are no contributors', () => {
const {options, content} = fixtures()
const contributorList = []
const expected = [
'# project',
'',
@ -149,42 +152,44 @@ test('should inject nothing if there are no contributors', t => {
'<!-- ALL-CONTRIBUTORS-LIST:START -->',
'<!-- ALL-CONTRIBUTORS-LIST:END -->',
'',
'Thanks a lot everyone!'
].join('\n');
'Thanks a lot everyone!',
].join('\n')
const result = generate(options, contributorList, content);
const result = generate(options, contributorList, content)
t.is(result, expected);
});
expect(result).toBe(expected)
})
test('should replace all-contributors badge if present', t => {
const {kentcdodds} = contributors;
const {options} = fixtures();
const contributorList = [kentcdodds];
test('replace all-contributors badge if present', () => {
const {kentcdodds} = contributors
const {options} = fixtures()
const contributorList = [kentcdodds]
const content = [
'# project',
'',
'Badges', [
'Badges',
[
'[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)',
'[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)',
'[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)'
'[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)',
].join(''),
'',
'License: MIT'
].join('\n');
'License: MIT',
].join('\n')
const expected = [
'# project',
'',
'Badges', [
'Badges',
[
'[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)',
'[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)',
'[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)'
'[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)',
].join(''),
'',
'License: MIT'
].join('\n');
'License: MIT',
].join('\n')
const result = generate(options, contributorList, content);
const result = generate(options, contributorList, content)
t.is(result, expected);
});
expect(result).toBe(expected)
})

View file

@ -0,0 +1,10 @@
const _ = require('lodash/fp')
const defaultTemplate =
'[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)'
module.exports = function formatBadge(options, contributors) {
return _.template(options.badgeTemplate || defaultTemplate)({
contributors,
})
}

View file

@ -0,0 +1,43 @@
const _ = require('lodash/fp')
const util = require('../util')
const linkTemplate = _.template(
'[<%= symbol %>](<%= url %> "<%= description %>")',
)
function getType(options, contribution) {
const types = util.contributionTypes(options)
return types[contribution.type || contribution]
}
module.exports = function formatContribution(
options,
contributor,
contribution,
) {
const type = getType(options, contribution)
if (!type) {
throw new Error(
`Unknown contribution type ${contribution} for contributor ${
contributor.login
}`,
)
}
const templateData = {
symbol: type.symbol,
description: type.description,
contributor,
options,
}
let url = `#${contribution}-${contributor.login}`
if (contribution.url) {
url = contribution.url
} else if (type.link) {
url = _.template(type.link)(templateData)
}
return linkTemplate(_.assign({url}, templateData))
}

View file

@ -0,0 +1,46 @@
const _ = require('lodash/fp')
const formatContributionType = require('./format-contribution-type')
const avatarTemplate = _.template(
'<img src="<%= contributor.avatar_url %>" width="<%= options.imageSize %>px;"/>',
)
const avatarBlockTemplate = _.template(
'[<%= avatar %><br /><sub><b><%= name %></b></sub>](<%= contributor.profile %>)',
)
const contributorTemplate = _.template(
'<%= avatarBlock %><br /><%= contributions %>',
)
const defaultImageSize = 100
function defaultTemplate(templateData) {
const avatar = avatarTemplate(templateData)
const avatarBlock = avatarBlockTemplate(
_.assign(
{
name: escapeName(templateData.contributor.name),
avatar,
},
templateData,
),
)
return contributorTemplate(_.assign({avatarBlock}, templateData))
}
function escapeName(name) {
return name.replace(new RegExp('\\|', 'g'), '&#124;')
}
module.exports = function formatContributor(options, contributor) {
const formatter = _.partial(formatContributionType, [options, contributor])
const contributions = contributor.contributions.map(formatter).join(' ')
const templateData = {
contributions,
contributor,
options: _.assign({imageSize: defaultImageSize}, options),
}
const customTemplate =
options.contributorTemplate && _.template(options.contributorTemplate)
return (customTemplate || defaultTemplate)(templateData)
}

88
src/generate/index.js Normal file
View file

@ -0,0 +1,88 @@
const _ = require('lodash/fp')
const injectContentBetween = require('../util').markdown.injectContentBetween
const formatBadge = require('./format-badge')
const formatContributor = require('./format-contributor')
const badgeRegex = /\[!\[All Contributors\]\([a-zA-Z0-9\-./_:?=]+\)\]\(#\w+\)/
function injectListBetweenTags(newContent) {
return function(previousContent) {
const tagToLookFor = '<!-- ALL-CONTRIBUTORS-LIST:'
const closingTag = '-->'
const startOfOpeningTagIndex = previousContent.indexOf(
`${tagToLookFor}START`,
)
const endOfOpeningTagIndex = previousContent.indexOf(
closingTag,
startOfOpeningTagIndex,
)
const startOfClosingTagIndex = previousContent.indexOf(
`${tagToLookFor}END`,
endOfOpeningTagIndex,
)
if (
startOfOpeningTagIndex === -1 ||
endOfOpeningTagIndex === -1 ||
startOfClosingTagIndex === -1
) {
return previousContent
}
return (
previousContent.slice(0, endOfOpeningTagIndex + closingTag.length) +
newContent +
previousContent.slice(startOfClosingTagIndex)
)
}
}
function formatLine(contributors) {
return `| ${contributors.join(' | ')} |`
}
function createColumnLine(options, contributors) {
const nbColumns = Math.min(options.contributorsPerLine, contributors.length)
return `${_.repeat(nbColumns, '| :---: ')}|`
}
function generateContributorsList(options, contributors) {
return _.flow(
_.map(function formatEveryContributor(contributor) {
return formatContributor(options, contributor)
}),
_.chunk(options.contributorsPerLine),
_.map(formatLine),
function insertColumns(lines) {
const columnLine = createColumnLine(options, contributors)
return injectContentBetween(lines, columnLine, 1, 1)
},
_.join('\n'),
newContent => {
return `\n${newContent}\n`
},
)(contributors)
}
function replaceBadge(newContent) {
return function(previousContent) {
const regexResult = badgeRegex.exec(previousContent)
if (!regexResult) {
return previousContent
}
return (
previousContent.slice(0, regexResult.index) +
newContent +
previousContent.slice(regexResult.index + regexResult[0].length)
)
}
}
module.exports = function generate(options, contributors, fileContent) {
const contributorsList =
contributors.length === 0
? '\n'
: generateContributorsList(options, contributors)
const badge = formatBadge(options, contributors)
return _.flow(injectListBetweenTags(contributorsList), replaceBadge(badge))(
fileContent,
)
}

View file

@ -0,0 +1,29 @@
import {addBadge} from '../init-content'
test('insert badge under title', () => {
const content = ['# project', '', 'Description', '', 'Foo bar'].join('\n')
const expected = [
'# project',
'[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)',
'',
'Description',
'',
'Foo bar',
].join('\n')
const result = addBadge(content)
expect(result).toBe(expected)
})
test('add badge if content is empty', () => {
const content = ''
const expected = [
'',
'[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)',
].join('\n')
const result = addBadge(content)
expect(result).toBe(expected)
})

View file

@ -1,15 +1,14 @@
import test from 'ava';
import {addContributorsList} from './init-content';
import {addContributorsList} from '../init-content'
test('should insert list under contributors section', t => {
test('insert list under contributors section', () => {
const content = [
'# project',
'',
'Description',
'',
'## Contributors',
''
].join('\n');
'',
].join('\n')
const expected = [
'# project',
'',
@ -17,20 +16,16 @@ test('should insert list under contributors section', t => {
'',
'## Contributors',
'',
'<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --><!-- ALL-CONTRIBUTORS-LIST:END -->'
].join('\n');
'<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --><!-- ALL-CONTRIBUTORS-LIST:END -->',
].join('\n')
const result = addContributorsList(content);
const result = addContributorsList(content)
t.is(result, expected);
});
expect(result).toBe(expected)
})
test('should create contributors section if it is absent', t => {
const content = [
'# project',
'',
'Description'
].join('\n');
test('create contributors section if it is absent', () => {
const content = ['# project', '', 'Description'].join('\n')
const expected = [
'# project',
'',
@ -41,16 +36,16 @@ test('should create contributors section if it is absent', t => {
'',
'<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --><!-- ALL-CONTRIBUTORS-LIST:END -->',
'',
'This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!'
].join('\n');
'This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!',
].join('\n')
const result = addContributorsList(content);
const result = addContributorsList(content)
t.is(result, expected);
});
expect(result).toBe(expected)
})
test('should create contributors section if content is empty', t => {
const content = '';
test('create contributors section if content is empty', () => {
const content = ''
const expected = [
'',
'## Contributors',
@ -59,10 +54,10 @@ test('should create contributors section if content is empty', t => {
'',
'<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --><!-- ALL-CONTRIBUTORS-LIST:END -->',
'',
'This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!'
].join('\n');
'This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!',
].join('\n')
const result = addContributorsList(content);
const result = addContributorsList(content)
t.is(result, expected);
});
expect(result).toBe(expected)
})

25
src/init/index.js Normal file
View file

@ -0,0 +1,25 @@
const util = require('../util')
const prompt = require('./prompt')
const initContent = require('./init-content')
const configFile = util.configFile
const markdown = util.markdown
function injectInFile(file, fn) {
return markdown.read(file).then(content => markdown.write(file, fn(content)))
}
module.exports = function init() {
return prompt().then(result => {
return configFile
.writeConfig('.all-contributorsrc', result.config)
.then(() =>
injectInFile(result.contributorFile, initContent.addContributorsList),
)
.then(() => {
if (result.badgeFile) {
return injectInFile(result.badgeFile, initContent.addBadge)
}
})
})
}

51
src/init/init-content.js Normal file
View file

@ -0,0 +1,51 @@
const _ = require('lodash/fp')
const injectContentBetween = require('../util').markdown.injectContentBetween
const badgeContent =
'[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)'
const headerContent =
'Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):'
const listContent =
'<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --><!-- ALL-CONTRIBUTORS-LIST:END -->'
const footerContent =
'This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!'
function addBadge(lines) {
return injectContentBetween(lines, badgeContent, 1, 1)
}
function splitAndRejoin(fn) {
return _.flow(_.split('\n'), fn, _.join('\n'))
}
const findContributorsSection = _.findIndex(function isContributorsSection(
str,
) {
return str.toLowerCase().indexOf('# contributors') === 1
})
function addContributorsList(lines) {
const insertionLine = findContributorsSection(lines)
if (insertionLine === -1) {
return lines.concat([
'## Contributors',
'',
headerContent,
'',
listContent,
'',
footerContent,
])
}
return injectContentBetween(
lines,
listContent,
insertionLine + 2,
insertionLine + 2,
)
}
module.exports = {
addBadge: splitAndRejoin(addBadge),
addContributorsList: splitAndRejoin(addContributorsList),
}

80
src/init/prompt.js Normal file
View file

@ -0,0 +1,80 @@
const _ = require('lodash/fp')
const inquirer = require('inquirer')
const git = require('../util').git
const questions = [
{
type: 'input',
name: 'projectName',
message: "What's the name of the repository?",
},
{
type: 'input',
name: 'projectOwner',
message: 'Who is the owner of the repository?',
},
{
type: 'input',
name: 'contributorFile',
message: 'In which file should contributors be listed?',
default: 'README.md',
},
{
type: 'confirm',
name: 'needBadge',
message: 'Do you want a badge tallying the number of contributors?',
},
{
type: 'input',
name: 'badgeFile',
message: 'In which file should the badge be shown?',
when: function(answers) {
return answers.needBadge
},
default: function(answers) {
return answers.contributorFile
},
},
{
type: 'input',
name: 'imageSize',
message: 'How big should the avatars be? (in px)',
filter: parseInt,
default: 100,
},
{
type: 'confirm',
name: 'commit',
message:
'Do you want this badge to auto-commit when contributors are added?',
default: true,
},
]
const uniqueFiles = _.flow(_.compact, _.uniq)
module.exports = function prompt() {
return git
.getRepoInfo()
.then(repoInfo => {
if (repoInfo) {
questions[0].default = repoInfo.projectName
questions[1].default = repoInfo.projectOwner
}
return inquirer.prompt(questions)
})
.then(answers => {
return {
config: {
projectName: answers.projectName,
projectOwner: answers.projectOwner,
files: uniqueFiles([answers.contributorFile, answers.badgeFile]),
imageSize: answers.imageSize,
commit: answers.commit,
contributors: [],
},
contributorFile: answers.contributorFile,
badgeFile: answers.badgeFile,
}
})
}

View file

@ -0,0 +1,48 @@
import nock from 'nock'
import check from '../check'
import allContributorsCliResponse from './fixtures/all-contributors.response.json'
import allContributorsCliTransformed from './fixtures/all-contributors.transformed.json'
import reactNativeResponse1 from './fixtures/react-native.response.1.json'
import reactNativeResponse2 from './fixtures/react-native.response.2.json'
import reactNativeResponse3 from './fixtures/react-native.response.3.json'
import reactNativeResponse4 from './fixtures/react-native.response.4.json'
import reactNativeTransformed from './fixtures/react-native.transformed.json'
beforeAll(() => {
nock('https://api.github.com')
.persist()
.get('/repos/jfmengels/all-contributors-cli/contributors?per_page=100')
.reply(200, allContributorsCliResponse)
.get('/repos/facebook/react-native/contributors?per_page=100')
.reply(200, reactNativeResponse1, {
Link:
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last"',
})
.get('/repositories/29028775/contributors?per_page=100&page=2')
.reply(200, reactNativeResponse2, {
Link:
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="prev"',
})
.get('/repositories/29028775/contributors?per_page=100&page=3')
.reply(200, reactNativeResponse3, {
Link:
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="prev"',
})
.get('/repositories/29028775/contributors?per_page=100&page=4')
.reply(200, reactNativeResponse4, {
Link:
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="prev"',
})
})
test('Handle a single results page correctly', async () => {
const transformed = await check('jfmengels', 'all-contributors-cli')
expect(transformed).toEqual(allContributorsCliTransformed)
})
test('Handle multiple results pages correctly', async () => {
const transformed = await check('facebook', 'react-native')
expect(transformed).toEqual(reactNativeTransformed)
})

View file

@ -0,0 +1,15 @@
import configFile from '../config-file'
const absentFile = './abc'
const expected = `Configuration file not found: ${absentFile}`
test('Reading an absent configuration file throws a helpful error', () => {
expect(() => configFile.readConfig(absentFile)).toThrowError(expected)
})
test('Writing contributors in an absent configuration file throws a helpful error', async () => {
const resolvedError = await configFile
.writeContributors(absentFile, [])
.catch(e => e)
expect(resolvedError.message).toBe(expected)
})

View file

@ -1,4 +1,3 @@
[
{
"login": "jfmengels",
@ -8,14 +7,17 @@
"url": "https://api.github.com/users/jfmengels",
"html_url": "https://github.com/jfmengels",
"followers_url": "https://api.github.com/users/jfmengels/followers",
"following_url": "https://api.github.com/users/jfmengels/following{/other_user}",
"following_url":
"https://api.github.com/users/jfmengels/following{/other_user}",
"gists_url": "https://api.github.com/users/jfmengels/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jfmengels/starred{/owner}{/repo}",
"starred_url":
"https://api.github.com/users/jfmengels/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jfmengels/subscriptions",
"organizations_url": "https://api.github.com/users/jfmengels/orgs",
"repos_url": "https://api.github.com/users/jfmengels/repos",
"events_url": "https://api.github.com/users/jfmengels/events{/privacy}",
"received_events_url": "https://api.github.com/users/jfmengels/received_events",
"received_events_url":
"https://api.github.com/users/jfmengels/received_events",
"type": "User",
"site_admin": false,
"contributions": 74
@ -28,14 +30,17 @@
"url": "https://api.github.com/users/machour",
"html_url": "https://github.com/machour",
"followers_url": "https://api.github.com/users/machour/followers",
"following_url": "https://api.github.com/users/machour/following{/other_user}",
"following_url":
"https://api.github.com/users/machour/following{/other_user}",
"gists_url": "https://api.github.com/users/machour/gists{/gist_id}",
"starred_url": "https://api.github.com/users/machour/starred{/owner}{/repo}",
"starred_url":
"https://api.github.com/users/machour/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/machour/subscriptions",
"organizations_url": "https://api.github.com/users/machour/orgs",
"repos_url": "https://api.github.com/users/machour/repos",
"events_url": "https://api.github.com/users/machour/events{/privacy}",
"received_events_url": "https://api.github.com/users/machour/received_events",
"received_events_url":
"https://api.github.com/users/machour/received_events",
"type": "User",
"site_admin": false,
"contributions": 6
@ -48,14 +53,18 @@
"url": "https://api.github.com/users/chrisinajar",
"html_url": "https://github.com/chrisinajar",
"followers_url": "https://api.github.com/users/chrisinajar/followers",
"following_url": "https://api.github.com/users/chrisinajar/following{/other_user}",
"following_url":
"https://api.github.com/users/chrisinajar/following{/other_user}",
"gists_url": "https://api.github.com/users/chrisinajar/gists{/gist_id}",
"starred_url": "https://api.github.com/users/chrisinajar/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/chrisinajar/subscriptions",
"starred_url":
"https://api.github.com/users/chrisinajar/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/chrisinajar/subscriptions",
"organizations_url": "https://api.github.com/users/chrisinajar/orgs",
"repos_url": "https://api.github.com/users/chrisinajar/repos",
"events_url": "https://api.github.com/users/chrisinajar/events{/privacy}",
"received_events_url": "https://api.github.com/users/chrisinajar/received_events",
"received_events_url":
"https://api.github.com/users/chrisinajar/received_events",
"type": "User",
"site_admin": false,
"contributions": 4
@ -68,14 +77,18 @@
"url": "https://api.github.com/users/alexjoverm",
"html_url": "https://github.com/alexjoverm",
"followers_url": "https://api.github.com/users/alexjoverm/followers",
"following_url": "https://api.github.com/users/alexjoverm/following{/other_user}",
"following_url":
"https://api.github.com/users/alexjoverm/following{/other_user}",
"gists_url": "https://api.github.com/users/alexjoverm/gists{/gist_id}",
"starred_url": "https://api.github.com/users/alexjoverm/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/alexjoverm/subscriptions",
"starred_url":
"https://api.github.com/users/alexjoverm/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/alexjoverm/subscriptions",
"organizations_url": "https://api.github.com/users/alexjoverm/orgs",
"repos_url": "https://api.github.com/users/alexjoverm/repos",
"events_url": "https://api.github.com/users/alexjoverm/events{/privacy}",
"received_events_url": "https://api.github.com/users/alexjoverm/received_events",
"received_events_url":
"https://api.github.com/users/alexjoverm/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
@ -88,14 +101,16 @@
"url": "https://api.github.com/users/ben-eb",
"html_url": "https://github.com/ben-eb",
"followers_url": "https://api.github.com/users/ben-eb/followers",
"following_url": "https://api.github.com/users/ben-eb/following{/other_user}",
"following_url":
"https://api.github.com/users/ben-eb/following{/other_user}",
"gists_url": "https://api.github.com/users/ben-eb/gists{/gist_id}",
"starred_url": "https://api.github.com/users/ben-eb/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/ben-eb/subscriptions",
"organizations_url": "https://api.github.com/users/ben-eb/orgs",
"repos_url": "https://api.github.com/users/ben-eb/repos",
"events_url": "https://api.github.com/users/ben-eb/events{/privacy}",
"received_events_url": "https://api.github.com/users/ben-eb/received_events",
"received_events_url":
"https://api.github.com/users/ben-eb/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
@ -108,14 +123,18 @@
"url": "https://api.github.com/users/kentcdodds",
"html_url": "https://github.com/kentcdodds",
"followers_url": "https://api.github.com/users/kentcdodds/followers",
"following_url": "https://api.github.com/users/kentcdodds/following{/other_user}",
"following_url":
"https://api.github.com/users/kentcdodds/following{/other_user}",
"gists_url": "https://api.github.com/users/kentcdodds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/kentcdodds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/kentcdodds/subscriptions",
"starred_url":
"https://api.github.com/users/kentcdodds/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/kentcdodds/subscriptions",
"organizations_url": "https://api.github.com/users/kentcdodds/orgs",
"repos_url": "https://api.github.com/users/kentcdodds/repos",
"events_url": "https://api.github.com/users/kentcdodds/events{/privacy}",
"received_events_url": "https://api.github.com/users/kentcdodds/received_events",
"received_events_url":
"https://api.github.com/users/kentcdodds/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
@ -128,14 +147,18 @@
"url": "https://api.github.com/users/itaisteinherz",
"html_url": "https://github.com/itaisteinherz",
"followers_url": "https://api.github.com/users/itaisteinherz/followers",
"following_url": "https://api.github.com/users/itaisteinherz/following{/other_user}",
"following_url":
"https://api.github.com/users/itaisteinherz/following{/other_user}",
"gists_url": "https://api.github.com/users/itaisteinherz/gists{/gist_id}",
"starred_url": "https://api.github.com/users/itaisteinherz/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/itaisteinherz/subscriptions",
"starred_url":
"https://api.github.com/users/itaisteinherz/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/itaisteinherz/subscriptions",
"organizations_url": "https://api.github.com/users/itaisteinherz/orgs",
"repos_url": "https://api.github.com/users/itaisteinherz/repos",
"events_url": "https://api.github.com/users/itaisteinherz/events{/privacy}",
"received_events_url": "https://api.github.com/users/itaisteinherz/received_events",
"received_events_url":
"https://api.github.com/users/itaisteinherz/received_events",
"type": "User",
"site_admin": false,
"contributions": 2
@ -148,14 +171,18 @@
"url": "https://api.github.com/users/brycereynolds",
"html_url": "https://github.com/brycereynolds",
"followers_url": "https://api.github.com/users/brycereynolds/followers",
"following_url": "https://api.github.com/users/brycereynolds/following{/other_user}",
"following_url":
"https://api.github.com/users/brycereynolds/following{/other_user}",
"gists_url": "https://api.github.com/users/brycereynolds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/brycereynolds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/brycereynolds/subscriptions",
"starred_url":
"https://api.github.com/users/brycereynolds/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/brycereynolds/subscriptions",
"organizations_url": "https://api.github.com/users/brycereynolds/orgs",
"repos_url": "https://api.github.com/users/brycereynolds/repos",
"events_url": "https://api.github.com/users/brycereynolds/events{/privacy}",
"received_events_url": "https://api.github.com/users/brycereynolds/received_events",
"received_events_url":
"https://api.github.com/users/brycereynolds/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
@ -168,7 +195,8 @@
"url": "https://api.github.com/users/jmeas",
"html_url": "https://github.com/jmeas",
"followers_url": "https://api.github.com/users/jmeas/followers",
"following_url": "https://api.github.com/users/jmeas/following{/other_user}",
"following_url":
"https://api.github.com/users/jmeas/following{/other_user}",
"gists_url": "https://api.github.com/users/jmeas/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jmeas/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jmeas/subscriptions",
@ -188,14 +216,18 @@
"url": "https://api.github.com/users/jerodsanto",
"html_url": "https://github.com/jerodsanto",
"followers_url": "https://api.github.com/users/jerodsanto/followers",
"following_url": "https://api.github.com/users/jerodsanto/following{/other_user}",
"following_url":
"https://api.github.com/users/jerodsanto/following{/other_user}",
"gists_url": "https://api.github.com/users/jerodsanto/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jerodsanto/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jerodsanto/subscriptions",
"starred_url":
"https://api.github.com/users/jerodsanto/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/jerodsanto/subscriptions",
"organizations_url": "https://api.github.com/users/jerodsanto/orgs",
"repos_url": "https://api.github.com/users/jerodsanto/repos",
"events_url": "https://api.github.com/users/jerodsanto/events{/privacy}",
"received_events_url": "https://api.github.com/users/jerodsanto/received_events",
"received_events_url":
"https://api.github.com/users/jerodsanto/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
@ -208,14 +240,18 @@
"url": "https://api.github.com/users/jccguimaraes",
"html_url": "https://github.com/jccguimaraes",
"followers_url": "https://api.github.com/users/jccguimaraes/followers",
"following_url": "https://api.github.com/users/jccguimaraes/following{/other_user}",
"following_url":
"https://api.github.com/users/jccguimaraes/following{/other_user}",
"gists_url": "https://api.github.com/users/jccguimaraes/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jccguimaraes/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jccguimaraes/subscriptions",
"starred_url":
"https://api.github.com/users/jccguimaraes/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/jccguimaraes/subscriptions",
"organizations_url": "https://api.github.com/users/jccguimaraes/orgs",
"repos_url": "https://api.github.com/users/jccguimaraes/repos",
"events_url": "https://api.github.com/users/jccguimaraes/events{/privacy}",
"received_events_url": "https://api.github.com/users/jccguimaraes/received_events",
"received_events_url":
"https://api.github.com/users/jccguimaraes/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
@ -228,14 +264,18 @@
"url": "https://api.github.com/users/kevinjalbert",
"html_url": "https://github.com/kevinjalbert",
"followers_url": "https://api.github.com/users/kevinjalbert/followers",
"following_url": "https://api.github.com/users/kevinjalbert/following{/other_user}",
"following_url":
"https://api.github.com/users/kevinjalbert/following{/other_user}",
"gists_url": "https://api.github.com/users/kevinjalbert/gists{/gist_id}",
"starred_url": "https://api.github.com/users/kevinjalbert/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/kevinjalbert/subscriptions",
"starred_url":
"https://api.github.com/users/kevinjalbert/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/kevinjalbert/subscriptions",
"organizations_url": "https://api.github.com/users/kevinjalbert/orgs",
"repos_url": "https://api.github.com/users/kevinjalbert/repos",
"events_url": "https://api.github.com/users/kevinjalbert/events{/privacy}",
"received_events_url": "https://api.github.com/users/kevinjalbert/received_events",
"received_events_url":
"https://api.github.com/users/kevinjalbert/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
@ -248,14 +288,16 @@
"url": "https://api.github.com/users/revelt",
"html_url": "https://github.com/revelt",
"followers_url": "https://api.github.com/users/revelt/followers",
"following_url": "https://api.github.com/users/revelt/following{/other_user}",
"following_url":
"https://api.github.com/users/revelt/following{/other_user}",
"gists_url": "https://api.github.com/users/revelt/gists{/gist_id}",
"starred_url": "https://api.github.com/users/revelt/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/revelt/subscriptions",
"organizations_url": "https://api.github.com/users/revelt/orgs",
"repos_url": "https://api.github.com/users/revelt/repos",
"events_url": "https://api.github.com/users/revelt/events{/privacy}",
"received_events_url": "https://api.github.com/users/revelt/received_events",
"received_events_url":
"https://api.github.com/users/revelt/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
@ -268,14 +310,18 @@
"url": "https://api.github.com/users/spirosikmd",
"html_url": "https://github.com/spirosikmd",
"followers_url": "https://api.github.com/users/spirosikmd/followers",
"following_url": "https://api.github.com/users/spirosikmd/following{/other_user}",
"following_url":
"https://api.github.com/users/spirosikmd/following{/other_user}",
"gists_url": "https://api.github.com/users/spirosikmd/gists{/gist_id}",
"starred_url": "https://api.github.com/users/spirosikmd/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/spirosikmd/subscriptions",
"starred_url":
"https://api.github.com/users/spirosikmd/starred{/owner}{/repo}",
"subscriptions_url":
"https://api.github.com/users/spirosikmd/subscriptions",
"organizations_url": "https://api.github.com/users/spirosikmd/orgs",
"repos_url": "https://api.github.com/users/spirosikmd/repos",
"events_url": "https://api.github.com/users/spirosikmd/events{/privacy}",
"received_events_url": "https://api.github.com/users/spirosikmd/received_events",
"received_events_url":
"https://api.github.com/users/spirosikmd/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
@ -288,14 +334,16 @@
"url": "https://api.github.com/users/fadc80",
"html_url": "https://github.com/fadc80",
"followers_url": "https://api.github.com/users/fadc80/followers",
"following_url": "https://api.github.com/users/fadc80/following{/other_user}",
"following_url":
"https://api.github.com/users/fadc80/following{/other_user}",
"gists_url": "https://api.github.com/users/fadc80/gists{/gist_id}",
"starred_url": "https://api.github.com/users/fadc80/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/fadc80/subscriptions",
"organizations_url": "https://api.github.com/users/fadc80/orgs",
"repos_url": "https://api.github.com/users/fadc80/repos",
"events_url": "https://api.github.com/users/fadc80/events{/privacy}",
"received_events_url": "https://api.github.com/users/fadc80/received_events",
"received_events_url":
"https://api.github.com/users/fadc80/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
@ -308,7 +356,8 @@
"url": "https://api.github.com/users/snipe",
"html_url": "https://github.com/snipe",
"followers_url": "https://api.github.com/users/snipe/followers",
"following_url": "https://api.github.com/users/snipe/following{/other_user}",
"following_url":
"https://api.github.com/users/snipe/following{/other_user}",
"gists_url": "https://api.github.com/users/snipe/gists{/gist_id}",
"starred_url": "https://api.github.com/users/snipe/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/snipe/subscriptions",

46
src/util/check.js Normal file
View file

@ -0,0 +1,46 @@
const pify = require('pify')
const request = pify(require('request'))
function getNextLink(link) {
if (!link) {
return null
}
const nextLink = link.split(',').find(s => s.includes('rel="next"'))
if (!nextLink) {
return null
}
return nextLink.split(';')[0].slice(1, -1)
}
function getContributorsPage(url) {
return request
.get({
url,
headers: {
'User-Agent': 'request',
},
})
.then(res => {
const body = JSON.parse(res.body)
const contributorsIds = body.map(contributor => contributor.login)
const nextLink = getNextLink(res.headers.link)
if (nextLink) {
return getContributorsPage(nextLink).then(nextContributors => {
return contributorsIds.concat(nextContributors)
})
}
return contributorsIds
})
}
module.exports = function getContributorsFromGithub(owner, name) {
const url = `https://api.github.com/repos/${owner}/${
name
}/contributors?per_page=100`
return getContributorsPage(url)
}

35
src/util/config-file.js Normal file
View file

@ -0,0 +1,35 @@
const fs = require('fs')
const pify = require('pify')
const _ = require('lodash/fp')
function readConfig(configPath) {
try {
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`Configuration file not found: ${configPath}`)
}
throw error
}
}
function writeConfig(configPath, content) {
return pify(fs.writeFile)(configPath, `${JSON.stringify(content, null, 2)}\n`)
}
function writeContributors(configPath, contributors) {
let config
try {
config = readConfig(configPath)
} catch (error) {
return Promise.reject(error)
}
const content = _.assign(config, {contributors})
return writeConfig(configPath, content)
}
module.exports = {
readConfig,
writeConfig,
writeContributors,
}

View file

@ -0,0 +1,97 @@
const _ = require('lodash/fp')
const linkToCommits =
'https://github.com/<%= options.projectOwner %>/<%= options.projectName %>/commits?author=<%= contributor.login %>'
const linkToIssues =
'https://github.com/<%= options.projectOwner %>/<%= options.projectName %>/issues?q=author%3A<%= contributor.login %>'
const defaultTypes = {
blog: {
symbol: '📝',
description: 'Blogposts',
},
bug: {
symbol: '🐛',
description: 'Bug reports',
link: linkToIssues,
},
code: {
symbol: '💻',
description: 'Code',
link: linkToCommits,
},
design: {
symbol: '🎨',
description: 'Design',
},
doc: {
symbol: '📖',
description: 'Documentation',
link: linkToCommits,
},
eventOrganizing: {
symbol: '📋',
description: 'Event Organizing',
},
example: {
symbol: '💡',
description: 'Examples',
},
financial: {
symbol: '💵',
description: 'Financial',
},
fundingFinding: {
symbol: '🔍',
description: 'Funding Finding',
},
ideas: {
symbol: '🤔',
description: 'Ideas, Planning, & Feedback',
},
infra: {
symbol: '🚇',
description: 'Infrastructure (Hosting, Build-Tools, etc)',
},
plugin: {
symbol: '🔌',
description: 'Plugin/utility libraries',
},
question: {
symbol: '💬',
description: 'Answering Questions',
},
review: {
symbol: '👀',
description: 'Reviewed Pull Requests',
},
talk: {
symbol: '📢',
description: 'Talks',
},
test: {
symbol: '⚠️',
description: 'Tests',
link: linkToCommits,
},
tool: {
symbol: '🔧',
description: 'Tools',
},
translation: {
symbol: '🌍',
description: 'Translation',
},
tutorial: {
symbol: '✅',
description: 'Tutorials',
},
video: {
symbol: '📹',
description: 'Videos',
},
}
module.exports = function(options) {
return _.assign(defaultTypes, options.types)
}

60
src/util/git.js Normal file
View file

@ -0,0 +1,60 @@
const path = require('path')
const spawn = require('child_process').spawn
const _ = require('lodash/fp')
const pify = require('pify')
const commitTemplate =
'<%= (newContributor ? "Add" : "Update") %> @<%= username %> as a contributor'
const getRemoteOriginData = pify(cb => {
let output = ''
const git = spawn('git', 'config --get remote.origin.url'.split(' '))
git.stdout.on('data', data => {
output += data
})
git.stderr.on('data', cb)
git.on('close', () => {
cb(null, output)
})
})
function parse(originUrl) {
const result = /:(\w+)\/([A-Za-z0-9-_]+)/.exec(originUrl)
if (!result) {
return null
}
return {
projectOwner: result[1],
projectName: result[2],
}
}
function getRepoInfo() {
return getRemoteOriginData().then(parse)
}
const spawnGitCommand = pify((args, cb) => {
const git = spawn('git', args)
git.stderr.on('data', cb)
git.on('close', cb)
})
function commit(options, data) {
const files = options.files.concat(options.config)
const absolutePathFiles = files.map(file => {
return path.resolve(process.cwd(), file)
})
return spawnGitCommand(['add'].concat(absolutePathFiles)).then(() => {
const commitMessage = _.template(options.commitTemplate || commitTemplate)(
data,
)
return spawnGitCommand(['commit', '-m', commitMessage])
})
}
module.exports = {
commit,
getRepoInfo,
}

View file

@ -1,9 +1,7 @@
'use strict';
module.exports = {
configFile: require('./config-file'),
contributionTypes: require('./contribution-types'),
git: require('./git'),
markdown: require('./markdown'),
check: require('./check')
};
check: require('./check'),
}

20
src/util/markdown.js Normal file
View file

@ -0,0 +1,20 @@
const fs = require('fs')
const pify = require('pify')
function read(filePath) {
return pify(fs.readFile)(filePath, 'utf8')
}
function write(filePath, content) {
return pify(fs.writeFile)(filePath, content)
}
function injectContentBetween(lines, content, startIndex, endIndex) {
return [].concat(lines.slice(0, startIndex), content, lines.slice(endIndex))
}
module.exports = {
read,
write,
injectContentBetween,
}

4621
yarn.lock

File diff suppressed because it is too large Load diff