feat: auto-fetching - continuation of #196 (#259)

* fix: Adjusting file access that was clashing with uncontrolled promises

* review: Adding code review changing and linting

* fix(cli): rectify an async call

Co-authored-by: Maximilian Berkmann <maxieberkmann@gmail.com>
This commit is contained in:
Justin Dalrymple 2020-05-25 01:52:14 +02:00 committed by Berkmann18
parent feefb2aac2
commit f54703d0ce
5 changed files with 212 additions and 202 deletions

View file

@ -1,13 +1,14 @@
module.exports = { module.exports = {
extends: [ extends: [
require.resolve('eslint-config-kentcdodds'), require.resolve('eslint-config-kentcdodds'),
require.resolve('eslint-config-kentcdodds/jest'), require.resolve('eslint-config-kentcdodds/jest'),
], ],
rules: { rules: {
'func-names': 0, 'func-names': 0,
'babel/camelcase': 0, 'babel/camelcase': 0,
'import/extensions': 0, 'import/extensions': 0,
'consistent-return': 0, 'consistent-return': 0,
'no-process-exit': 0, 'no-process-exit': 0,
} 'no-continue': 0,
},
} }

View file

@ -43,6 +43,7 @@
"homepage": "https://github.com/all-contributors/all-contributors-cli#readme", "homepage": "https://github.com/all-contributors/all-contributors-cli#readme",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.6", "@babel/runtime": "^7.7.6",
"ac-learn": "^1.5.1",
"async": "^3.1.0", "async": "^3.1.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"ac-learn": "^1.0.3", "ac-learn": "^1.0.3",
@ -51,7 +52,7 @@
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
"json-fixer": "^1.6.8", "json-fixer": "^1.6.8",
"lodash": "^4.11.2", "lodash": "^4.11.2",
"name-your-contributors": "^3.4.0", "name-your-contributors": "^3.8.3",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"pify": "^5.0.0", "pify": "^5.0.0",
"yargs": "^15.0.1", "yargs": "^15.0.1",

View file

@ -12,7 +12,7 @@ const util = require('./util')
const repo = require('./repo') const repo = require('./repo')
const updateContributors = require('./contributors') const updateContributors = require('./contributors')
const {getContributors} = require('./discover') const {getContributors} = require('./discover')
const learner = require('./discover/learner') const {getLearner} = require('./discover/learner')
const cwd = process.cwd() const cwd = process.cwd()
const defaultRCFile = path.join(cwd, '.all-contributorsrc') const defaultRCFile = path.join(cwd, '.all-contributorsrc')
@ -51,10 +51,26 @@ const yargv = yargs
} }
}).argv }).argv
function onError(error) {
if (error) {
console.error(error.message)
process.exit(1)
}
process.exit(0)
}
function suggestCommands(cmd) {
const availableCommands = ['generate', 'add', 'init', 'check']
const suggestion = didYouMean(cmd, availableCommands)
if (suggestion) console.log(chalk.bold(`Did you mean ${suggestion}`))
}
function startGeneration(argv) { function startGeneration(argv) {
return Promise.all( return Promise.all(
argv.files.map(file => { argv.files.map(file => {
const filePath = path.join(cwd, file) const filePath = path.join(cwd, file)
return util.markdown.read(filePath).then(fileContent => { return util.markdown.read(filePath).then(fileContent => {
const newFileContent = generate(argv, argv.contributors, fileContent) const newFileContent = generate(argv, argv.contributors, fileContent)
return util.markdown.write(filePath, newFileContent) return util.markdown.write(filePath, newFileContent)
@ -63,7 +79,7 @@ function startGeneration(argv) {
) )
} }
function addContribution(argv) { async function addContribution(argv) {
util.configFile.readConfig(argv.config) // ensure the config file exists util.configFile.readConfig(argv.config) // ensure the config file exists
const username = argv._[1] === undefined ? undefined : String(argv._[1]) const username = argv._[1] === undefined ? undefined : String(argv._[1])
/* Example: (for clarity & debugging purposes) /* Example: (for clarity & debugging purposes)
@ -85,204 +101,188 @@ function addContribution(argv) {
} }
*/ */
const contributions = argv._[2] const contributions = argv._[2]
// Add or update contributor in the config file // Add or update contributor in the config file
return updateContributors(argv, username, contributions).then( let data
data => {
argv.contributors = data.contributors try {
/* Example data = await updateContributors(argv, username, contributions)
[ { login: 'Berkmann18', } catch (error) {
name: 'Maximilian Berkmann', return console.error('Contributor Update fail:', error)
avatar_url: 'https://avatars0.githubusercontent.com/u/8260834?v=4', }
profile: 'http://maxcubing.wordpress.com',
contributions: [ 'code', 'ideas' ] }, argv.contributors = data.contributors
{ already in argv.contributors } ]
*/ /* Example
return startGeneration(argv).then( [ { login: 'Berkmann18',
() => { name: 'Maximilian Berkmann',
if (argv.commit) { avatar_url: 'https://avatars0.githubusercontent.com/u/8260834?v=4',
return util.git.commit(argv, data) profile: 'http://maxcubing.wordpress.com',
} contributions: [ 'code', 'ideas' ] },
}, { already in argv.contributors } ]
err => console.error('Generation fail:', err), */
)
}, try {
err => console.error('Contributor Update fail:', err), await startGeneration(argv)
)
return argv.commit ? util.git.commit(argv, data) : null
} catch (error) {
console.error('Generation fail:', error)
}
} }
function checkContributors(argv) { async function checkContributors(argv) {
const configData = util.configFile.readConfig(argv.config) const configData = util.configFile.readConfig(argv.config)
return repo const repoContributors = await repo.getContributors(
.getContributors( configData.projectOwner,
configData.projectOwner, configData.projectName,
configData.projectName, configData.repoType,
configData.repoType, configData.repoHost,
configData.repoHost, )
const checkKey = repo.getCheckKey(configData.repoType)
const knownContributions = configData.contributors.reduce((obj, item) => {
obj[item[checkKey]] = item.contributions
return obj
}, {})
const knownContributors = configData.contributors.map(
contributor => contributor[checkKey],
)
const missingInConfig = repoContributors.filter(
key => !knownContributors.includes(key),
)
const missingFromRepo = knownContributors.filter(key => {
return (
!repoContributors.includes(key) &&
(knownContributions[key].includes('code') ||
knownContributions[key].includes('test'))
) )
.then(repoContributors => { })
// console.dir(repoContributors) //['jfmengels', 'jakebolam', ...]
const checkKey = repo.getCheckKey(configData.repoType)
const knownContributions = configData.contributors.reduce((obj, item) => {
obj[item[checkKey]] = item.contributions
return obj
}, {})
const knownContributors = configData.contributors.map(
contributor => contributor[checkKey],
)
const missingInConfig = repoContributors.filter( if (missingInConfig.length) {
key => !knownContributors.includes(key), process.stdout.write(
) chalk.bold('Missing contributors in .all-contributorsrc:\n'),
const missingFromRepo = knownContributors.filter(key => { )
return ( process.stdout.write(` ${missingInConfig.join(', ')}\n`)
!repoContributors.includes(key) && }
(knownContributions[key].includes('code') ||
knownContributions[key].includes('test'))
)
})
if (missingInConfig.length) { if (missingFromRepo.length) {
process.stdout.write( process.stdout.write(
chalk.bold('Missing contributors in .all-contributorsrc:\n'), chalk.bold('Unknown contributors found in .all-contributorsrc:\n'),
) )
process.stdout.write(` ${missingInConfig.join(', ')}\n`) process.stdout.write(`${missingFromRepo.join(', ')}\n`)
} }
if (missingFromRepo.length) {
process.stdout.write(
chalk.bold('Unknown contributors found in .all-contributorsrc:\n'),
)
process.stdout.write(`${missingFromRepo.join(', ')}\n`)
}
})
} }
function fetchContributors(argv) { async function fetchContributors(argv) {
// console.log('argv=', argv); const {reviewers, commitAuthors, issueCreators} = await getContributors(
// const configData = util.configFile.readConfig(argv.config) argv.projectOwner,
// console.log('configData') argv.projectName,
// console.dir(configData) )
const args = {...argv, _: []}
const contributorsToAdd = []
const learner = await getLearner()
return getContributors(argv.projectOwner, argv.projectName).then( reviewers.forEach(usr => {
repoContributors => { contributorsToAdd.push({login: usr.login, contributions: ['review']})
// repoContributors = {prCreators, prCommentators, issueCreators, issueCommentators, reviewers, commitAuthors, commitCommentators}
// console.dir(repoContributors)
// const checkKey = repo.getCheckKey(configData.repoType) console.log(
// const knownContributions = configData.contributors.reduce((obj, item) => { `Adding ${chalk.underline('Reviewer')} ${chalk.blue(usr.login)}`,
// obj[item[checkKey]] = item.contributions )
// return obj })
// }, {})
// console.log('knownContributions', knownContributions) //{ jfmengels: ['code', 'test', 'doc'], ...}
// const knownContributors = configData.contributors.map(
// contributor => contributor[checkKey],
// )
// console.log('knownContributors', knownContributors) //['kentcdodds', 'ben-eb', ...]
// let contributors = new Set( issueCreators.forEach(usr => {
// repoContributors.prCreators.map(usr => usr.login), const contributor = {
// ) login: usr.login,
contributions: [],
}
// repoContributors.issueCreators.forEach(usr => contributors.add(usr.login)) usr.labels.forEach(lbl => {
// repoContributors.reviewers.forEach(usr => contributors.add(usr.login)) const guessedCategory = learner
// repoContributors.commitAuthors.forEach(usr => contributors.add(usr.login)) .classify(lbl)
// contributors = Array.from(contributors) .find(ctr => ctr && ctr !== 'null')
// console.log('ctbs=', contributors); if (!guessedCategory) {
console.warn(
//~1. Auto-add reviewers for review~ `Oops, I couldn't find any category for the "${lbl}" label`,
//~2. Auto-add issue creators for any categories found~
//~3. Auto-add commit authors~
//4. Roll onto other contribution categories following https://www.draw.io/#G1uL9saIuZl3rj8sOo9xsLOPByAe28qhwa
const args = {...argv, _: []}
const contributorsToAdd = []
repoContributors.reviewers.forEach(usr => {
// args._ = ['add', usr.login, 'review']
// addContribution(args)
contributorsToAdd.push({login: usr.login, contributions: ['review']})
// console.log(
// `Adding ${chalk.underline('Reviewer')} ${chalk.blue(usr.login)}`,
// )
})
repoContributors.issueCreators.forEach(usr => {
// console.log('usr=', usr.login, 'labels=', usr.labels)
const contributor = {
login: usr.login,
contributions: [],
}
usr.labels.forEach(lbl => {
const guesses = learner.classify(lbl).filter(c => c && c !== 'null')
if (guesses.length) {
const category = guesses[0]
// args._ = ['', usr.login, category]
// addContribution(args)
if (!contributor.contributions.includes(category))
contributor.contributions.push(category)
// console.log(
// `Adding ${chalk.blue(usr.login)} for ${chalk.underline(category)}`,
// )
} //else console.warn(`Oops, I couldn't find any category for the "${lbl}" label`)
})
const existingContributor = contributorsToAdd.filter(
ctrb => ctrb.login === usr.login,
) )
if (existingContributor.length) {
existingContributor[0].contributions = [
...new Set(
existingContributor[0].contributions.concat(
contributor.contributions,
),
),
]
} else contributorsToAdd.push(contributor)
})
repoContributors.commitAuthors.forEach(usr => { return
// const contributor = { }
// login: usr.login,
// contributions: [], if (!contributor.contributions.includes(guessedCategory)) {
// } contributor.contributions.push(guessedCategory)
// console.log('commit auth:', usr)
const existingContributor = contributorsToAdd.filter(
ctrb => ctrb.login === usr.login,
)
if (existingContributor.length) {
//there's no label or commit message info so use only code for now
if (!existingContributor[0].contributions.includes('code')) {
existingContributor[0].contributions.push('code')
}
} else
contributorsToAdd.push({login: usr.login, contributions: ['code']})
})
// console.log('contributorsToAdd=', contributorsToAdd)
contributorsToAdd.forEach(contributor => {
console.log( console.log(
`Adding ${chalk.blue(contributor.login)} for ${chalk.underline( `Adding ${chalk.blue(usr.login)} for ${chalk.underline(
contributor.contributions.join('/'), guessedCategory,
)}`, )}`,
) )
args._ = ['', contributor.login, contributor.contributions.join(',')] }
// if (contributor.contributions.length) addContribution(args) })
// else console.log('Skipping', contributor.login)
})
},
err => console.error('fetch error:', err),
)
}
function onError(error) { const existingContributor = contributorsToAdd.find(
if (error) { ctr => ctr.login === usr.login,
console.error(error.stack || error.message || error) )
process.exit(1)
if (existingContributor) {
existingContributor.contributions.push(...contributor.contributions)
} else {
contributorsToAdd.push(contributor)
}
})
commitAuthors.forEach(usr => {
const existingContributor = contributorsToAdd.find(
ctr => ctr.login === usr.login,
)
if (existingContributor) {
// There's no label or commit message info so use only code for now
if (!existingContributor.contributions.includes('code')) {
existingContributor.contributions.push('code')
}
} else {
contributorsToAdd.push({login: usr.login, contributions: ['code']})
}
})
// TODO: Roll onto other contribution categories following https://www.draw.io/#G1uL9saIuZl3rj8sOo9xsLOPByAe28qhwa
for (const contributor of contributorsToAdd) {
if (!contributor.contributions.length) {
console.log('Skipping', contributor.login)
continue
}
// Format contributor contributions
const contributions = contributor.contributions.join('/')
console.log(
`Adding ${chalk.blue(contributor.login)} for ${chalk.underline(
contributions,
)}`,
)
args._ = ['', contributor.login, contributor.contributions.join(',')]
try {
/* eslint-disable no-await-in-loop */
await addContribution(args)
} catch (error) {
console.error(
`Adding ${chalk.blue(contributor.login)} for ${chalk.underline(
contributions,
)} Failed: ${JSON.stringify(error)}`,
)
}
} }
process.exit(0)
} }
function promptForCommand(argv) { async function promptForCommand(argv) {
const questions = [ const questions = [
{ {
type: 'list', type: 'list',
@ -312,9 +312,9 @@ function promptForCommand(argv) {
}, },
] ]
return inquirer.prompt(questions).then(answers => { const answers = await inquirer.prompt(questions)
return answers.command || argv._[0]
}) return answers.command || argv._[0]
} }
promptForCommand(yargv) promptForCommand(yargv)

View file

@ -4,15 +4,18 @@ const {Spinner} = require('clui')
const privateToken = (process.env && process.env.PRIVATE_TOKEN) || '' const privateToken = (process.env && process.env.PRIVATE_TOKEN) || ''
const loader = new Spinner('Loading...') const loader = new Spinner('Loading...')
const getContributors = function(owner, name, token = privateToken) { const getContributors = async function(owner, name, token = privateToken) {
loader.start() loader.start()
const contributors = nyc.repoContributors({
const contributors = await nyc.repoContributors({
token, token,
user: owner, user: owner,
repo: name, repo: name,
commits: true, commits: true,
}) })
loader.stop() loader.stop()
return contributors return contributors
} }

View file

@ -3,21 +3,26 @@ const Learner = require('ac-learn')
const JSON_PATH = `${__dirname}/learner.json` const JSON_PATH = `${__dirname}/learner.json`
//@TODO: Use the JSON methods from `ac-learn` to get the whole thing saveable async function getLearner() {
const learner = new Learner() const learner = new Learner()
/* eslint-disable no-console */
if (existsSync(JSON_PATH)) {
learner.loadAndDeserializeClassifier(JSON_PATH).then(classifier => {
learner.classifier = classifier
// console.log('Re-using existing classifier')
}, console.error)
} else {
learner.crossValidate(6)
learner.eval()
learner.serializeAndSaveClassifier(JSON_PATH).then(_ => {
// console.log('Classifier saved', classifier)
}, console.error)
}
/* eslint-enable no-console */
module.exports = learner try {
if (existsSync(JSON_PATH)) {
learner.classifier = await learner.loadAndDeserializeClassifier(JSON_PATH)
} else {
learner.crossValidate(6)
learner.eval()
await learner.serializeAndSaveClassifier(JSON_PATH)
}
} catch (e) {
/* eslint-disable no-console */
console.error(e)
}
return learner
}
module.exports = {
getLearner,
}