mirror of
https://github.com/all-contributors/cli.git
synced 2025-01-24 21:46:29 +00:00
feat: add support for GitLab repositories (#84)
This commit is contained in:
parent
ab41caa0c7
commit
152a1fe30a
29 changed files with 746 additions and 327 deletions
|
@ -191,6 +191,17 @@
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
11
README.md
11
README.md
|
@ -10,7 +10,7 @@
|
||||||
[![version][version-badge]][package] [![downloads][downloads-badge]][downloads]
|
[![version][version-badge]][package] [![downloads][downloads-badge]][downloads]
|
||||||
[![MIT License][license-badge]][license]
|
[![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]
|
[![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc]
|
||||||
[![Watch on GitHub][github-watch-badge]][github-watch]
|
[![Watch on GitHub][github-watch-badge]][github-watch]
|
||||||
[![Star on GitHub][github-star-badge]][github-star]
|
[![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
|
This is a tool to help automate adding contributor acknowledgements according to
|
||||||
the [all-contributors](https://github.com/kentcdodds/all-contributors)
|
the [all-contributors](https://github.com/kentcdodds/all-contributors)
|
||||||
specification.
|
specification for your GitHub or GitLab repository.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ all-contributors add <username> <contribution>
|
||||||
all-contributors add jfmengels code,doc
|
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
|
`,`-separated list of ways to contribute, from the following list
|
||||||
([see the specs](https://github.com/kentcdodds/all-contributors#emoji-key)):
|
([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.
|
`jfmengels/all-contributors-cli` --> `jfmengels`. Mandatory.
|
||||||
* `projectName`: Name of the project. Example: `jfmengels/all-contributors-cli`
|
* `projectName`: Name of the project. Example: `jfmengels/all-contributors-cli`
|
||||||
--> `all-contributors-cli`. Mandatory.
|
--> `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
|
* `types`: Specify custom symbols or link templates for contribution types. Can
|
||||||
override the documented types.
|
override the documented types.
|
||||||
* `imageSize`: Size (in px) of the user's avatar. Default: `100`.
|
* `imageSize`: Size (in px) of the user's avatar. Default: `100`.
|
||||||
|
@ -176,8 +178,7 @@ Thanks goes to these wonderful people
|
||||||
| :---: | :---: | :---: | :---: | :---: | :---: |
|
| :---: | :---: | :---: | :---: | :---: | :---: |
|
||||||
| [<img src="https://avatars3.githubusercontent.com/u/8212?v=3" width="100px;"/><br /><sub><b>Jerod Santo</b></sub>](https://jerodsanto.net)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jerodsanto "Code") | [<img src="https://avatars1.githubusercontent.com/u/574871?v=3" width="100px;"/><br /><sub><b>Kevin Jalbert</b></sub>](https://github.com/kevinjalbert)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=kevinjalbert "Code") | [<img src="https://avatars3.githubusercontent.com/u/5038030?v=4" width="100px;"/><br /><sub><b>tunnckoCore</b></sub>](https://i.am.charlike.online)<br />[🔧](#tool-charlike "Tools") | [<img src="https://avatars2.githubusercontent.com/u/304450?v=4" width="100px;"/><br /><sub><b>Mehdi Achour</b></sub>](https://machour.idk.tn/)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=machour "Code") | [<img src="https://avatars1.githubusercontent.com/u/8344688?v=4" width="100px;"/><br /><sub><b>Roy Revelt</b></sub>](https://codsen.com)<br />[🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3Arevelt "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/422331?v=4" width="100px;"/><br /><sub><b>Chris Vickery</b></sub>](https://github.com/chrisinajar)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=chrisinajar "Code") |
|
| [<img src="https://avatars3.githubusercontent.com/u/8212?v=3" width="100px;"/><br /><sub><b>Jerod Santo</b></sub>](https://jerodsanto.net)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jerodsanto "Code") | [<img src="https://avatars1.githubusercontent.com/u/574871?v=3" width="100px;"/><br /><sub><b>Kevin Jalbert</b></sub>](https://github.com/kevinjalbert)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=kevinjalbert "Code") | [<img src="https://avatars3.githubusercontent.com/u/5038030?v=4" width="100px;"/><br /><sub><b>tunnckoCore</b></sub>](https://i.am.charlike.online)<br />[🔧](#tool-charlike "Tools") | [<img src="https://avatars2.githubusercontent.com/u/304450?v=4" width="100px;"/><br /><sub><b>Mehdi Achour</b></sub>](https://machour.idk.tn/)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=machour "Code") | [<img src="https://avatars1.githubusercontent.com/u/8344688?v=4" width="100px;"/><br /><sub><b>Roy Revelt</b></sub>](https://codsen.com)<br />[🐛](https://github.com/jfmengels/all-contributors-cli/issues?q=author%3Arevelt "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/422331?v=4" width="100px;"/><br /><sub><b>Chris Vickery</b></sub>](https://github.com/chrisinajar)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=chrisinajar "Code") |
|
||||||
| [<img src="https://avatars2.githubusercontent.com/u/1026002?v=4" width="100px;"/><br /><sub><b>Bryce Reynolds</b></sub>](https://github.com/brycereynolds)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=brycereynolds "Code") | [<img src="https://avatars3.githubusercontent.com/u/2322305?v=4" width="100px;"/><br /><sub><b>James, please</b></sub>](http://www.jmeas.com)<br />[🤔](#ideas-jmeas "Ideas, Planning, & Feedback") [💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jmeas "Code") | [<img src="https://avatars3.githubusercontent.com/u/1057324?v=4" width="100px;"/><br /><sub><b>Spyros Ioakeimidis</b></sub>](http://www.spyros.io)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=spirosikmd "Code") | [<img src="https://avatars3.githubusercontent.com/u/12335761?v=4" width="100px;"/><br /><sub><b>Fernando Costa</b></sub>](https://github.com/fadc80)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=fadc80 "Code") | [<img src="https://avatars0.githubusercontent.com/u/197404?v=4" width="100px;"/><br /><sub><b>snipe</b></sub>](https://snipe.net)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=snipe "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/997157?v=4" width="100px;"/><br /><sub><b>Gant Laborde</b></sub>](http://gantlaborde.com/)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=GantMan "Code") |
|
| [<img src="https://avatars2.githubusercontent.com/u/1026002?v=4" width="100px;"/><br /><sub><b>Bryce Reynolds</b></sub>](https://github.com/brycereynolds)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=brycereynolds "Code") | [<img src="https://avatars3.githubusercontent.com/u/2322305?v=4" width="100px;"/><br /><sub><b>James, please</b></sub>](http://www.jmeas.com)<br />[🤔](#ideas-jmeas "Ideas, Planning, & Feedback") [💻](https://github.com/jfmengels/all-contributors-cli/commits?author=jmeas "Code") | [<img src="https://avatars3.githubusercontent.com/u/1057324?v=4" width="100px;"/><br /><sub><b>Spyros Ioakeimidis</b></sub>](http://www.spyros.io)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=spirosikmd "Code") | [<img src="https://avatars3.githubusercontent.com/u/12335761?v=4" width="100px;"/><br /><sub><b>Fernando Costa</b></sub>](https://github.com/fadc80)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=fadc80 "Code") | [<img src="https://avatars0.githubusercontent.com/u/197404?v=4" width="100px;"/><br /><sub><b>snipe</b></sub>](https://snipe.net)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=snipe "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/997157?v=4" width="100px;"/><br /><sub><b>Gant Laborde</b></sub>](http://gantlaborde.com/)<br />[💻](https://github.com/jfmengels/all-contributors-cli/commits?author=GantMan "Code") |
|
||||||
| [<img src="https://avatars2.githubusercontent.com/u/17708702?v=4" width="100px;"/><br /><sub><b>Md Zubair Ahmed</b></sub>](https://in.linkedin.com/in/mzubairahmed)<br />[📖](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") | [<img src="https://avatars3.githubusercontent.com/u/6177621?v=4" width="100px;"/><br /><sub><b>Divjot Singh</b></sub>](http://bogas04.github.io)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=bogas04 "Documentation") |
|
| [<img src="https://avatars2.githubusercontent.com/u/17708702?v=4" width="100px;"/><br /><sub><b>Md Zubair Ahmed</b></sub>](https://in.linkedin.com/in/mzubairahmed)<br />[📖](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") | [<img src="https://avatars3.githubusercontent.com/u/6177621?v=4" width="100px;"/><br /><sub><b>Divjot Singh</b></sub>](http://bogas04.github.io)<br />[📖](https://github.com/jfmengels/all-contributors-cli/commits?author=bogas04 "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/15315098?v=4" width="100px;"/><br /><sub><b>João Marques</b></sub>](https://github.com/tigermarques)<br />[💻](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") |
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
This project follows the
|
This project follows the
|
||||||
|
|
52
src/cli.js
52
src/cli.js
|
@ -9,6 +9,7 @@ const inquirer = require('inquirer')
|
||||||
const init = require('./init')
|
const init = require('./init')
|
||||||
const generate = require('./generate')
|
const generate = require('./generate')
|
||||||
const util = require('./util')
|
const util = require('./util')
|
||||||
|
const repo = require('./repo')
|
||||||
const updateContributors = require('./contributors')
|
const updateContributors = require('./contributors')
|
||||||
|
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
|
@ -27,7 +28,7 @@ const yargv = yargs
|
||||||
.usage('Usage: $0 init')
|
.usage('Usage: $0 init')
|
||||||
.command(
|
.command(
|
||||||
'check',
|
'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')
|
.usage('Usage: $0 check')
|
||||||
.boolean('commit')
|
.boolean('commit')
|
||||||
|
@ -74,25 +75,26 @@ function addContribution(argv) {
|
||||||
function checkContributors(argv) {
|
function checkContributors(argv) {
|
||||||
const configData = util.configFile.readConfig(argv.config)
|
const configData = util.configFile.readConfig(argv.config)
|
||||||
|
|
||||||
return util
|
return repo
|
||||||
.check(configData.projectOwner, configData.projectName)
|
.getContributors(configData.projectOwner, configData.projectName, configData.repoType, configData.repoHost)
|
||||||
.then(ghContributors => {
|
.then(repoContributors => {
|
||||||
|
const checkKey = repo.getCheckKey(configData.repoType)
|
||||||
const knownContributions = configData.contributors.reduce((obj, item) => {
|
const knownContributions = configData.contributors.reduce((obj, item) => {
|
||||||
obj[item.login] = item.contributions
|
obj[item[checkKey]] = item.contributions
|
||||||
return obj
|
return obj
|
||||||
}, {})
|
}, {})
|
||||||
const knownContributors = configData.contributors.map(
|
const knownContributors = configData.contributors.map(
|
||||||
contributor => contributor.login,
|
contributor => contributor[checkKey],
|
||||||
)
|
)
|
||||||
|
|
||||||
const missingInConfig = ghContributors.filter(
|
const missingInConfig = repoContributors.filter(
|
||||||
login => !knownContributors.includes(login),
|
key => !knownContributors.includes(key),
|
||||||
)
|
)
|
||||||
const missingFromGithub = knownContributors.filter(login => {
|
const missingFromRepo = knownContributors.filter(key => {
|
||||||
return (
|
return (
|
||||||
!ghContributors.includes(login) &&
|
!repoContributors.includes(key) &&
|
||||||
(knownContributions[login].includes('code') ||
|
(knownContributions[key].includes('code') ||
|
||||||
knownContributions[login].includes('test'))
|
knownContributions[key].includes('test'))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -103,11 +105,11 @@ function checkContributors(argv) {
|
||||||
process.stdout.write(` ${missingInConfig.join(', ')}\n`)
|
process.stdout.write(` ${missingInConfig.join(', ')}\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missingFromGithub.length) {
|
if (missingFromRepo.length) {
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
chalk.bold('Unknown contributors found in .all-contributorsrc:\n'),
|
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',
|
value: 'generate',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Compare contributors from GitHub with the credited ones',
|
name: 'Compare contributors from the repository with the credited ones',
|
||||||
value: 'check',
|
value: 'check',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -153,16 +155,16 @@ function promptForCommand(argv) {
|
||||||
promptForCommand(yargv)
|
promptForCommand(yargv)
|
||||||
.then(command => {
|
.then(command => {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'init':
|
case 'init':
|
||||||
return init()
|
return init()
|
||||||
case 'generate':
|
case 'generate':
|
||||||
return startGeneration(yargv)
|
return startGeneration(yargv)
|
||||||
case 'add':
|
case 'add':
|
||||||
return addContribution(yargv)
|
return addContribution(yargv)
|
||||||
case 'check':
|
case 'check':
|
||||||
return checkContributors(yargv)
|
return checkContributors(yargv)
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown command ${command}`)
|
throw new Error(`Unknown command ${command}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(onError)
|
.catch(onError)
|
||||||
|
|
|
@ -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')
|
|
||||||
})
|
|
|
@ -38,7 +38,7 @@ function updateExistingContributor(options, username, contributions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNewContributor(options, username, contributions, infoFetcher) {
|
function addNewContributor(options, username, contributions, infoFetcher) {
|
||||||
return infoFetcher(username).then(userData => {
|
return infoFetcher(username, options.repoType, options.repoHost).then(userData => {
|
||||||
const contributor = _.assign(userData, {
|
const contributor = _.assign(userData, {
|
||||||
contributions: formatContributions(options, [], contributions),
|
contributions: formatContributions(options, [], contributions),
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const util = require('../util')
|
const util = require('../util')
|
||||||
|
const repo = require('../repo')
|
||||||
const add = require('./add')
|
const add = require('./add')
|
||||||
const github = require('./github')
|
|
||||||
const prompt = require('./prompt')
|
const prompt = require('./prompt')
|
||||||
|
|
||||||
function isNewContributor(contributorList, username) {
|
function isNewContributor(contributorList, username) {
|
||||||
|
@ -11,7 +11,7 @@ function isNewContributor(contributorList, username) {
|
||||||
module.exports = function addContributor(options, username, contributions) {
|
module.exports = function addContributor(options, username, contributions) {
|
||||||
const answersP = prompt(options, username, contributions)
|
const answersP = prompt(options, username, contributions)
|
||||||
const contributorsP = answersP.then(answers =>
|
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 =>
|
const writeContributorsP = contributorsP.then(contributors =>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const inquirer = require('inquirer')
|
const inquirer = require('inquirer')
|
||||||
const util = require('../util')
|
const util = require('../util')
|
||||||
|
const repo = require('../repo')
|
||||||
|
|
||||||
const contributionChoices = _.flow(
|
const contributionChoices = _.flow(
|
||||||
util.contributionTypes,
|
util.contributionTypes,
|
||||||
|
@ -21,7 +22,7 @@ function getQuestions(options, username, contributions) {
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'username',
|
name: 'username',
|
||||||
message: "What is the contributor's GitHub username?",
|
message: `What is the contributor's ${repo.getTypeName(options.repoType)} username?`,
|
||||||
when: !username,
|
when: !username,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,6 +5,8 @@ const fixtures = () => {
|
||||||
const options = {
|
const options = {
|
||||||
projectOwner: 'jfmengels',
|
projectOwner: 'jfmengels',
|
||||||
projectName: 'all-contributors-cli',
|
projectName: 'all-contributors-cli',
|
||||||
|
repoType: 'github',
|
||||||
|
repoHost: 'https://github.com',
|
||||||
imageSize: 100,
|
imageSize: 100,
|
||||||
}
|
}
|
||||||
return {options}
|
return {options}
|
||||||
|
|
|
@ -6,6 +6,8 @@ function fixtures() {
|
||||||
const options = {
|
const options = {
|
||||||
projectOwner: 'jfmengels',
|
projectOwner: 'jfmengels',
|
||||||
projectName: 'all-contributors-cli',
|
projectName: 'all-contributors-cli',
|
||||||
|
repoType: 'github',
|
||||||
|
repoHost: 'https://github.com',
|
||||||
imageSize: 150,
|
imageSize: 150,
|
||||||
}
|
}
|
||||||
return {options}
|
return {options}
|
||||||
|
|
|
@ -13,6 +13,34 @@ const questions = [
|
||||||
name: 'projectOwner',
|
name: 'projectOwner',
|
||||||
message: 'Who is the owner of the repository?',
|
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',
|
type: 'input',
|
||||||
name: 'contributorFile',
|
name: 'contributorFile',
|
||||||
|
@ -68,6 +96,8 @@ module.exports = function prompt() {
|
||||||
config: {
|
config: {
|
||||||
projectName: answers.projectName,
|
projectName: answers.projectName,
|
||||||
projectOwner: answers.projectOwner,
|
projectOwner: answers.projectOwner,
|
||||||
|
repoType: answers.repoType,
|
||||||
|
repoHost: answers.repoHost,
|
||||||
files: uniqueFiles([answers.contributorFile, answers.badgeFile]),
|
files: uniqueFiles([answers.contributorFile, answers.badgeFile]),
|
||||||
imageSize: answers.imageSize,
|
imageSize: answers.imageSize,
|
||||||
commit: answers.commit,
|
commit: answers.commit,
|
||||||
|
|
132
src/repo/__tests__/github.js
Normal file
132
src/repo/__tests__/github.js
Normal file
|
@ -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:
|
||||||
|
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last"',
|
||||||
|
})
|
||||||
|
.get('/repositories/29028775/contributors?per_page=100&page=2')
|
||||||
|
.reply(200, reactNativeResponse2, {
|
||||||
|
Link:
|
||||||
|
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="prev"',
|
||||||
|
})
|
||||||
|
.get('/repositories/29028775/contributors?per_page=100&page=3')
|
||||||
|
.reply(200, reactNativeResponse3, {
|
||||||
|
Link:
|
||||||
|
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="prev"',
|
||||||
|
})
|
||||||
|
.get('/repositories/29028775/contributors?per_page=100&page=4')
|
||||||
|
.reply(200, reactNativeResponse4, {
|
||||||
|
Link:
|
||||||
|
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="prev"',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Handle a single results page correctly', async () => {
|
||||||
|
const transformed = await check('jfmengels', 'all-contributors-cli')
|
||||||
|
expect(transformed).toEqual(allContributorsCliTransformed)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Handle multiple results pages correctly', async () => {
|
||||||
|
const transformed = await check('facebook', 'react-native')
|
||||||
|
expect(transformed).toEqual(reactNativeTransformed)
|
||||||
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
87
src/repo/__tests__/gitlab.js
Normal file
87
src/repo/__tests__/gitlab.js
Normal file
|
@ -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')
|
||||||
|
})
|
64
src/repo/__tests__/index.js
Normal file
64
src/repo/__tests__/index.js
Normal file
|
@ -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)
|
||||||
|
})
|
94
src/repo/github.js
Normal file
94
src/repo/github.js
Normal file
|
@ -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
|
||||||
|
}
|
91
src/repo/gitlab.js
Normal file
91
src/repo/gitlab.js
Normal file
|
@ -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
|
||||||
|
}
|
96
src/repo/index.js
Normal file
96
src/repo/index.js
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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:
|
|
||||||
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last"',
|
|
||||||
})
|
|
||||||
.get('/repositories/29028775/contributors?per_page=100&page=2')
|
|
||||||
.reply(200, reactNativeResponse2, {
|
|
||||||
Link:
|
|
||||||
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="prev"',
|
|
||||||
})
|
|
||||||
.get('/repositories/29028775/contributors?per_page=100&page=3')
|
|
||||||
.reply(200, reactNativeResponse3, {
|
|
||||||
Link:
|
|
||||||
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="next", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=4>; rel="last", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=2>; rel="prev"',
|
|
||||||
})
|
|
||||||
.get('/repositories/29028775/contributors?per_page=100&page=4')
|
|
||||||
.reply(200, reactNativeResponse4, {
|
|
||||||
Link:
|
|
||||||
'<https://api.github.com/repositories/29028775/contributors?per_page=100&page=1>; rel="first", <https://api.github.com/repositories/29028775/contributors?per_page=100&page=3>; rel="prev"',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Handle a single results page correctly', async () => {
|
|
||||||
const transformed = await check('jfmengels', 'all-contributors-cli')
|
|
||||||
expect(transformed).toEqual(allContributorsCliTransformed)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Handle multiple results pages correctly', async () => {
|
|
||||||
const transformed = await check('facebook', 'react-native')
|
|
||||||
expect(transformed).toEqual(reactNativeTransformed)
|
|
||||||
})
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -4,7 +4,11 @@ const _ = require('lodash/fp')
|
||||||
|
|
||||||
function readConfig(configPath) {
|
function readConfig(configPath) {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
throw new Error(`Configuration file not found: ${configPath}`)
|
throw new Error(`Configuration file not found: ${configPath}`)
|
||||||
|
|
|
@ -1,101 +1,99 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
const repo = require('../repo')
|
||||||
|
|
||||||
const linkToCommits =
|
const defaultTypes = function(repoType) {
|
||||||
'https://github.com/<%= options.projectOwner %>/<%= options.projectName %>/commits?author=<%= contributor.login %>'
|
return {
|
||||||
const linkToIssues =
|
blog: {
|
||||||
'https://github.com/<%= options.projectOwner %>/<%= options.projectName %>/issues?q=author%3A<%= contributor.login %>'
|
symbol: '📝',
|
||||||
|
description: 'Blogposts',
|
||||||
const defaultTypes = {
|
},
|
||||||
blog: {
|
bug: {
|
||||||
symbol: '📝',
|
symbol: '🐛',
|
||||||
description: 'Blogposts',
|
description: 'Bug reports',
|
||||||
},
|
link: repo.getLinkToIssues(repoType),
|
||||||
bug: {
|
},
|
||||||
symbol: '🐛',
|
code: {
|
||||||
description: 'Bug reports',
|
symbol: '💻',
|
||||||
link: linkToIssues,
|
description: 'Code',
|
||||||
},
|
link: repo.getLinkToCommits(repoType),
|
||||||
code: {
|
},
|
||||||
symbol: '💻',
|
design: {
|
||||||
description: 'Code',
|
symbol: '🎨',
|
||||||
link: linkToCommits,
|
description: 'Design',
|
||||||
},
|
},
|
||||||
design: {
|
doc: {
|
||||||
symbol: '🎨',
|
symbol: '📖',
|
||||||
description: 'Design',
|
description: 'Documentation',
|
||||||
},
|
link: repo.getLinkToCommits(repoType),
|
||||||
doc: {
|
},
|
||||||
symbol: '📖',
|
eventOrganizing: {
|
||||||
description: 'Documentation',
|
symbol: '📋',
|
||||||
link: linkToCommits,
|
description: 'Event Organizing',
|
||||||
},
|
},
|
||||||
eventOrganizing: {
|
example: {
|
||||||
symbol: '📋',
|
symbol: '💡',
|
||||||
description: 'Event Organizing',
|
description: 'Examples',
|
||||||
},
|
},
|
||||||
example: {
|
financial: {
|
||||||
symbol: '💡',
|
symbol: '💵',
|
||||||
description: 'Examples',
|
description: 'Financial',
|
||||||
},
|
},
|
||||||
financial: {
|
fundingFinding: {
|
||||||
symbol: '💵',
|
symbol: '🔍',
|
||||||
description: 'Financial',
|
description: 'Funding Finding',
|
||||||
},
|
},
|
||||||
fundingFinding: {
|
ideas: {
|
||||||
symbol: '🔍',
|
symbol: '🤔',
|
||||||
description: 'Funding Finding',
|
description: 'Ideas, Planning, & Feedback',
|
||||||
},
|
},
|
||||||
ideas: {
|
infra: {
|
||||||
symbol: '🤔',
|
symbol: '🚇',
|
||||||
description: 'Ideas, Planning, & Feedback',
|
description: 'Infrastructure (Hosting, Build-Tools, etc)',
|
||||||
},
|
},
|
||||||
infra: {
|
platform: {
|
||||||
symbol: '🚇',
|
symbol: '📦',
|
||||||
description: 'Infrastructure (Hosting, Build-Tools, etc)',
|
description: 'Packaging/porting to new platform',
|
||||||
},
|
},
|
||||||
platform: {
|
plugin: {
|
||||||
symbol: '📦',
|
symbol: '🔌',
|
||||||
description: 'Packaging/porting to new platform',
|
description: 'Plugin/utility libraries',
|
||||||
},
|
},
|
||||||
plugin: {
|
question: {
|
||||||
symbol: '🔌',
|
symbol: '💬',
|
||||||
description: 'Plugin/utility libraries',
|
description: 'Answering Questions',
|
||||||
},
|
},
|
||||||
question: {
|
review: {
|
||||||
symbol: '💬',
|
symbol: '👀',
|
||||||
description: 'Answering Questions',
|
description: 'Reviewed Pull Requests',
|
||||||
},
|
},
|
||||||
review: {
|
talk: {
|
||||||
symbol: '👀',
|
symbol: '📢',
|
||||||
description: 'Reviewed Pull Requests',
|
description: 'Talks',
|
||||||
},
|
},
|
||||||
talk: {
|
test: {
|
||||||
symbol: '📢',
|
symbol: '⚠️',
|
||||||
description: 'Talks',
|
description: 'Tests',
|
||||||
},
|
link: repo.getLinkToCommits(repoType),
|
||||||
test: {
|
},
|
||||||
symbol: '⚠️',
|
tool: {
|
||||||
description: 'Tests',
|
symbol: '🔧',
|
||||||
link: linkToCommits,
|
description: 'Tools',
|
||||||
},
|
},
|
||||||
tool: {
|
translation: {
|
||||||
symbol: '🔧',
|
symbol: '🌍',
|
||||||
description: 'Tools',
|
description: 'Translation',
|
||||||
},
|
},
|
||||||
translation: {
|
tutorial: {
|
||||||
symbol: '🌍',
|
symbol: '✅',
|
||||||
description: 'Translation',
|
description: 'Tutorials',
|
||||||
},
|
},
|
||||||
tutorial: {
|
video: {
|
||||||
symbol: '✅',
|
symbol: '📹',
|
||||||
description: 'Tutorials',
|
description: 'Videos',
|
||||||
},
|
}
|
||||||
video: {
|
}
|
||||||
symbol: '📹',
|
|
||||||
description: 'Videos',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function(options) {
|
module.exports = function(options) {
|
||||||
return _.assign(defaultTypes, options.types)
|
return _.assign(defaultTypes(options.repoType), options.types)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,5 @@ module.exports = {
|
||||||
configFile: require('./config-file'),
|
configFile: require('./config-file'),
|
||||||
contributionTypes: require('./contribution-types'),
|
contributionTypes: require('./contribution-types'),
|
||||||
git: require('./git'),
|
git: require('./git'),
|
||||||
markdown: require('./markdown'),
|
markdown: require('./markdown')
|
||||||
check: require('./check'),
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue