mirror of
https://github.com/all-contributors/cli.git
synced 2025-01-08 05:06:23 +00:00
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:
parent
3d998c4c7c
commit
482a8ab956
75 changed files with 4502 additions and 7518 deletions
|
@ -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
|
19
.eslintrc
19
.eslintrc
|
@ -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
43
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
14
.gitignore
vendored
|
@ -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
|
||||
|
|
11
.travis.yml
11
.travis.yml
|
@ -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
189
README.md
|
@ -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
155
cli.js
|
@ -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
12
jest.config.js
Normal 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,
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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'}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
};
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
};
|
||||
});
|
||||
};
|
|
@ -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));
|
||||
};
|
|
@ -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
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
|
@ -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));
|
||||
};
|
|
@ -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');
|
||||
});
|
|
@ -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'), '|');
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
|
@ -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 | Needs | 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);
|
||||
});
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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)
|
||||
};
|
|
@ -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
|
||||
};
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
});
|
|
@ -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
|
||||
};
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
87
other/CODE_OF_CONDUCT.md
Normal 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
75
other/MAINTAINING.md
Normal 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
47
other/manual-releases.md
Normal 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
|
57
package.json
57
package.json
|
@ -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
2
prettier.config.js
Normal 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
168
src/cli.js
Executable 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)
|
184
src/contributors/__tests__/add.js
Normal file
184
src/contributors/__tests__/add.js
Normal 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'}],
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
69
src/contributors/__tests__/github.js
Normal file
69
src/contributors/__tests__/github.js
Normal 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
63
src/contributors/add.js
Normal 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)
|
||||
}
|
30
src/contributors/github.js
Normal file
30
src/contributors/github.js
Normal 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
36
src/contributors/index.js
Normal 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,
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
76
src/contributors/prompt.js
Normal file
76
src/contributors/prompt.js
Normal 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))
|
||||
}
|
|
@ -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"]
|
||||
}
|
||||
}
|
26
src/generate/__tests__/format-badge.js
Normal file
26
src/generate/__tests__/format-badge.js
Normal 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',
|
||||
)
|
||||
})
|
155
src/generate/__tests__/format-contribution-type.js
Normal file
155
src/generate/__tests__/format-contribution-type.js
Normal 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')
|
||||
})
|
67
src/generate/__tests__/format-contributor.js
Normal file
67
src/generate/__tests__/format-contributor.js
Normal 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 | Needs | 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)
|
||||
})
|
|
@ -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)
|
||||
})
|
10
src/generate/format-badge.js
Normal file
10
src/generate/format-badge.js
Normal 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,
|
||||
})
|
||||
}
|
43
src/generate/format-contribution-type.js
Normal file
43
src/generate/format-contribution-type.js
Normal 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))
|
||||
}
|
46
src/generate/format-contributor.js
Normal file
46
src/generate/format-contributor.js
Normal 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'), '|')
|
||||
}
|
||||
|
||||
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
88
src/generate/index.js
Normal 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,
|
||||
)
|
||||
}
|
29
src/init/__tests__/add-badge.js
Normal file
29
src/init/__tests__/add-badge.js
Normal 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)
|
||||
})
|
|
@ -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
25
src/init/index.js
Normal 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
51
src/init/init-content.js
Normal 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
80
src/init/prompt.js
Normal 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,
|
||||
}
|
||||
})
|
||||
}
|
48
src/util/__tests__/check.js
Normal file
48
src/util/__tests__/check.js
Normal 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)
|
||||
})
|
15
src/util/__tests__/config-file.js
Normal file
15
src/util/__tests__/config-file.js
Normal 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)
|
||||
})
|
|
@ -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",
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
46
src/util/check.js
Normal file
46
src/util/check.js
Normal 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
35
src/util/config-file.js
Normal 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,
|
||||
}
|
97
src/util/contribution-types.js
Normal file
97
src/util/contribution-types.js
Normal 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
60
src/util/git.js
Normal 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,
|
||||
}
|
|
@ -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
20
src/util/markdown.js
Normal 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,
|
||||
}
|
Loading…
Reference in a new issue