diff --git a/.eslintrc.js b/.eslintrc.js index bf679f97..b2dc3918 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,13 +1,14 @@ module.exports = { extends: [ - require.resolve('eslint-config-kentcdodds'), - require.resolve('eslint-config-kentcdodds/jest'), - ], + require.resolve('eslint-config-kentcdodds'), + require.resolve('eslint-config-kentcdodds/jest'), + ], rules: { 'func-names': 0, 'babel/camelcase': 0, 'import/extensions': 0, 'consistent-return': 0, 'no-process-exit': 0, - } + 'no-continue': 0, + }, } diff --git a/package.json b/package.json index 83beb017..5b4329db 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "homepage": "https://github.com/all-contributors/all-contributors-cli#readme", "dependencies": { "@babel/runtime": "^7.7.6", + "ac-learn": "^1.5.1", "async": "^3.1.0", "chalk": "^4.0.0", "ac-learn": "^1.0.3", @@ -51,7 +52,7 @@ "inquirer": "^7.3.3", "json-fixer": "^1.6.8", "lodash": "^4.11.2", - "name-your-contributors": "^3.4.0", + "name-your-contributors": "^3.8.3", "node-fetch": "^2.6.0", "pify": "^5.0.0", "yargs": "^15.0.1", diff --git a/src/cli.js b/src/cli.js index 434da67e..982afeed 100755 --- a/src/cli.js +++ b/src/cli.js @@ -12,7 +12,7 @@ const util = require('./util') const repo = require('./repo') const updateContributors = require('./contributors') const {getContributors} = require('./discover') -const learner = require('./discover/learner') +const {getLearner} = require('./discover/learner') const cwd = process.cwd() const defaultRCFile = path.join(cwd, '.all-contributorsrc') @@ -51,10 +51,26 @@ const yargv = yargs } }).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) { return Promise.all( argv.files.map(file => { const filePath = path.join(cwd, file) + return util.markdown.read(filePath).then(fileContent => { const newFileContent = generate(argv, argv.contributors, fileContent) return util.markdown.write(filePath, newFileContent) @@ -63,7 +79,7 @@ function startGeneration(argv) { ) } -function addContribution(argv) { +async function addContribution(argv) { util.configFile.readConfig(argv.config) // ensure the config file exists const username = argv._[1] === undefined ? undefined : String(argv._[1]) /* Example: (for clarity & debugging purposes) @@ -85,204 +101,188 @@ function addContribution(argv) { } */ const contributions = argv._[2] + // Add or update contributor in the config file - return updateContributors(argv, username, contributions).then( - data => { - argv.contributors = data.contributors - /* Example - [ { login: 'Berkmann18', - name: 'Maximilian Berkmann', - avatar_url: 'https://avatars0.githubusercontent.com/u/8260834?v=4', - profile: 'http://maxcubing.wordpress.com', - contributions: [ 'code', 'ideas' ] }, - { already in argv.contributors } ] - */ - return startGeneration(argv).then( - () => { - if (argv.commit) { - return util.git.commit(argv, data) - } - }, - err => console.error('Generation fail:', err), - ) - }, - err => console.error('Contributor Update fail:', err), - ) + let data + + try { + data = await updateContributors(argv, username, contributions) + } catch (error) { + return console.error('Contributor Update fail:', error) + } + + argv.contributors = data.contributors + + /* Example + [ { login: 'Berkmann18', + name: 'Maximilian Berkmann', + avatar_url: 'https://avatars0.githubusercontent.com/u/8260834?v=4', + profile: 'http://maxcubing.wordpress.com', + contributions: [ 'code', 'ideas' ] }, + { already in argv.contributors } ] + */ + + try { + 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) - return repo - .getContributors( - configData.projectOwner, - configData.projectName, - configData.repoType, - configData.repoHost, + const repoContributors = await repo.getContributors( + configData.projectOwner, + configData.projectName, + configData.repoType, + 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( - key => !knownContributors.includes(key), - ) - const missingFromRepo = knownContributors.filter(key => { - return ( - !repoContributors.includes(key) && - (knownContributions[key].includes('code') || - knownContributions[key].includes('test')) - ) - }) + if (missingInConfig.length) { + process.stdout.write( + chalk.bold('Missing contributors in .all-contributorsrc:\n'), + ) + process.stdout.write(` ${missingInConfig.join(', ')}\n`) + } + + if (missingFromRepo.length) { + process.stdout.write( + chalk.bold('Unknown contributors found in .all-contributorsrc:\n'), + ) + process.stdout.write(`${missingFromRepo.join(', ')}\n`) + } +} + +async function fetchContributors(argv) { + const {reviewers, commitAuthors, issueCreators} = await getContributors( + argv.projectOwner, + argv.projectName, + ) + const args = {...argv, _: []} + const contributorsToAdd = [] + const learner = await getLearner() + + reviewers.forEach(usr => { + contributorsToAdd.push({login: usr.login, contributions: ['review']}) + + console.log( + `Adding ${chalk.underline('Reviewer')} ${chalk.blue(usr.login)}`, + ) + }) - if (missingInConfig.length) { - process.stdout.write( - chalk.bold('Missing contributors in .all-contributorsrc:\n'), + issueCreators.forEach(usr => { + const contributor = { + login: usr.login, + contributions: [], + } + + usr.labels.forEach(lbl => { + const guessedCategory = learner + .classify(lbl) + .find(ctr => ctr && ctr !== 'null') + + if (!guessedCategory) { + console.warn( + `Oops, I couldn't find any category for the "${lbl}" label`, ) - process.stdout.write(` ${missingInConfig.join(', ')}\n`) + + return } - if (missingFromRepo.length) { - process.stdout.write( - chalk.bold('Unknown contributors found in .all-contributorsrc:\n'), + if (!contributor.contributions.includes(guessedCategory)) { + contributor.contributions.push(guessedCategory) + + console.log( + `Adding ${chalk.blue(usr.login)} for ${chalk.underline( + guessedCategory, + )}`, ) - process.stdout.write(`${missingFromRepo.join(', ')}\n`) } }) -} -function fetchContributors(argv) { - // console.log('argv=', argv); - // const configData = util.configFile.readConfig(argv.config) - // console.log('configData') - // console.dir(configData) - - return getContributors(argv.projectOwner, argv.projectName).then( - repoContributors => { - // repoContributors = {prCreators, prCommentators, issueCreators, issueCommentators, reviewers, commitAuthors, commitCommentators} - // console.dir(repoContributors) - - // const checkKey = repo.getCheckKey(configData.repoType) - // const knownContributions = configData.contributors.reduce((obj, item) => { - // 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( - // repoContributors.prCreators.map(usr => usr.login), - // ) - - // repoContributors.issueCreators.forEach(usr => contributors.add(usr.login)) - // repoContributors.reviewers.forEach(usr => contributors.add(usr.login)) - // repoContributors.commitAuthors.forEach(usr => contributors.add(usr.login)) - // contributors = Array.from(contributors) - - // console.log('ctbs=', contributors); - - //~1. Auto-add reviewers for review~ - //~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)}`, - // ) - }) + const existingContributor = contributorsToAdd.find( + ctr => ctr.login === 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) - }) + if (existingContributor) { + existingContributor.contributions.push(...contributor.contributions) + } else { + contributorsToAdd.push(contributor) + } + }) - repoContributors.commitAuthors.forEach(usr => { - // const contributor = { - // login: usr.login, - // contributions: [], - // } - // 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']}) - }) + commitAuthors.forEach(usr => { + const existingContributor = contributorsToAdd.find( + ctr => ctr.login === usr.login, + ) - // console.log('contributorsToAdd=', contributorsToAdd) - contributorsToAdd.forEach(contributor => { - console.log( - `Adding ${chalk.blue(contributor.login)} for ${chalk.underline( - contributor.contributions.join('/'), - )}`, - ) - args._ = ['', contributor.login, contributor.contributions.join(',')] - // if (contributor.contributions.length) addContribution(args) - // else console.log('Skipping', contributor.login) - }) - }, - err => console.error('fetch error:', err), - ) -} + 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']}) + } + }) -function onError(error) { - if (error) { - console.error(error.stack || error.message || error) - process.exit(1) + // 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 = [ { type: 'list', @@ -312,9 +312,9 @@ function promptForCommand(argv) { }, ] - return inquirer.prompt(questions).then(answers => { - return answers.command || argv._[0] - }) + const answers = await inquirer.prompt(questions) + + return answers.command || argv._[0] } promptForCommand(yargv) diff --git a/src/discover/index.js b/src/discover/index.js index 6c4ee396..4b69ea73 100644 --- a/src/discover/index.js +++ b/src/discover/index.js @@ -4,15 +4,18 @@ const {Spinner} = require('clui') const privateToken = (process.env && process.env.PRIVATE_TOKEN) || '' const loader = new Spinner('Loading...') -const getContributors = function(owner, name, token = privateToken) { +const getContributors = async function(owner, name, token = privateToken) { loader.start() - const contributors = nyc.repoContributors({ + + const contributors = await nyc.repoContributors({ token, user: owner, repo: name, commits: true, }) + loader.stop() + return contributors } diff --git a/src/discover/learner.js b/src/discover/learner.js index 5afe2121..3a19d859 100644 --- a/src/discover/learner.js +++ b/src/discover/learner.js @@ -3,21 +3,26 @@ const Learner = require('ac-learn') const JSON_PATH = `${__dirname}/learner.json` -//@TODO: Use the JSON methods from `ac-learn` to get the whole thing saveable -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) +async function getLearner() { + const learner = new 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 } -/* eslint-enable no-console */ -module.exports = learner +module.exports = { + getLearner, +}