diff --git a/.all-contributorsrc b/.all-contributorsrc index 7e7f36d4..d1527719 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,11 +1,16 @@ { "projectOwner": "all-contributors", - "projectName": "all-contributors-cli", + "projectName": "cli", "imageSize": 100, "repoType": "github", "commit": false, "contributorsPerLine": 6, "linkToUsage": true, + "files": [ + "README.md" + ], + "repoHost": "https://github.com", + "commitConvention": "angular", "contributors": [ { "login": "jfmengels", @@ -254,7 +259,9 @@ "infra", "code", "doc", - "test" + "test", + "review", + "question" ] }, { @@ -265,7 +272,11 @@ "contributions": [ "code", "test", - "doc" + "doc", + "tool", + "maintenance", + "review", + "question" ] }, { @@ -294,10 +305,288 @@ "contributions": [ "code" ] + }, + { + "login": "nschonni", + "name": "Nick Schonning", + "avatar_url": "https://avatars2.githubusercontent.com/u/1297909?v=4", + "profile": "https://github.com/nschonni", + "contributions": [ + "code" + ] + }, + { + "login": "cezaraugusto", + "name": "Cezar Augusto", + "avatar_url": "https://avatars0.githubusercontent.com/u/4672033?v=4", + "profile": "https://cezaraugusto.net/", + "contributions": [ + "doc" + ] + }, + { + "login": "JReinhold", + "name": "Jeppe Reinhold", + "avatar_url": "https://avatars1.githubusercontent.com/u/5678122?v=4", + "profile": "https://reinhold.is", + "contributions": [ + "code" + ] + }, + { + "login": "rachelcarmena", + "name": "Rachel M. Carmena", + "avatar_url": "https://avatars0.githubusercontent.com/u/22792183?v=4", + "profile": "https://rachelcarmena.github.io", + "contributions": [ + "code" + ] + }, + { + "login": "simon300000", + "name": "simon3000", + "avatar_url": "https://avatars1.githubusercontent.com/u/12656264?v=4", + "profile": "https://github.com/simon300000", + "contributions": [ + "test" + ] + }, + { + "login": "SnO2WMaN", + "name": "SnO₂WMaN", + "avatar_url": "https://avatars3.githubusercontent.com/u/15155608?v=4", + "profile": "https://sno2wman.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "dexpota", + "name": "Fabrizio", + "avatar_url": "https://avatars1.githubusercontent.com/u/7031675?v=4", + "profile": "https://www.destro.me", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "kharaone", + "name": "kharaone", + "avatar_url": "https://avatars1.githubusercontent.com/u/6599271?v=4", + "profile": "https://github.com/kharaone", + "contributions": [ + "code" + ] + }, + { + "login": "MarceloAlves", + "name": "Marcelo Alves", + "avatar_url": "https://avatars1.githubusercontent.com/u/216782?v=4", + "profile": "https://github.com/marceloalves", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "AnandChowdhary", + "name": "Anand Chowdhary", + "avatar_url": "https://avatars3.githubusercontent.com/u/2841780?v=4", + "profile": "https://anandchowdhary.com/?utm_source=github&utm_campaign=about-link", + "contributions": [ + "test", + "bug", + "code" + ] + }, + { + "login": "phacks", + "name": "Nicolas Goutay", + "avatar_url": "https://avatars1.githubusercontent.com/u/2587348?v=4", + "profile": "https://phacks.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "tylerkrupicka", + "name": "Tyler Krupicka", + "avatar_url": "https://avatars1.githubusercontent.com/u/5761061?s=460&v=4", + "profile": "https://github.com/tylerkrupicka", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "smoia", + "name": "Stefano Moia", + "avatar_url": "https://avatars3.githubusercontent.com/u/35300580?v=4", + "profile": "https://github.com/smoia", + "contributions": [ + "code" + ] + }, + { + "login": "ilai-deutel", + "name": "Ilaï Deutel", + "avatar_url": "https://avatars0.githubusercontent.com/u/10098207?v=4", + "profile": "https://github.com/ilai-deutel", + "contributions": [ + "platform" + ] + }, + { + "login": "jdalrymple", + "name": "Justin Dalrymple", + "avatar_url": "https://avatars3.githubusercontent.com/u/3743662?v=4", + "profile": "https://github.com/jdalrymple", + "contributions": [ + "code" + ] + }, + { + "login": "k3nsei", + "name": "Piotr Stępniewski", + "avatar_url": "https://avatars2.githubusercontent.com/u/190422?v=4", + "profile": "https://github.com/k3nsei", + "contributions": [ + "bug", + "code", + "test" + ] + }, + { + "login": "gr2m", + "name": "Gregor Martynus", + "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", + "profile": "https://dev.to/gr2m", + "contributions": [ + "review", + "question" + ] + }, + { + "login": "sinchang", + "name": "Jeff Wen", + "avatar_url": "https://avatars0.githubusercontent.com/u/3297859?v=4", + "profile": "https://sinchang.me/", + "contributions": [ + "review" + ] + }, + { + "login": "pavelloz", + "name": "Paweł Kowalski", + "avatar_url": "https://avatars1.githubusercontent.com/u/546845?v=4", + "profile": "https://github.com/pavelloz", + "contributions": [ + "code" + ] + }, + { + "login": "mloning", + "name": "Markus Löning", + "avatar_url": "https://avatars3.githubusercontent.com/u/21020482?v=4", + "profile": "https://www.linkedin.com/in/mloning/", + "contributions": [ + "code" + ] + }, + { + "login": "DavidAnson", + "name": "David Anson", + "avatar_url": "https://avatars1.githubusercontent.com/u/1828270?v=4", + "profile": "https://dlaa.me/", + "contributions": [ + "bug" + ] + }, + { + "login": "Favna", + "name": "Jeroen Claassens", + "avatar_url": "https://avatars3.githubusercontent.com/u/4019718?v=4", + "profile": "https://favware.tech/", + "contributions": [ + "code" + ] + }, + { + "login": "melink14", + "name": "Erek Speed", + "avatar_url": "https://avatars3.githubusercontent.com/u/1176550?v=4", + "profile": "https://erekspeed.com", + "contributions": [ + "code" + ] + }, + { + "login": "shairez", + "name": "Shai Reznik", + "avatar_url": "https://avatars1.githubusercontent.com/u/1430726?v=4", + "profile": "http://www.hirez.io", + "contributions": [ + "bug", + "code", + "test" + ] + }, + { + "login": "darekkay", + "name": "Darek Kay", + "avatar_url": "https://avatars0.githubusercontent.com/u/3101914?v=4", + "profile": "https://darekkay.com", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "LaChapeliere", + "name": "LaChapeliere", + "avatar_url": "https://avatars2.githubusercontent.com/u/7062546?v=4", + "profile": "https://github.com/LaChapeliere", + "contributions": [ + "code" + ] + }, + { + "login": "SirWindfield", + "name": "SirWindfield", + "avatar_url": "https://avatars.githubusercontent.com/u/5113257?v=4", + "profile": "https://github.com/SirWindfield", + "contributions": [ + "code" + ] + }, + { + "login": "vapurrmaid", + "name": "G r e y", + "avatar_url": "https://avatars.githubusercontent.com/u/11184711?v=4", + "profile": "https://vapurrmaid.ca", + "contributions": [ + "security" + ] + }, + { + "login": "Lucas-C", + "name": "Lucas Cimon", + "avatar_url": "https://avatars.githubusercontent.com/u/925560?v=4", + "profile": "https://chezsoi.org/lucas/blog/", + "contributions": [ + "doc" + ] + }, + { + "login": "JoshuaKGoldberg", + "name": "Josh Goldberg", + "avatar_url": "https://avatars.githubusercontent.com/u/3335181?v=4", + "profile": "http://www.joshuakgoldberg.com", + "contributions": [ + "bug" + ] } ], - "files": [ - "README.md" - ], - "repoHost": "https://github.com" + "skipCi": true } diff --git a/.circleci/config.yml b/.circleci/config.yml index 58dae44b..ceb88c74 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 docker_defaults: &docker_defaults docker: - - image: circleci/node:8.14.0 + - image: cimg/node:16.17.0 commands: prep_env: @@ -12,13 +12,15 @@ commands: path: ~/repo - restore_cache: name: Restore node_modules cache - key: all-contributors-cli-v2-{{ checksum "package.json" }}-{{ .Branch }} + key: + all-contributors-cli-v2-{{ checksum "package.json" }}-{{ .Branch }} save_env_cache: description: Saves environment cache steps: - save_cache: name: Save node_modules cache - key: all-contributors-cli-v2-{{ checksum "package.json" }}-{{ .Branch }} + key: + all-contributors-cli-v2-{{ checksum "package.json" }}-{{ .Branch }} paths: - node_modules/ diff --git a/.gitignore b/.gitignore index ab4f4cb3..22b6454c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ package-lock.json yarn.lock .vscode cache +.idea diff --git a/.nvmrc b/.nvmrc index 2a5dd0d6..2a4e4ab8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8.14.0 +16.17.0 diff --git a/.yvmrc b/.yvmrc index 8fdcf386..28449774 100644 --- a/.yvmrc +++ b/.yvmrc @@ -1 +1 @@ -1.9.2 +1.21.1 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..93a7c2f9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +MIT License (MIT) Copyright (c) 2016 Kent C. Dodds, 2019 Jake Bolam, 2020 +Maximilian Berkmann + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 4d7ae894..d5c2e0fd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,19 @@ -> [There is now a GitHub Bot](https://github.com/all-contributors/all-contributors-bot) for automating the maintenance of your contributors table ✨
Say goodbye to command line tool dependencies and hello to the [@all-contributors bot 🤖](https://github.com/all-contributors/all-contributors-bot) + + + +- [ all-contributors-cli ](#all-contributors-cli) + - [The problem](#the-problem) + - [This solution](#this-solution) + - [Using the all-contributors-cli](#using-the-all-contributors-cli) + - [Contributors ✨](#contributors-) + - [LICENSE](#license) + + + +> [There is now a GitHub Bot](https://github.com/all-contributors/all-contributors-bot) +> for automating the maintenance of your contributors table ✨
Say goodbye +> to command line tool dependencies and hello to the +> [@all-contributors bot 🤖](https://github.com/all-contributors/all-contributors-bot)

all-contributors-cli @@ -28,17 +43,19 @@ -[![Build Status](https://img.shields.io/circleci/project/all-contributors/all-contributors-cli/master.svg)](https://circleci.com/gh/all-contributors/workflows/all-contributors-cli/tree/master) -[![Code Coverage](https://img.shields.io/codecov/c/github/all-contributors/all-contributors-cli.svg)](https://codecov.io/github/all-contributors/all-contributors-cli) +[![Build Status](https://dl.circleci.com/status-badge/img/gh/all-contributors/cli/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/all-contributors/cli/tree/master) +[![Code Coverage](https://codecov.io/gh/all-contributors/cli/branch/master/graph/badge.svg?token=jHIrCqevli)](https://codecov.io/gh/all-contributors/cli) [![Version](https://img.shields.io/npm/v/all-contributors-cli.svg)](https://www.npmjs.com/package/all-contributors-cli) [![Downloads](https://img.shields.io/npm/dm/all-contributors-cli.svg)](http://www.npmtrends.com/all-contributors-cli) -[![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors) +[![AUR Version](https://img.shields.io/aur/version/all-contributors-cli.svg)](https://aur.archlinux.org/packages/all-contributors-cli) +[![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) [![Star on Github](https://img.shields.io/github/stars/all-contributors/all-contributors-cli.svg?style=social)](https://github.com/all-contributors/all-contributors-cli/stargazers) ## The problem -You want to implement the [All Contributors][all-contributors] spec, but don't -want to maintain the table by hand +You want to implement the +[All Contributors](https://github.com/all-contributors/all-contributors) spec, +but don't want to maintain the table by hand ## This solution @@ -48,9 +65,12 @@ specification for your GitHub or GitLab repository. ## Using the all-contributors-cli -If you're looking to use the cli, head over to [the cli docs on allcontributors.org](https://allcontributors.org/docs/en/cli/overview). The all-contributors website contains all the information required to install, configure and use the all-contributors-cli. +If you're looking to use the cli, head over to +[the cli docs on allcontributors.org](https://allcontributors.org/docs/en/cli/overview). +The all-contributors website contains all the information required to install, +configure and use the all-contributors-cli. -## Contributors +## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/other/MAINTAINING.md b/other/MAINTAINING.md index 5232261b..a31d2adf 100644 --- a/other/MAINTAINING.md +++ b/other/MAINTAINING.md @@ -41,8 +41,8 @@ as you want/need to. Nobody can ask any more of you than that. 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. +When we receive a pull request, a Circle CI build is kicked off automatically (see the `.circleci/` +directory for what runs in the CI pipeline). We avoid merging anything that breaks the CI pipeline. 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 @@ -56,8 +56,8 @@ 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 +Our releases are automatic. They happen whenever code lands into `master`. A Circle CI build +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 diff --git a/package.json b/package.json index 6094ff1f..5ff44f18 100644 --- a/package.json +++ b/package.json @@ -42,24 +42,24 @@ }, "homepage": "https://github.com/all-contributors/all-contributors-cli#readme", "dependencies": { - "@babel/runtime": "^7.2.0", - "async": "^2.0.0-rc.1", - "chalk": "^2.3.0", + "@babel/runtime": "^7.7.6", + "async": "^3.1.0", + "chalk": "^4.0.0", "didyoumean": "^1.2.1", - "inquirer": "^6.2.1", - "json-fixer": "^1.3.1-0", + "inquirer": "^7.3.3", + "json-fixer": "^1.6.8", "lodash": "^4.11.2", - "pify": "^4.0.1", - "request": "^2.72.0", - "yargs": "^13.1.0" + "node-fetch": "^2.6.0", + "pify": "^5.0.0", + "yargs": "^15.0.1" }, "devDependencies": { - "codecov": "^3.1.0", - "cz-conventional-changelog": "^2.1.0", - "git-cz": "^3.0.0", - "kcd-scripts": "^1.0.0", - "nock": "^10.0.6", - "semantic-release": "^15.13.2" + "codecov": "^3.8.1", + "cz-conventional-changelog": "^3.3.0", + "git-cz": "^4.7.6", + "kcd-scripts": "^6.2.0", + "nock": "^12.0.0", + "semantic-release": "^17.0.8" }, "eslintIgnore": [ "node_modules", diff --git a/src/cli.js b/src/cli.js index 1ed3a394..1c52e969 100755 --- a/src/cli.js +++ b/src/cli.js @@ -5,10 +5,6 @@ const path = require('path') const yargs = require('yargs') const chalk = require('chalk') const inquirer = require('inquirer') -const didYouMean = require('didyoumean') - -// Setting edit length to be 60% of the input string's length -didYouMean.threshold = 0.6 const init = require('./init') const generate = require('./generate') @@ -20,24 +16,27 @@ const cwd = process.cwd() const defaultRCFile = path.join(cwd, '.all-contributorsrc') const yargv = yargs + .scriptName('all-contributors') .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 ') - .command('init', 'Prepare the project to be used with this tool') - .usage('Usage: $0 init') + .recommendCommands() + .command('generate', `Generate the list of contributors\n\nUSAGE: all-contributors generate`) + .command('add', `Add a new contributor\n\nUSAGE: all-contributors add `) + .command('init', `Prepare the project to be used with this tool\n\nUSAGE: all-contributors init`) .command( 'check', - 'Compares contributors from the repository with the ones credited in .all-contributorsrc', - ) - .usage('Usage: $0 check') + `Compare contributors from the repository with the ones credited in .all-contributorsrc'\n\nUSAGE: all-contributors check`) .boolean('commit') .default('files', ['README.md']) .default('contributorsPerLine', 7) + .option('contributorsSortAlphabetically', { + type: 'boolean', + default: false, + description: + 'Sort the list of contributors alphabetically in the generated list', + }) .default('contributors', []) .default('config', defaultRCFile) .config('config', configPath => { @@ -50,15 +49,6 @@ const yargv = yargs } }).argv -function suggestCommands(cmd) { - const availableCommands = ['generate', 'add', 'init', 'check'] - const suggestion = didYouMean(cmd, availableCommands) - - if (suggestion) { - console.log(chalk.bold(`Did you mean ${suggestion}`)) - } -} - function startGeneration(argv) { return Promise.all( argv.files.map(file => { @@ -72,7 +62,8 @@ function startGeneration(argv) { } function addContribution(argv) { - const username = argv._[1] + util.configFile.readConfig(argv.config) // ensure the config file exists + const username = argv._[1] === undefined ? undefined : String(argv._[1]) const contributions = argv._[2] // Add or update contributor in the config file return updateContributors(argv, username, contributions).then(data => { @@ -134,7 +125,7 @@ function checkContributors(argv) { function onError(error) { if (error) { - console.error(error.message) + console.error(error.stack || error.message || error) process.exit(1) } process.exit(0) @@ -183,7 +174,6 @@ promptForCommand(yargv) case 'check': return checkContributors(yargv) default: - suggestCommands(command) throw new Error(`Unknown command ${command}`) } }) diff --git a/src/generate/__tests__/__snapshots__/index.js.snap b/src/generate/__tests__/__snapshots__/index.js.snap index 860d71f5..15d4d74b 100644 --- a/src/generate/__tests__/__snapshots__/index.js.snap +++ b/src/generate/__tests__/__snapshots__/index.js.snap @@ -8,8 +8,20 @@ Description ## Contributors These people contributed to the project: - -
Kent C. Dodds is awesome!Divjot Singh is awesome!Jeroen Engels is awesome!
+ + + + + + + + + + +
Kent C. Dodds is awesome!Divjot Singh is awesome!Jeroen Engels is awesome!
+ + + @@ -24,8 +36,26 @@ Description ## Contributors These people contributed to the project: - -
Kent C. Dodds is awesome!Kent C. Dodds is awesome!Kent C. Dodds is awesome!Kent C. Dodds is awesome!Kent C. Dodds is awesome!
Kent C. Dodds is awesome!Kent C. Dodds is awesome!
+ + + + + + + + + + + + + + + + +
Kent C. Dodds is awesome!Kent C. Dodds is awesome!Kent C. Dodds is awesome!Kent C. Dodds is awesome!Kent C. Dodds is awesome!
Kent C. Dodds is awesome!Kent C. Dodds is awesome!
+ + + diff --git a/src/generate/__tests__/format-badge.js b/src/generate/__tests__/format-badge.js index 3bcb15e0..1f13e65a 100644 --- a/src/generate/__tests__/format-badge.js +++ b/src/generate/__tests__/format-badge.js @@ -4,9 +4,9 @@ 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)' + '[![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)' + '[![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) diff --git a/src/generate/__tests__/format-contribution-type.js b/src/generate/__tests__/format-contribution-type.js index a7317af4..11bff893 100644 --- a/src/generate/__tests__/format-contribution-type.js +++ b/src/generate/__tests__/format-contribution-type.js @@ -50,6 +50,15 @@ test('return link to issues', () => { expect(formatContributionType(options, contributor, 'bug')).toBe(expected) }) +test('return link to reviews', () => { + const contributor = contributors.kentcdodds + const {options} = fixtures() + const expected = + '👀' + + expect(formatContributionType(options, contributor, 'review')).toBe(expected) +}) + test('make any symbol into a link if contribution is an object', () => { const contributor = contributors.kentcdodds const {options} = fixtures() diff --git a/src/generate/__tests__/format-contributor.js b/src/generate/__tests__/format-contributor.js index b8feece1..bb3451ec 100644 --- a/src/generate/__tests__/format-contributor.js +++ b/src/generate/__tests__/format-contributor.js @@ -20,7 +20,7 @@ test('format a simple contributor', () => { const {options} = fixtures() const expected = - 'Kent C. Dodds
Kent C. Dodds

👀' + '
Kent C. Dodds

👀' expect(formatContributor(options, contributor)).toBe(expected) }) @@ -30,7 +30,7 @@ test('format contributor with complex contribution types', () => { const {options} = fixtures() const expected = - 'Kent C. Dodds
Kent C. Dodds

📖 👀 💬' + '
Kent C. Dodds

📖 👀 💬' expect(formatContributor(options, contributor)).toBe(expected) }) @@ -53,7 +53,7 @@ test('default image size to 100', () => { delete options.imageSize const expected = - 'Kent C. Dodds
Kent C. Dodds

👀' + '
Kent C. Dodds

👀' expect(formatContributor(options, contributor)).toBe(expected) }) @@ -63,17 +63,17 @@ test('format contributor with pipes in their name', () => { const {options} = fixtures() const expected = - 'Who | Needs | Pipes?
Who | Needs | Pipes?

📖' + '
Who | Needs | Pipes?

📖' expect(formatContributor(options, contributor)).toBe(expected) }) -test('format contributor with no github account', () => { +test('format contributor with no GitHub account', () => { const contributor = contributors.nologin const {options} = fixtures() const expected = - 'No Github Account
No Github Account
🌍' + '
No Github Account
🌍' expect(formatContributor(options, contributor)).toBe(expected) }) diff --git a/src/generate/__tests__/index.js b/src/generate/__tests__/index.js index abe5f047..ece35d58 100644 --- a/src/generate/__tests__/index.js +++ b/src/generate/__tests__/index.js @@ -60,6 +60,26 @@ test('split contributors into multiples lines when there are too many', () => { expect(result).toMatchSnapshot() }) +test('sorts the list of contributors if contributorsSortAlphabetically=true', () => { + const {kentcdodds, bogas04} = contributors + const {options, jfmengels, content} = fixtures() + + const resultPreSorted = generate( + options, + [bogas04, jfmengels, kentcdodds], + content, + ) + + options.contributorsSortAlphabetically = true + const resultAutoSorted = generate( + options, + [jfmengels, kentcdodds, bogas04], + content, + ) + + expect(resultPreSorted).toEqual(resultAutoSorted) +}) + test('not inject anything if there is no tags to inject content in', () => { const {kentcdodds} = contributors const {options} = fixtures() @@ -119,7 +139,11 @@ test('inject nothing if there are no contributors', () => { '## Contributors', 'These people contributed to the project:', '', - '', + '', + '', + '', + '', + '', '', '', 'Thanks a lot everyone!', @@ -140,7 +164,9 @@ test('replace all-contributors badge if present', () => { '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)', + '\n', + '[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-)\n', + '\n', '[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)', ].join(''), '', @@ -152,7 +178,9 @@ test('replace all-contributors badge if present', () => { '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)', + '\n', + '[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)\n', + '\n', '[![version](https://img.shields.io/npm/v/all-contributors-cli.svg?style=flat-square)](http://npm.im/all-contributors-cli)', ].join(''), '', diff --git a/src/generate/format-badge.js b/src/generate/format-badge.js index b349cfbf..f03730ca 100644 --- a/src/generate/format-badge.js +++ b/src/generate/format-badge.js @@ -1,7 +1,7 @@ const _ = require('lodash/fp') const defaultTemplate = - '[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)' + '[![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)({ diff --git a/src/generate/format-contributor.js b/src/generate/format-contributor.js index 67b55eea..048d63db 100644 --- a/src/generate/format-contributor.js +++ b/src/generate/format-contributor.js @@ -2,7 +2,7 @@ const _ = require('lodash/fp') const formatContributionType = require('./format-contribution-type') const avatarTemplate = _.template( - '<%= name %>', + '', ) const avatarBlockTemplate = _.template( '<%= avatar %>
<%= name %>
', diff --git a/src/generate/index.js b/src/generate/index.js index 33592361..49cafde7 100644 --- a/src/generate/index.js +++ b/src/generate/index.js @@ -2,11 +2,9 @@ const _ = require('lodash/fp') 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 = '' const startOfOpeningTagIndex = previousContent.indexOf( `${tagToLookFor}START`, @@ -28,8 +26,12 @@ function injectListBetweenTags(newContent) { } return [ previousContent.slice(0, endOfOpeningTagIndex + closingTag.length), - '\n', + '\n', + '\n', newContent, + '', + '\n', + '\n\n', previousContent.slice(startOfClosingTagIndex), ].join('') } @@ -57,12 +59,17 @@ function generateContributorsList(options, contributors) { const tableFooter = formatFooter(options) return _.flow( + _.sortBy(contributor => { + if (options.contributorsSortAlphabetically) { + return contributor.name + } + }), _.map(function formatEveryContributor(contributor) { return formatContributor(options, contributor) }), - _.chunk(options.contributorsPerLine), + _.chunk(contributorsPerLine), _.map(formatLine), - _.join(''), + _.join('\n \n \n '), newContent => { return `\n${newContent}\n${tableFooter}\n
\n` }, @@ -70,16 +77,34 @@ function generateContributorsList(options, contributors) { } function replaceBadge(newContent) { - return function(previousContent) { - const regexResult = badgeRegex.exec(previousContent) - if (!regexResult) { + return function (previousContent) { + const tagToLookFor = `' + 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, regexResult.index) + - newContent + - previousContent.slice(regexResult.index + regexResult[0].length) - ) + return [ + previousContent.slice(0, endOfOpeningTagIndex + closingTag.length), + '\n', + newContent, + '\n', + previousContent.slice(startOfClosingTagIndex), + ].join('') } } diff --git a/src/init/__tests__/__snapshots__/add-contributors-list.js.snap b/src/init/__tests__/__snapshots__/add-contributors-list.js.snap index 3877768c..c528a5a1 100644 --- a/src/init/__tests__/__snapshots__/add-contributors-list.js.snap +++ b/src/init/__tests__/__snapshots__/add-contributors-list.js.snap @@ -7,7 +7,10 @@ exports[`create contributors section if content is empty 1`] = ` Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - + + + + This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!" @@ -22,7 +25,10 @@ Description Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - + + + + This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!" @@ -36,6 +42,9 @@ Description ## Contributors - + + + + " `; diff --git a/src/init/__tests__/add-badge.js b/src/init/__tests__/add-badge.js index cbcb4ab0..78b6d0d4 100644 --- a/src/init/__tests__/add-badge.js +++ b/src/init/__tests__/add-badge.js @@ -4,7 +4,9 @@ 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)', + '', + '[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-)', + '', '', 'Description', '', @@ -20,7 +22,9 @@ 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)', + '', + '[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-)', + '', ].join('\n') const result = addBadge(content) diff --git a/src/init/__tests__/add-contributors-list.js b/src/init/__tests__/add-contributors-list.js index cb690ce5..bb532338 100644 --- a/src/init/__tests__/add-contributors-list.js +++ b/src/init/__tests__/add-contributors-list.js @@ -30,20 +30,24 @@ test('create contributors section if content is empty', () => { expect(result).toMatchSnapshot() }) -test('README exists', done => { - const file = 'README.md' - ensureFileExists(file) - .then(data => expect(data).toStrictEqual(file)) - .then(_ => done()) +test('README exists', () => { + return new Promise(done => { + const file = 'README.md' + ensureFileExists(file) + .then(data => expect(data).toStrictEqual(file)) + .then(_ => done()) + }) }) -test("LOREM doesn't exists", done => { - const file = 'LOREM.md' - ensureFileExists(file).then(data => { - expect(data).toStrictEqual(file) - return unlink(file, err => { - if (err) throw err - done() +test("LOREM doesn't exists", () => { + return new Promise(done => { + const file = 'LOREM.md' + ensureFileExists(file).then(data => { + expect(data).toStrictEqual(file) + return unlink(file, err => { + if (err) throw err + done() + }) }) }) }) diff --git a/src/init/commit-conventions.js b/src/init/commit-conventions.js index 5600533a..96d631b2 100644 --- a/src/init/commit-conventions.js +++ b/src/init/commit-conventions.js @@ -14,6 +14,10 @@ const conventions = { name: 'Atom', msg: ':memo:', }, + gitmoji: { + name: 'Gitmoji', + msg: ':busts_in_silhouette:', + }, ember: { name: 'Ember', msg: '[DOC canary]', diff --git a/src/init/init-content.js b/src/init/init-content.js index 20f08ebb..7e75426c 100644 --- a/src/init/init-content.js +++ b/src/init/init-content.js @@ -1,13 +1,20 @@ 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 badgeContent = [ + '', + '[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-)', + '', +].join('\n') + const headerContent = 'Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):' const listContent = [ '', - '', + '', + '', + '', + '', '', ].join('\n') const footerContent = @@ -18,7 +25,11 @@ function addBadge(lines) { } function splitAndRejoin(fn) { - return _.flow(_.split('\n'), fn, _.join('\n')) + return _.flow( + _.split('\n'), + fn, + _.join('\n'), + ) } const findContributorsSection = _.findIndex(function isContributorsSection( @@ -43,8 +54,8 @@ function addContributorsList(lines) { return injectContentBetween( lines, listContent, - insertionLine + 2, - insertionLine + 2, + insertionLine + 3, + insertionLine + 3, ) } diff --git a/src/repo/__tests__/github.js b/src/repo/__tests__/github.js index 478d50aa..ad9de9c7 100644 --- a/src/repo/__tests__/github.js +++ b/src/repo/__tests__/github.js @@ -58,9 +58,7 @@ async function rejects(promise) { } test('handle errors', async () => { - nock('https://api.github.com') - .get('/users/nodisplayname') - .replyWithError(404) + nock('https://api.github.com').get('/users/nodisplayname').replyWithError(404) await rejects(getUserInfo('nodisplayname')) }) @@ -73,43 +71,49 @@ test('Throw error when no username is provided', () => { test('Throw error when non existent username is provided', async () => { const username = 'thisusernamedoesntexist' - nock('https://api.github.com') + nock('https://api.github.com').get(`/users/${username}`).reply(404, { + message: 'Not Found', + documentation_url: + 'https://developer.github.com/v3/users/#get-a-single-user', + }) + await expect(getUserInfo(username)).rejects.toThrow( + `The username ${username} doesn't exist on GitHub.`, + ) +}) + +test('Throw error when missing enterprise authentication', async () => { + const username = 'notauthenticated' + nock('http://github.myhost.com:3000/api/v3') .get(`/users/${username}`) - .reply(404, { - message: 'Not Found', - documentation_url: - 'https://developer.github.com/v3/users/#get-a-single-user', + .reply(401, { + message: 'Must authenticate to access this API.', + documentation_url: 'https://developer.github.com/enterprise/2.17/v3', }) - try { - await getUserInfo(username) - } catch (error) { - expect(error.message).toEqual( - `Login not found when adding a contributor for username - ${username}.`, - ) - } + await expect( + getUserInfo(username, 'http://github.myhost.com:3000'), + ).rejects.toThrow( + `Missing authentication for GitHub API. Did you set PRIVATE_TOKEN?`, + ) }) -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', - }) +test('handle API rate GitHub errors', async () => { + const githubErrorMessage = + "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." + nock('https://api.github.com').get('/users/nodisplayname').reply(200, { + message: githubErrorMessage, + documentation_url: 'https://developer.github.com/v3/#rate-limiting', + }) - await rejects(getUserInfo('nodisplayname')) + await expect(getUserInfo('nodisplayname')).rejects.toThrow(githubErrorMessage) }) 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', - }) + 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') @@ -150,35 +154,31 @@ test('attaches no token when not supplied', async () => { }) 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', - }) + 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', - }) + 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') }) -test('retrieve user from a different github registry', async () => { - nock('http://api.github.myhost.com:3000') +test('retrieve user from a different GitHub registry', async () => { + nock('http://github.myhost.com:3000/api/v3') .get('/users/nodisplayname') .reply(200, { login: 'nodisplayname', diff --git a/src/repo/github.js b/src/repo/github.js index b3069650..15261e63 100644 --- a/src/repo/github.js +++ b/src/repo/github.js @@ -1,16 +1,37 @@ -const pify = require('pify') -const request = pify(require('request')) +const url = require('url') +const fetch = require('node-fetch') +const {parseHttpUrl, isValidHttpUrl} = require('../util/url') + +/** + * Get the host based on public or enterprise GitHub. + * https://developer.github.com/enterprise/2.17/v3/#current-version + * + * @param {String} hostname - Hostname from config. + * @returns {String} - Host for GitHub API. + */ +function getApiHost(hostname) { + if (!hostname) { + hostname = 'https://github.com' + } + + if (hostname !== 'https://github.com') { + // Assume Github Enterprise + return url.resolve(hostname, '/api/v3') + } + + return hostname.replace(/:\/\//, '://api.') +} -function getRequestHeaders(optionalPrivateToken = '') { - const requestHeaders = { - 'User-Agent': 'request', +function getFetchHeaders(optionalPrivateToken = '') { + const fetchHeaders = { + 'User-Agent': 'node-fetch', } if (optionalPrivateToken && optionalPrivateToken.length > 0) { - requestHeaders.Authorization = `token ${optionalPrivateToken}` + fetchHeaders.Authorization = `token ${optionalPrivateToken}` } - return requestHeaders + return fetchHeaders } function getNextLink(link) { @@ -24,67 +45,71 @@ function getNextLink(link) { return null } - return nextLink.split(';')[0].slice(1, -1) + return nextLink.split(';')[0].trim().slice(1, -1) } -function getContributorsPage(url, optionalPrivateToken) { - return request - .get({ - url, - headers: getRequestHeaders(optionalPrivateToken), - }) - .then(res => { - const body = JSON.parse(res.body) - if (res.statusCode >= 400) { - if (res.statusCode === 404) { - throw new Error('No contributors found on the GitHub repository') - } +function getContributorsPage(githubUrl, optionalPrivateToken) { + return fetch(githubUrl, { + headers: getFetchHeaders(optionalPrivateToken), + }).then(res => { + if (res.status === 404 || res.status >= 500) { + throw new Error('No contributors found on the GitHub repository') + } + + return res.json().then(body => { + if (res.status >= 400 || !res.ok) { throw new Error(body.message) } const contributorsIds = body.map(contributor => contributor.login) - const nextLink = getNextLink(res.headers.link) + const nextLink = getNextLink(res.headers.get('link')) if (nextLink) { - return getContributorsPage(nextLink).then(nextContributors => { - return contributorsIds.concat(nextContributors) - }) + return getContributorsPage(nextLink, optionalPrivateToken).then( + nextContributors => { + return contributorsIds.concat(nextContributors) + }, + ) } return contributorsIds }) + }) } -const getUserInfo = function(username, hostname, optionalPrivateToken) { - /* eslint-disable complexity */ - if (!hostname) { - hostname = 'https://github.com' - } - +const getUserInfo = function (username, hostname, optionalPrivateToken) { if (!username) { throw new Error( `No login when adding a contributor. Please specify a username.`, ) } - const root = hostname.replace(/:\/\//, '://api.') - return request - .get({ - url: `${root}/users/${username}`, - headers: getRequestHeaders(optionalPrivateToken), - }) - .then(res => { - const body = JSON.parse(res.body) - - let profile = body.blog || body.html_url + const root = getApiHost(hostname) + return fetch(`${root}/users/${username}`, { + headers: getFetchHeaders(optionalPrivateToken), + }).then(res => + res.json().then(body => { + let profile = isValidHttpUrl(body.blog) ? body.blog : body.html_url + + // Check for authentication required + if ( + (!profile && body.message.includes('Must authenticate')) || + res.status === 401 + ) { + throw new Error( + `Missing authentication for GitHub API. Did you set PRIVATE_TOKEN?`, + ) + } // Github throwing specific errors as 200... if (!profile && body.message) { - throw new Error( - `Login not found when adding a contributor for username - ${username}.`, - ) + if (body.message.toLowerCase().includes('api rate limit exceeded')) { + throw new Error(body.message) + } else { + throw new Error(`The username ${username} doesn't exist on GitHub.`) + } } - profile = profile.startsWith('http') ? profile : `http://${profile}` + profile = parseHttpUrl(profile) return { login: body.login, @@ -92,17 +117,14 @@ const getUserInfo = function(username, hostname, optionalPrivateToken) { avatar_url: body.avatar_url, profile, } - }) + }), + ) } -const getContributors = function(owner, name, hostname, optionalPrivateToken) { - if (!hostname) { - hostname = 'https://github.com' - } - - const root = hostname.replace(/:\/\//, '://api.') - const url = `${root}/repos/${owner}/${name}/contributors?per_page=100` - return getContributorsPage(url, optionalPrivateToken) +const getContributors = function (owner, name, hostname, optionalPrivateToken) { + const root = getApiHost(hostname) + const contributorsUrl = `${root}/repos/${owner}/${name}/contributors?per_page=100` + return getContributorsPage(contributorsUrl, optionalPrivateToken) } module.exports = { diff --git a/src/repo/gitlab.js b/src/repo/gitlab.js index 2115f105..7269e479 100644 --- a/src/repo/gitlab.js +++ b/src/repo/gitlab.js @@ -1,5 +1,4 @@ -const pify = require('pify') -const request = pify(require('request')) +const fetch = require('node-fetch') const addPrivateToken = (url, privateToken = '') => { if (privateToken === '') return url @@ -9,25 +8,24 @@ const addPrivateToken = (url, privateToken = '') => { .replace('&', '?') } -const getUserInfo = function(username, hostname, privateToken) { +const getUserInfo = function (username, hostname, privateToken) { /* eslint-disable complexity */ if (!hostname) { hostname = 'https://gitlab.com' } - return request - .get({ - url: addPrivateToken( - `${hostname}/api/v4/users?username=${username}`, - privateToken, - ), + return fetch( + addPrivateToken( + `${hostname}/api/v4/users?username=${username}`, + privateToken, + ), + { headers: { - 'User-Agent': 'request', + 'User-Agent': 'node-fetch', }, - }) - .then(res => { - const body = JSON.parse(res.body) - + }, + ).then(res => + res.json().then(body => { // Gitlab returns an array of users. If it is empty, it means the username provided does not exist if (!body || body.length === 0) { throw new Error(`User ${username} not found`) @@ -48,27 +46,20 @@ const getUserInfo = function(username, hostname, privateToken) { ? user.web_url : `http://${user.web_url}`, } - }) + }), + ) } -const getContributors = function(owner, name, hostname, privateToken) { +const getContributors = function (owner, name, hostname, privateToken) { if (!hostname) { hostname = 'https://gitlab.com' } - return request - .get({ - url: addPrivateToken( - `${hostname}/api/v4/projects?search=${name}`, - privateToken, - ), - headers: { - 'User-Agent': 'request', - }, - }) - .then(res => { - const projects = JSON.parse(res.body) - + return fetch( + addPrivateToken(`${hostname}/api/v4/projects?search=${name}`, privateToken), + {headers: {'User-Agent': 'node-fetch'}}, + ).then(res => + res.json().then(projects => { // Gitlab returns an array of users. If it is empty, it means the username provided does not exist if (!projects || projects.length === 0) { throw new Error(`Project ${owner}/${name} not found`) @@ -86,27 +77,25 @@ const getContributors = function(owner, name, hostname, privateToken) { throw new Error(`Project ${owner}/${name} not found`) } - return request - .get({ - url: addPrivateToken( - `${hostname}/api/v4/projects/${project.id}/repository/contributors`, - privateToken, - ), - headers: { - 'User-Agent': 'request', - }, - }) - .then(newRes => { - const contributors = JSON.parse(newRes.body) - if (newRes.statusCode >= 400) { - if (newRes.statusCode === 404) { - throw new Error('No contributors found on the GitLab repository') - } + return fetch( + addPrivateToken( + `${hostname}/api/v4/projects/${project.id}/repository/contributors`, + privateToken, + ), + {headers: {'User-Agent': 'node-fetch'}}, + ).then(newRes => { + if (newRes.status === 404 || newRes.status >= 500) { + throw new Error('No contributors found on the GitLab repository') + } + return newRes.json().then(contributors => { + if (newRes.status >= 400 || !newRes.ok) { throw new Error(contributors.message) } return contributors.map(item => item.name) }) - }) + }) + }), + ) } module.exports = { diff --git a/src/repo/index.js b/src/repo/index.js index ac3196c8..3ff5492a 100644 --- a/src/repo/index.js +++ b/src/repo/index.js @@ -1,7 +1,7 @@ const githubAPI = require('./github') const gitlabAPI = require('./gitlab') -const privateToken = (process.env && process.env.PRIVATE_TOKEN) || '' +const privateToken = (process.env && (process.env.ALL_CONTRIBUTORS_PRIVATE_TOKEN || process.env.PRIVATE_TOKEN)) || '' const SUPPORTED_REPO_TYPES = { github: { value: 'github', @@ -12,6 +12,8 @@ const SUPPORTED_REPO_TYPES = { '<%= options.repoHost || "https://github.com" %>/<%= options.projectOwner %>/<%= options.projectName %>/commits?author=<%= contributor.login %>', linkToIssues: '<%= options.repoHost || "https://github.com" %>/<%= options.projectOwner %>/<%= options.projectName %>/issues?q=author%3A<%= contributor.login %>', + linkToReviews: + '<%= options.repoHost || "https://github.com" %>/<%= options.projectOwner %>/<%= options.projectName %>/pulls?q=is%3Apr+reviewed-by%3A<%= contributor.login %>', getUserInfo: githubAPI.getUserInfo, getContributors: githubAPI.getContributors, }, @@ -24,6 +26,8 @@ const SUPPORTED_REPO_TYPES = { '<%= options.repoHost || "https://gitlab.com" %>/<%= options.projectOwner %>/<%= options.projectName %>/commits/master', linkToIssues: '<%= options.repoHost || "https://gitlab.com" %>/<%= options.projectOwner %>/<%= options.projectName %>/issues?author_username=<%= contributor.login %>', + linkToReviews: + '<%= options.repoHost || "https://gitlab.com" %>/<%= options.projectOwner %>/<%= options.projectName %>/merge_requests?scope=all&state=all&approver_usernames[]=<%= contributor.login %>', getUserInfo: gitlabAPI.getUserInfo, getContributors: gitlabAPI.getContributors, }, @@ -77,6 +81,13 @@ const getLinkToIssues = function(repoType) { return null } +const getLinkToReviews = function(repoType) { + if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].linkToReviews + } + return null +} + const getUserInfo = function(username, repoType, repoHost) { if (repoType in SUPPORTED_REPO_TYPES) { return SUPPORTED_REPO_TYPES[repoType].getUserInfo( @@ -107,6 +118,7 @@ module.exports = { getTypeName, getLinkToCommits, getLinkToIssues, + getLinkToReviews, getUserInfo, getContributors, } diff --git a/src/util/__tests__/url.js b/src/util/__tests__/url.js new file mode 100644 index 00000000..359775e9 --- /dev/null +++ b/src/util/__tests__/url.js @@ -0,0 +1,57 @@ +import url from '../url' + +test(`Result of protocol validation should be true`, () => { + expect(url.isHttpProtocol('http:')).toBe(true) + expect(url.isHttpProtocol('https:')).toBe(true) +}) + +test(`Result of protocol validation should be false`, () => { + expect(url.isHttpProtocol('ftp:')).toBe(false) +}) + +test(`Result of url validation should be true`, () => { + expect(url.isValidHttpUrl('https://api.github.com/users/octocat')).toBe(true) +}) + +test(`Result of url validation should be false when url uses wrong protocol`, () => { + expect( + url.isValidHttpUrl( + 'git://git@github.com:all-contributors/all-contributors-cli.git', + ), + ).toBe(false) +}) + +test(`Result of url validation should be false when input isn't url`, () => { + expect(url.isValidHttpUrl('github-octocat')).toBe(false) +}) + +test(`Result of parsed url should be equal`, () => { + const input = 'https://api.github.com/users/octocat' + const expected = 'https://api.github.com/users/octocat' + expect(url.parseHttpUrl(input)).toBe(expected) +}) + +test(`Result of parsed url without protocol should be equal`, () => { + const input = 'example.com' + const expected = 'http://example.com/' + expect(url.parseHttpUrl(input)).toBe(expected) +}) + +test(`Throw an error when parsed input isn't a string`, () => { + const input = 123 + expect(url.parseHttpUrl.bind(null, input)).toThrowError( + 'input must be a string', + ) +}) + +test(`Throw an error when parsed url has wrong protocol`, () => { + const input = 'ftp://domain.xyz' + expect(url.parseHttpUrl.bind(null, input)).toThrowError( + 'Provided URL has an invalid protocol', + ) +}) + +test(`Throw an error when parsed input isn't a URL`, () => { + const input = 'some string' + expect(url.parseHttpUrl.bind(null, input)).toThrowError('Invalid URL') +}) diff --git a/src/util/contribution-types.js b/src/util/contribution-types.js index 8d792c11..98073263 100644 --- a/src/util/contribution-types.js +++ b/src/util/contribution-types.js @@ -3,6 +3,14 @@ const repo = require('../repo') const defaultTypes = function(repoType) { return { + a11y: { + symbol: '️️️️♿️', + description: 'Accessibility', + }, + audio: { + symbol: '🔊', + description: 'Audio', + }, blog: { symbol: '📝', description: 'Blogposts', @@ -25,6 +33,10 @@ const defaultTypes = function(repoType) { symbol: '🖋', description: 'Content', }, + data: { + symbol: '🔣', + description: 'Data', + }, design: { symbol: '🎨', description: 'Design', @@ -62,6 +74,10 @@ const defaultTypes = function(repoType) { symbol: '🚧', description: 'Maintenance', }, + mentoring: { + symbol: '🧑‍🏫', + description: 'Mentoring', + }, platform: { symbol: '📦', description: 'Packaging/porting to new platform', @@ -78,9 +94,14 @@ const defaultTypes = function(repoType) { symbol: '💬', description: 'Answering Questions', }, + research: { + symbol: '🔬', + description: 'Research', + }, review: { symbol: '👀', description: 'Reviewed Pull Requests', + link: repo.getLinkToReviews(repoType), }, security: { symbol: '🛡️', diff --git a/src/util/index.js b/src/util/index.js index a58592d7..9cb53145 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -3,4 +3,5 @@ module.exports = { contributionTypes: require('./contribution-types'), git: require('./git'), markdown: require('./markdown'), + url: require('./url'), } diff --git a/src/util/url.js b/src/util/url.js new file mode 100644 index 00000000..348cbdda --- /dev/null +++ b/src/util/url.js @@ -0,0 +1,33 @@ +function isHttpProtocol(input) { + return new RegExp('^https?\\:?$').test(input) +} + +function isValidHttpUrl(input) { + try { + const url = new URL(input) + + return isHttpProtocol(url.protocol) + } catch (e) { + return false + } +} + +function parseHttpUrl(input) { + if (typeof input !== 'string') { + throw new TypeError('input must be a string') + } + + const url = new URL(new RegExp('^\\w+\\:\\/\\/').test(input) ? input : `http://${input}`) + + if (!isHttpProtocol(url.protocol)) { + throw new TypeError('Provided URL has an invalid protocol') + } + + return url.toString() +} + +module.exports = { + isHttpProtocol, + isValidHttpUrl, + parseHttpUrl +}