diff --git a/.all-contributorsrc b/.all-contributorsrc index 76082f3a..53d241e1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -191,6 +191,17 @@ "contributions": [ "doc" ] + }, + { + "login": "tigermarques", + "name": "João Marques", + "avatar_url": "https://avatars0.githubusercontent.com/u/15315098?v=4", + "profile": "https://github.com/tigermarques", + "contributions": [ + "code", + "doc", + "ideas" + ] } ] } diff --git a/README.md b/README.md index e351bc57..a0357fcc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![version][version-badge]][package] [![downloads][downloads-badge]][downloads] [![MIT License][license-badge]][license] -[![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-21-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] @@ -25,7 +25,7 @@ want to maintain the table by hand This is a tool to help automate adding contributor acknowledgements according to the [all-contributors](https://github.com/kentcdodds/all-contributors) -specification. +specification for your GitHub or GitLab repository. ## Table of Contents @@ -104,7 +104,7 @@ all-contributors add all-contributors add jfmengels code,doc ``` -Where `username` is the user's GitHub username, and `contribution` is a +Where `username` is the user's GitHub or Gitlab 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)): @@ -156,6 +156,8 @@ These are the keys you can specify: `jfmengels/all-contributors-cli` --> `jfmengels`. Mandatory. * `projectName`: Name of the project. Example: `jfmengels/all-contributors-cli` --> `all-contributors-cli`. Mandatory. +* `repoType`: Type of repository. Must be either `github` or `gitlab`. Default: `github`. +* `repoHost`: Points to the repository hostname. Change it if you use a self hosted repository. Default: `https://github.com` if `repoType` is `github`, and `https://gitlab.com` if `repoType` is `gitlab`. * `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`. @@ -176,8 +178,7 @@ Thanks goes to these wonderful people | :---: | :---: | :---: | :---: | :---: | :---: | | [
Jerod Santo](https://jerodsanto.net)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jerodsanto "Code") | [
Kevin Jalbert](https://github.com/kevinjalbert)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=kevinjalbert "Code") | [
tunnckoCore](https://i.am.charlike.online)
[🔧](#tool-charlike "Tools") | [
Mehdi Achour](https://machour.idk.tn/)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=machour "Code") | [
Roy Revelt](https://codsen.com)
[🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3Arevelt "Bug reports") | [
Chris Vickery](https://github.com/chrisinajar)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=chrisinajar "Code") | | [
Bryce Reynolds](https://github.com/brycereynolds)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=brycereynolds "Code") | [
James, please](http://www.jmeas.com)
[🤔](#ideas-jmeas "Ideas, Planning, & Feedback") [💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jmeas "Code") | [
Spyros Ioakeimidis](http://www.spyros.io)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=spirosikmd "Code") | [
Fernando Costa](https://github.com/fadc80)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=fadc80 "Code") | [
snipe](https://snipe.net)
[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=snipe "Documentation") | [
Gant Laborde](http://gantlaborde.com/)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=GantMan "Code") | -| [
Md Zubair Ahmed](https://in.linkedin.com/in/mzubairahmed)
[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=M-ZubairAhmed "Documentation") [🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3AM-ZubairAhmed "Bug reports") | [
Divjot Singh](http://bogas04.github.io)
[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=bogas04 "Documentation") | - +| [
Md Zubair Ahmed](https://in.linkedin.com/in/mzubairahmed)
[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=M-ZubairAhmed "Documentation") [🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3AM-ZubairAhmed "Bug reports") | [
Divjot Singh](http://bogas04.github.io)
[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=bogas04 "Documentation") | [
João Marques](https://github.com/tigermarques)
[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=tigermarques "Code") [📖](https://github.com/jfmengels/all-contributors-cli/commits?author=tigermarques "Documentation") [🤔](#ideas-tigermarques "Ideas, Planning, & Feedback") | This project follows the diff --git a/src/cli.js b/src/cli.js index c62c221c..636881dc 100755 --- a/src/cli.js +++ b/src/cli.js @@ -9,6 +9,7 @@ const inquirer = require('inquirer') const init = require('./init') const generate = require('./generate') const util = require('./util') +const repo = require('./repo') const updateContributors = require('./contributors') const cwd = process.cwd() @@ -27,7 +28,7 @@ const yargv = yargs .usage('Usage: $0 init') .command( 'check', - 'Compares contributors from GitHub with the ones credited in .all-contributorsrc', + 'Compares contributors from the repository with the ones credited in .all-contributorsrc', ) .usage('Usage: $0 check') .boolean('commit') @@ -74,25 +75,26 @@ function addContribution(argv) { function checkContributors(argv) { const configData = util.configFile.readConfig(argv.config) - return util - .check(configData.projectOwner, configData.projectName) - .then(ghContributors => { + return repo + .getContributors(configData.projectOwner, configData.projectName, configData.repoType, configData.repoHost) + .then(repoContributors => { + const checkKey = repo.getCheckKey(configData.repoType) const knownContributions = configData.contributors.reduce((obj, item) => { - obj[item.login] = item.contributions + obj[item[checkKey]] = item.contributions return obj }, {}) const knownContributors = configData.contributors.map( - contributor => contributor.login, + contributor => contributor[checkKey], ) - const missingInConfig = ghContributors.filter( - login => !knownContributors.includes(login), + const missingInConfig = repoContributors.filter( + key => !knownContributors.includes(key), ) - const missingFromGithub = knownContributors.filter(login => { + const missingFromRepo = knownContributors.filter(key => { return ( - !ghContributors.includes(login) && - (knownContributions[login].includes('code') || - knownContributions[login].includes('test')) + !repoContributors.includes(key) && + (knownContributions[key].includes('code') || + knownContributions[key].includes('test')) ) }) @@ -103,11 +105,11 @@ function checkContributors(argv) { process.stdout.write(` ${missingInConfig.join(', ')}\n`) } - if (missingFromGithub.length) { + if (missingFromRepo.length) { process.stdout.write( chalk.bold('Unknown contributors found in .all-contributorsrc:\n'), ) - process.stdout.write(`${missingFromGithub.join(', ')}\n`) + process.stdout.write(`${missingFromRepo.join(', ')}\n`) } }) } @@ -136,7 +138,7 @@ function promptForCommand(argv) { value: 'generate', }, { - name: 'Compare contributors from GitHub with the credited ones', + name: 'Compare contributors from the repository with the credited ones', value: 'check', }, ], @@ -153,16 +155,16 @@ function promptForCommand(argv) { 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}`) + 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) diff --git a/src/contributors/__tests__/github.js b/src/contributors/__tests__/github.js deleted file mode 100644 index 6a305639..00000000 --- a/src/contributors/__tests__/github.js +++ /dev/null @@ -1,69 +0,0 @@ -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') -}) diff --git a/src/contributors/add.js b/src/contributors/add.js index 63a44d76..a23a2ddf 100644 --- a/src/contributors/add.js +++ b/src/contributors/add.js @@ -38,7 +38,7 @@ function updateExistingContributor(options, username, contributions) { } function addNewContributor(options, username, contributions, infoFetcher) { - return infoFetcher(username).then(userData => { + return infoFetcher(username, options.repoType, options.repoHost).then(userData => { const contributor = _.assign(userData, { contributions: formatContributions(options, [], contributions), }) diff --git a/src/contributors/github.js b/src/contributors/github.js deleted file mode 100644 index 88eae20d..00000000 --- a/src/contributors/github.js +++ /dev/null @@ -1,31 +0,0 @@ -const pify = require('pify') -const request = pify(require('request')) - -module.exports = function getUserInfo(username) { - /* eslint-disable complexity */ - 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, - } - }) -} diff --git a/src/contributors/index.js b/src/contributors/index.js index 4e3186f0..3e3544c9 100644 --- a/src/contributors/index.js +++ b/src/contributors/index.js @@ -1,7 +1,7 @@ const _ = require('lodash/fp') const util = require('../util') +const repo = require('../repo') const add = require('./add') -const github = require('./github') const prompt = require('./prompt') function isNewContributor(contributorList, username) { @@ -11,7 +11,7 @@ function isNewContributor(contributorList, username) { 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), + add(options, answers.username, answers.contributions, repo.getUserInfo), ) const writeContributorsP = contributorsP.then(contributors => diff --git a/src/contributors/prompt.js b/src/contributors/prompt.js index 232cc356..50dfc5c8 100644 --- a/src/contributors/prompt.js +++ b/src/contributors/prompt.js @@ -1,6 +1,7 @@ const _ = require('lodash/fp') const inquirer = require('inquirer') const util = require('../util') +const repo = require('../repo') const contributionChoices = _.flow( util.contributionTypes, @@ -21,7 +22,7 @@ function getQuestions(options, username, contributions) { { type: 'input', name: 'username', - message: "What is the contributor's GitHub username?", + message: `What is the contributor's ${repo.getTypeName(options.repoType)} username?`, when: !username, }, { diff --git a/src/generate/__tests__/format-contribution-type.js b/src/generate/__tests__/format-contribution-type.js index 5b242d16..d5bbf7e0 100644 --- a/src/generate/__tests__/format-contribution-type.js +++ b/src/generate/__tests__/format-contribution-type.js @@ -5,6 +5,8 @@ const fixtures = () => { const options = { projectOwner: 'jfmengels', projectName: 'all-contributors-cli', + repoType: 'github', + repoHost: 'https://github.com', imageSize: 100, } return {options} diff --git a/src/generate/__tests__/format-contributor.js b/src/generate/__tests__/format-contributor.js index 8182ea2e..1c12d909 100644 --- a/src/generate/__tests__/format-contributor.js +++ b/src/generate/__tests__/format-contributor.js @@ -6,6 +6,8 @@ function fixtures() { const options = { projectOwner: 'jfmengels', projectName: 'all-contributors-cli', + repoType: 'github', + repoHost: 'https://github.com', imageSize: 150, } return {options} diff --git a/src/init/prompt.js b/src/init/prompt.js index e529c684..81dcdd19 100644 --- a/src/init/prompt.js +++ b/src/init/prompt.js @@ -13,6 +13,34 @@ const questions = [ name: 'projectOwner', message: 'Who is the owner of the repository?', }, + { + type: 'list', + name: 'repoType', + message: 'What is the repository type?', + choices: [ + { + value: 'github', + name: 'GitHub', + }, + { + value: 'gitlab', + name: 'GitLab', + }, + ], + default: 'github', + }, + { + type: 'input', + name: 'repoHost', + message: 'Where is the repository hosted?', + default: function(answers) { + if (answers.repoType === 'github') { + return 'https://github.com' + } else if (answers.repoType === 'gitlab') { + return 'https://gitlab.com' + } + }, + }, { type: 'input', name: 'contributorFile', @@ -68,6 +96,8 @@ module.exports = function prompt() { config: { projectName: answers.projectName, projectOwner: answers.projectOwner, + repoType: answers.repoType, + repoHost: answers.repoHost, files: uniqueFiles([answers.contributorFile, answers.badgeFile]), imageSize: answers.imageSize, commit: answers.commit, diff --git a/src/repo/__tests__/github.js b/src/repo/__tests__/github.js new file mode 100644 index 00000000..66b7a837 --- /dev/null +++ b/src/repo/__tests__/github.js @@ -0,0 +1,132 @@ +import nock from 'nock' +import githubAPI from '../github' + +import allContributorsCliResponse from './github/all-contributors.response.json' +import allContributorsCliTransformed from './github/all-contributors.transformed.json' + +import reactNativeResponse1 from './github/react-native.response.1.json' +import reactNativeResponse2 from './github/react-native.response.2.json' +import reactNativeResponse3 from './github/react-native.response.3.json' +import reactNativeResponse4 from './github/react-native.response.4.json' +import reactNativeTransformed from './github/react-native.transformed.json' + +const getUserInfo = githubAPI.getUserInfo +const check = githubAPI.getContributors + +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: + '; rel="next", ; rel="last"', + }) + .get('/repositories/29028775/contributors?per_page=100&page=2') + .reply(200, reactNativeResponse2, { + Link: + '; rel="next", ; rel="last", ; rel="first", ; rel="prev"', + }) + .get('/repositories/29028775/contributors?per_page=100&page=3') + .reply(200, reactNativeResponse3, { + Link: + '; rel="next", ; rel="last", ; rel="first", ; rel="prev"', + }) + .get('/repositories/29028775/contributors?per_page=100&page=4') + .reply(200, reactNativeResponse4, { + Link: + '; rel="first", ; 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) +}) + +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') +}) + +test('retrieve user from a different github registry', async () => { + nock('http://api.github.myhost.com:3000') + .get('/users/nodisplayname') + .reply(200, { + login: 'nodisplayname', + name: 'No Display Name', + avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400', + html_url: 'http://github.myhost.com:3000/nodisplayname', + }) + + const info = await getUserInfo('nodisplayname', 'http://github.myhost.com:3000') + expect(info.name).toBe('No Display Name') +}) diff --git a/src/util/__tests__/fixtures/all-contributors.response.json b/src/repo/__tests__/github/all-contributors.response.json similarity index 100% rename from src/util/__tests__/fixtures/all-contributors.response.json rename to src/repo/__tests__/github/all-contributors.response.json diff --git a/src/util/__tests__/fixtures/all-contributors.transformed.json b/src/repo/__tests__/github/all-contributors.transformed.json similarity index 100% rename from src/util/__tests__/fixtures/all-contributors.transformed.json rename to src/repo/__tests__/github/all-contributors.transformed.json diff --git a/src/util/__tests__/fixtures/react-native.response.1.json b/src/repo/__tests__/github/react-native.response.1.json similarity index 100% rename from src/util/__tests__/fixtures/react-native.response.1.json rename to src/repo/__tests__/github/react-native.response.1.json diff --git a/src/util/__tests__/fixtures/react-native.response.2.json b/src/repo/__tests__/github/react-native.response.2.json similarity index 100% rename from src/util/__tests__/fixtures/react-native.response.2.json rename to src/repo/__tests__/github/react-native.response.2.json diff --git a/src/util/__tests__/fixtures/react-native.response.3.json b/src/repo/__tests__/github/react-native.response.3.json similarity index 100% rename from src/util/__tests__/fixtures/react-native.response.3.json rename to src/repo/__tests__/github/react-native.response.3.json diff --git a/src/util/__tests__/fixtures/react-native.response.4.json b/src/repo/__tests__/github/react-native.response.4.json similarity index 100% rename from src/util/__tests__/fixtures/react-native.response.4.json rename to src/repo/__tests__/github/react-native.response.4.json diff --git a/src/util/__tests__/fixtures/react-native.transformed.json b/src/repo/__tests__/github/react-native.transformed.json similarity index 100% rename from src/util/__tests__/fixtures/react-native.transformed.json rename to src/repo/__tests__/github/react-native.transformed.json diff --git a/src/repo/__tests__/gitlab.js b/src/repo/__tests__/gitlab.js new file mode 100644 index 00000000..c13cbff0 --- /dev/null +++ b/src/repo/__tests__/gitlab.js @@ -0,0 +1,87 @@ +import nock from 'nock' +import gitlabAPI from '../gitlab' + +const getUserInfo = gitlabAPI.getUserInfo + +async function rejects(promise) { + const error = await promise.catch(e => e) + expect(error).toBeTruthy() +} + +test('handle errors', async () => { + nock('https://gitlab.com') + .get('/api/v4/users?username=nodisplayname') + .reply(200, []) + + await rejects(getUserInfo('nodisplayname')) +}) + +test('fill in the name when it is returned', async () => { + nock('https://gitlab.com') + .get('/api/v4/users?username=nodisplayname') + .reply(200, [{ + username: 'nodisplayname', + name: 'No Display Name', + avatar_url: 'http://www.gravatar.com/avatar/3186450a99d1641bf75a44baa23f0826?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/nodisplayname', + }]) + + const info = await getUserInfo('nodisplayname') + expect(info.name).toBe('No Display Name') +}) + +test('fill in the name when null is returned', async () => { + nock('https://gitlab.com') + .get('/api/v4/users?username=nodisplayname') + .reply(200, [{ + username: 'nodisplayname', + name: null, + avatar_url: 'http://www.gravatar.com/avatar/3186450a99d1641bf75a44baa23f0826?s=80\u0026d=identicon', + web_url: 'https://gitlab.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://gitlab.com') + .get('/api/v4/users?username=nodisplayname') + .reply(200, [{ + username: 'nodisplayname', + name: '', + avatar_url: 'http://www.gravatar.com/avatar/3186450a99d1641bf75a44baa23f0826?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/nodisplayname', + }]) + + const info = await getUserInfo('nodisplayname') + expect(info.name).toBe('nodisplayname') +}) + +test('append http when no absolute link is provided', async () => { + nock('https://gitlab.com') + .get('/api/v4/users?username=nodisplayname') + .reply(200, [{ + username: 'nodisplayname', + name: 'No Display Name', + avatar_url: 'http://www.gravatar.com/avatar/3186450a99d1641bf75a44baa23f0826?s=80\u0026d=identicon', + web_url: 'www.gitlab.com/nodisplayname', + }]) + + const info = await getUserInfo('nodisplayname') + expect(info.profile).toBe('http://www.gitlab.com/nodisplayname') +}) + +test('retrieve user from a different gitlab registry', async () => { + nock('http://gitlab.myhost.com:3000') + .get('/api/v4/users?username=nodisplayname') + .reply(200, [{ + username: 'nodisplayname', + name: 'No Display Name', + avatar_url: 'http://www.gravatar.com/avatar/3186450a99d1641bf75a44baa23f0826?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/nodisplayname', + }]) + + const info = await getUserInfo('nodisplayname', 'http://gitlab.myhost.com:3000') + expect(info.name).toBe('No Display Name') +}) diff --git a/src/repo/__tests__/index.js b/src/repo/__tests__/index.js new file mode 100644 index 00000000..f4bab34c --- /dev/null +++ b/src/repo/__tests__/index.js @@ -0,0 +1,64 @@ +import repo from '..' + +jest.mock('../github') +jest.mock('../gitlab') + +const githubAPI = require('../github') +const gitlabAPI = require('../gitlab') + +test('get choices for init command', () => { + expect(repo.getChoices()).toEqual([{ + value: 'github', + name: 'GitHub' + }, { + value: 'gitlab', + name: 'GitLab' + }]) +}) + +test('get hostname for a given repo type', () => { + expect(repo.getHostname('github')).toEqual('https://github.com') + expect(repo.getHostname('github', 'http://my-github.com')).toEqual('http://my-github.com') + expect(repo.getHostname('gitlab')).toEqual('https://gitlab.com') + expect(repo.getHostname('gitlab', 'http://my-gitlab.com:3000')).toEqual('http://my-gitlab.com:3000') + expect(repo.getHostname('other')).toBe(null) +}) + +test('get repo name given a repo type', () => { + expect(repo.getTypeName('github')).toEqual('GitHub') + expect(repo.getTypeName('gitlab')).toEqual('GitLab') + expect(repo.getTypeName('other')).toBe(null) +}) + +test('get user info calls underlying APIs', () => { + githubAPI.getUserInfo.mockImplementationOnce(() => { + return { + login: 'nodisplayname', + name: 'nodisplayname', + avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400', + profile: 'https://github.com/nodisplayname', + } + }) + gitlabAPI.getUserInfo.mockImplementationOnce(() => { + return { + login: 'nodisplayname', + name: 'nodisplayname', + avatar_url: 'http://www.gravatar.com/avatar/3186450a99d1641bf75a44baa23f0826?s=80\u0026d=identicon', + profile: 'https://gitlab.com/nodisplayname', + } + }) + + expect(repo.getUserInfo('nodisplayname', 'github')).toEqual({ + login: 'nodisplayname', + name: 'nodisplayname', + avatar_url: 'https://avatars2.githubusercontent.com/u/3869412?v=3&s=400', + profile: 'https://github.com/nodisplayname', + }) + expect(repo.getUserInfo('nodisplayname', 'gitlab')).toEqual({ + login: 'nodisplayname', + name: 'nodisplayname', + avatar_url: 'http://www.gravatar.com/avatar/3186450a99d1641bf75a44baa23f0826?s=80\u0026d=identicon', + profile: 'https://gitlab.com/nodisplayname', + }) + expect(repo.getUserInfo('nodisplayname', 'other')).toBe(null) +}) diff --git a/src/repo/github.js b/src/repo/github.js new file mode 100644 index 00000000..84e1c4fd --- /dev/null +++ b/src/repo/github.js @@ -0,0 +1,94 @@ +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) + if (res.statusCode >= 400) { + if (res.statusCode === 404) { + throw new Error('No contributors found on the GitHub repository') + } + throw new Error(body.message) + } + 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 + }) +} + +const getUserInfo = function(username, hostname) { + /* eslint-disable complexity */ + if (!hostname) { + hostname = 'https://github.com'; + } + + const root = hostname.replace(/:\/\//, '://api.') + return request + .get({ + url: `${root}/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, + } + }) +} + +const getContributors = function(owner, name, hostname) { + if (!hostname) { + hostname = 'https://github.com'; + } + + const root = hostname.replace(/:\/\//, '://api.') + const url = `${root}/repos/${owner}/${name}/contributors?per_page=100` + return getContributorsPage(url) +} + +module.exports = { + getUserInfo, + getContributors +} diff --git a/src/repo/gitlab.js b/src/repo/gitlab.js new file mode 100644 index 00000000..5d7a69bc --- /dev/null +++ b/src/repo/gitlab.js @@ -0,0 +1,91 @@ +const pify = require('pify') +const request = pify(require('request')) + +const getUserInfo = function(username, hostname) { + /* eslint-disable complexity */ + if (!hostname) { + hostname = 'https://gitlab.com'; + } + + return request + .get({ + url: `${hostname}/api/v4/users?username=${username}`, + headers: { + 'User-Agent': 'request', + }, + }) + .then(res => { + const body = JSON.parse(res.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`) + } + + const user = body[0] + + return { + login: user.username, + name: user.name || username, + avatar_url: user.avatar_url, + profile: user.web_url.startsWith('http') ? user.web_url : `http://${user.web_url}`, + } + }) +} + +const getContributors = function(owner, name, hostname) { + if (!hostname) { + hostname = 'https://gitlab.com'; + } + + return request + .get({ + url: `${hostname}/api/v4/projects?search=${name}`, + headers: { + 'User-Agent': 'request', + }, + }) + .then(res => { + const projects = JSON.parse(res.body) + + // 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`) + } + + let project = null + for (let i = 0; i < projects.length; i++) { + if (projects[i].path_with_namespace === `${owner}/${name}`) { + project = projects[i] + break; + } + } + + if (!project) { + throw new Error(`Project ${owner}/${name} not found`) + } + + return request + .get({ + url: `${hostname}/api/v4/projects/${project.id}/repository/contributors`, + 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') + } + throw new Error(contributors.message) + } + return contributors.map(item => item.name) + }) + }) +} + +module.exports = { + getUserInfo, + getContributors +} diff --git a/src/repo/index.js b/src/repo/index.js new file mode 100644 index 00000000..2587fab9 --- /dev/null +++ b/src/repo/index.js @@ -0,0 +1,96 @@ +const githubAPI = require('./github') +const gitlabAPI = require('./gitlab') + +const SUPPORTED_REPO_TYPES = { + github: { + value: 'github', + name: 'GitHub', + checkKey: 'login', + defaultHost: 'https://github.com', + linkToCommits: '<%= options.repoHost %>/<%= options.projectOwner %>/<%= options.projectName %>/commits?author=<%= contributor.login %>', + linkToIssues: '<%= options.repoHost %>/<%= options.projectOwner %>/<%= options.projectName %>/issues?q=author%3A<%= contributor.login %>', + getUserInfo: githubAPI.getUserInfo, + getContributors: githubAPI.getContributors + }, + gitlab: { + value: 'gitlab', + name: 'GitLab', + checkKey: 'name', + defaultHost: 'https://gitlab.com', + linkToCommits: '<%= 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 %>', + getUserInfo: gitlabAPI.getUserInfo, + getContributors: gitlabAPI.getContributors + } +} + +const getChoices = function() { + return Object.keys(SUPPORTED_REPO_TYPES).map(key => SUPPORTED_REPO_TYPES[key]).map(item => { + return { + value: item.value, + name: item.name + } + }) +} + +const getHostname = function(repoType, repoHost) { + if (repoHost) { + return repoHost + } else if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].defaultHost + } + return null +} + +const getCheckKey = function(repoType) { + if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].checkKey + } + return null +} + +const getTypeName = function(repoType) { + if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].name + } + return null +} + +const getLinkToCommits = function(repoType) { + if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].linkToCommits + } + return null +} + +const getLinkToIssues = function(repoType) { + if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].linkToIssues + } + return null +} + +const getUserInfo = function(username, repoType, repoHost) { + if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].getUserInfo(username, getHostname(repoType, repoHost)) + } + return null +} + +const getContributors = function(owner, name, repoType, repoHost) { + if (repoType in SUPPORTED_REPO_TYPES) { + return SUPPORTED_REPO_TYPES[repoType].getContributors(owner, name, getHostname(repoType, repoHost)) + } + return null +} + +module.exports = { + getChoices, + getHostname, + getCheckKey, + getTypeName, + getLinkToCommits, + getLinkToIssues, + getUserInfo, + getContributors +} diff --git a/src/util/__tests__/check.js b/src/util/__tests__/check.js deleted file mode 100644 index 0bab1a8e..00000000 --- a/src/util/__tests__/check.js +++ /dev/null @@ -1,48 +0,0 @@ -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: - '; rel="next", ; rel="last"', - }) - .get('/repositories/29028775/contributors?per_page=100&page=2') - .reply(200, reactNativeResponse2, { - Link: - '; rel="next", ; rel="last", ; rel="first", ; rel="prev"', - }) - .get('/repositories/29028775/contributors?per_page=100&page=3') - .reply(200, reactNativeResponse3, { - Link: - '; rel="next", ; rel="last", ; rel="first", ; rel="prev"', - }) - .get('/repositories/29028775/contributors?per_page=100&page=4') - .reply(200, reactNativeResponse4, { - Link: - '; rel="first", ; 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) -}) diff --git a/src/util/check.js b/src/util/check.js deleted file mode 100644 index d87b08ef..00000000 --- a/src/util/check.js +++ /dev/null @@ -1,47 +0,0 @@ -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) - if (res.statusCode >= 400) { - throw new Error(body.message) - } - 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) -} diff --git a/src/util/config-file.js b/src/util/config-file.js index 294a32ef..9e02b2e7 100644 --- a/src/util/config-file.js +++ b/src/util/config-file.js @@ -4,7 +4,11 @@ const _ = require('lodash/fp') function readConfig(configPath) { try { - return JSON.parse(fs.readFileSync(configPath, 'utf-8')) + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + if (!('repoType' in config)) { + config.repoType = 'github' + } + return config } catch (error) { if (error.code === 'ENOENT') { throw new Error(`Configuration file not found: ${configPath}`) diff --git a/src/util/contribution-types.js b/src/util/contribution-types.js index e0424bad..d8c593bd 100644 --- a/src/util/contribution-types.js +++ b/src/util/contribution-types.js @@ -1,101 +1,99 @@ const _ = require('lodash/fp') +const repo = require('../repo') -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)', - }, - platform: { - symbol: '📦', - description: 'Packaging/porting to new platform', - }, - 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', - }, +const defaultTypes = function(repoType) { + return { + blog: { + symbol: '📝', + description: 'Blogposts', + }, + bug: { + symbol: '🐛', + description: 'Bug reports', + link: repo.getLinkToIssues(repoType), + }, + code: { + symbol: '💻', + description: 'Code', + link: repo.getLinkToCommits(repoType), + }, + design: { + symbol: '🎨', + description: 'Design', + }, + doc: { + symbol: '📖', + description: 'Documentation', + link: repo.getLinkToCommits(repoType), + }, + 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)', + }, + platform: { + symbol: '📦', + description: 'Packaging/porting to new platform', + }, + 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: repo.getLinkToCommits(repoType), + }, + 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) + return _.assign(defaultTypes(options.repoType), options.types) } diff --git a/src/util/index.js b/src/util/index.js index 116b95f1..93b0cbad 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -2,6 +2,5 @@ module.exports = { configFile: require('./config-file'), contributionTypes: require('./contribution-types'), git: require('./git'), - markdown: require('./markdown'), - check: require('./check'), + markdown: require('./markdown') }