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 = {
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,
},
}

View file

@ -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",

View file

@ -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 (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`)
}
})
if (missingFromRepo.length) {
process.stdout.write(
chalk.bold('Unknown contributors found in .all-contributorsrc:\n'),
)
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)
async function fetchContributors(argv) {
const {reviewers, commitAuthors, issueCreators} = await getContributors(
argv.projectOwner,
argv.projectName,
)
const args = {...argv, _: []}
const contributorsToAdd = []
const learner = await getLearner()
return getContributors(argv.projectOwner, argv.projectName).then(
repoContributors => {
// repoContributors = {prCreators, prCommentators, issueCreators, issueCommentators, reviewers, commitAuthors, commitCommentators}
// console.dir(repoContributors)
reviewers.forEach(usr => {
contributorsToAdd.push({login: usr.login, contributions: ['review']})
// 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', ...]
console.log(
`Adding ${chalk.underline('Reviewer')} ${chalk.blue(usr.login)}`,
)
})
// let contributors = new Set(
// repoContributors.prCreators.map(usr => usr.login),
// )
issueCreators.forEach(usr => {
const contributor = {
login: usr.login,
contributions: [],
}
// 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)
usr.labels.forEach(lbl => {
const guessedCategory = learner
.classify(lbl)
.find(ctr => ctr && ctr !== 'null')
// 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)}`,
// )
})
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 (!guessedCategory) {
console.warn(
`Oops, I couldn't find any category for the "${lbl}" label`,
)
if (existingContributor.length) {
existingContributor[0].contributions = [
...new Set(
existingContributor[0].contributions.concat(
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']})
})
return
}
if (!contributor.contributions.includes(guessedCategory)) {
contributor.contributions.push(guessedCategory)
// console.log('contributorsToAdd=', contributorsToAdd)
contributorsToAdd.forEach(contributor => {
console.log(
`Adding ${chalk.blue(contributor.login)} for ${chalk.underline(
contributor.contributions.join('/'),
`Adding ${chalk.blue(usr.login)} for ${chalk.underline(
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) {
if (error) {
console.error(error.stack || error.message || error)
process.exit(1)
const existingContributor = contributorsToAdd.find(
ctr => ctr.login === usr.login,
)
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 = [
{
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)

View file

@ -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
}

View file

@ -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)
}
/* eslint-enable no-console */
async function getLearner() {
const learner = new Learner()
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,
}