-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
694103d
commit ca9516c
Showing
7 changed files
with
378 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
#!/usr/bin/env node | ||
|
||
'use strict'; | ||
|
||
const SEARCH_ISSUE = 'SearchIssue'; | ||
|
||
const Request = require('../lib/request'); | ||
const auth = require('../lib/auth'); | ||
const { runPromise } = require('../lib/run'); | ||
const CLI = require('../lib/cli'); | ||
const ContributionAnalyzer = require('../lib/contribution-analyzer'); | ||
|
||
const yargs = require('yargs'); | ||
const argv = yargs | ||
.command({ | ||
command: 'collaborators', | ||
desc: 'Getting contributions from collaborators', | ||
handler: handler | ||
}) | ||
.command({ | ||
command: 'tsc', | ||
desc: 'Getting contributions from TSC members', | ||
handler: handler | ||
}) | ||
.command({ | ||
command: 'for <ids..>', | ||
desc: 'Getting contributions from GitHub handles', | ||
builder: (yargs) => { | ||
yargs | ||
.positional('ids', { | ||
describe: 'GitHub handles', | ||
type: 'array' | ||
}); | ||
}, | ||
handler: handler | ||
}) | ||
.string('repo') | ||
.string('owner') | ||
.string('type') | ||
.string('branch') | ||
.string('readme') | ||
.default({ | ||
repo: 'node', | ||
owner: 'nodejs', | ||
type: 'participation', | ||
branch: 'master' | ||
}) | ||
.demandCommand(1, 'must provide a valid command') | ||
.help() | ||
.argv; | ||
|
||
async function main(argv) { | ||
const cli = new CLI(); | ||
const credentials = await auth(); | ||
const request = new Request(credentials); | ||
const analyzer = new ContributionAnalyzer(request, cli, argv); | ||
|
||
const [ command ] = argv._; | ||
switch (command) { | ||
case 'collaborators': | ||
const collaborators = await analyzer.getCollaborators(); | ||
await analyzer.getLatestContributionForIds( | ||
collaborators.map(user => user.login)); | ||
break; | ||
case 'tsc': | ||
const tsc = await analyzer.getTSC(); | ||
await analyzer.getLatestContributionForIds(tsc.map(user => user.login)); | ||
break; | ||
case 'for': | ||
await analyzer.getLatestContributionForIds(argv.ids); | ||
break; | ||
default: | ||
throw new Error(`Unknown command ${command}`); | ||
} | ||
} | ||
|
||
function handler(argv) { | ||
runPromise(main(argv)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
'use strict'; | ||
|
||
const SEARCH_ISSUE = 'SearchIssue'; | ||
const SEARCH_COMMIT = 'SearchCommit'; | ||
const USER = 'User'; | ||
|
||
const fs = require('fs'); | ||
const path = require('path'); | ||
const { writeJson, readJson, readFile } = require('./file'); | ||
const { getCollaborators } = require('./collaborators'); | ||
const { isTheSamePerson } = require('./user'); | ||
|
||
class ContributionAnalyzer { | ||
constructor(request, cli, argv) { | ||
this.request = request; | ||
this.cli = cli; | ||
this.argv = argv; | ||
const cacheDir = path.resolve(__dirname, '..', '.ncu', | ||
'cache'); | ||
const type = this.argv.type; | ||
this.cache = { | ||
temp: path.join(cacheDir, `${type}.json`), | ||
result: path.join(cacheDir, `${type}-sorted.json`) | ||
}; | ||
} | ||
|
||
participationByUser(issue, user) { | ||
const result = { | ||
url: issue.url, | ||
date: new Date(issue.publishedAt).toISOString(), | ||
isRelavent: false, | ||
type: '' | ||
}; | ||
|
||
// Author | ||
if (isTheSamePerson(issue.author, user)) { | ||
result.date = issue.publishedAt; | ||
result.isRelavent = true; | ||
result.type = /pull/.test(issue.url) ? 'pull' : 'issue'; | ||
} | ||
|
||
if (issue.reviews) { | ||
issue.reviews.nodes.forEach((review) => { | ||
if (!isTheSamePerson(review.author, user)) { | ||
return; | ||
} | ||
|
||
result.isRelavent = true; | ||
if (review.publishedAt > result.date) { | ||
result.date = review.publishedAt; | ||
result.type = 'review'; | ||
} | ||
}); | ||
} | ||
|
||
issue.comments.nodes.forEach((comment) => { | ||
if (!isTheSamePerson(comment.author, user)) { | ||
return; | ||
} | ||
|
||
result.isRelavent = true; | ||
if (comment.publishedAt > result.date) { | ||
result.date = comment.publishedAt; | ||
result.type = 'comment'; | ||
} | ||
}); | ||
|
||
return result; | ||
} | ||
|
||
async getParticipation(user) { | ||
const { cli, request } = this; | ||
const results = await request.gql(SEARCH_ISSUE, { | ||
queryString: `involves:${user} org:nodejs`, | ||
mustBeAuthor: false | ||
}); | ||
if (results.search.nodes.length === 0) { | ||
return [{ | ||
url: 'N/A', | ||
date: 'N/A', | ||
isRelavent: false, | ||
type: 'N/A' | ||
}]; | ||
} | ||
|
||
const res = results.search.nodes | ||
.map(issue => this.participationByUser(issue, user)) | ||
// .filter((res) => res.isRelavent) | ||
.sort((a, b) => { | ||
return a.date > b.date ? -1 : 1; | ||
}); | ||
return res; | ||
} | ||
|
||
async getCommits(user) { | ||
const { cli, request, argv } = this; | ||
const { owner, repo, branch } = argv; | ||
|
||
const userData = await request.gql(USER, { login: user }); | ||
const authorId = userData.user.id; | ||
const results = await request.gql(SEARCH_COMMIT, { | ||
owner, repo, branch, authorId | ||
}, [ 'repository', 'ref', 'target', 'history' ]); | ||
return results | ||
.sort((a, b) => { | ||
return a.authoredDate > b.authoredDate ? -1 : 1; | ||
}); | ||
} | ||
|
||
async getContributionsForId(user) { | ||
const { argv } = this; | ||
if (argv.type === 'participation') { | ||
return this.getParticipation(user); | ||
} else if (argv.type === 'commit') { | ||
return this.getCommits(user); | ||
} | ||
} | ||
|
||
async getLatestContributionForId(user) { | ||
const contributions = await this.getContributionsForId(user); | ||
return contributions[0] || { | ||
url: 'N/A', | ||
date: 'N/A', | ||
isRelavent: false, | ||
type: 'N/A' | ||
}; | ||
} | ||
|
||
async getCollaborators() { | ||
const { cli, request, argv } = this; | ||
const { owner, repo, readme } = argv; | ||
cli.startSpinner('Getting collaborator contacts'); | ||
let readmeText; | ||
if (readme) { | ||
cli.updateSpinner(`Reading collaborator contacts from ${readme}`); | ||
readmeText = readFile(readme); | ||
} else { | ||
cli.updateSpinner( | ||
`Getting collaborator contacts from README of ${owner}/${repo}`); | ||
const url = `https://raw.githubusercontent.com/${owner}/${repo}/master/README.md`; | ||
readmeText = await request.text(url); | ||
} | ||
const collaborators = getCollaborators(readmeText, cli, owner, repo); | ||
const result = Array.from(collaborators.values()); | ||
cli.stopSpinner(`Read ${result.length} collaborators`); | ||
return result; | ||
} | ||
|
||
async getTSC() { | ||
const collaborators = await this.getCollaborators(); | ||
return collaborators.filter((user) => user.isTSC()); | ||
} | ||
|
||
formatContribution(user, data) { | ||
if (this.argv.type === 'participation') { | ||
const type = | ||
data.type.padEnd(8).toUpperCase(); | ||
const date = data.date.slice(0, 10); | ||
return `${date} ${type} @${user.padEnd(22)} ${data.url}`; | ||
} else if (this.argv.type === 'commit') { | ||
const hash = data.oid.slice(0, 7); | ||
const date = data.authoredDate.slice(0, 10); | ||
const message = data.messageHeadline; | ||
return `${date} ${hash} @${user.padEnd(22)} ${message}`; | ||
} | ||
} | ||
|
||
getResult(user, data) { | ||
if (this.argv.type === 'participation') { | ||
return { | ||
user, | ||
date: data.date, | ||
url: data.url, | ||
type: data.type | ||
}; | ||
} else if (this.argv.type === 'commit') { | ||
return { | ||
user, | ||
authoredDate: data.authoredDate, | ||
messageHeadline: data.messageHeadline, | ||
oid: data.oid | ||
}; | ||
} | ||
} | ||
|
||
getDate(item) { | ||
if (this.argv.type === 'participation') { | ||
return item.date; | ||
} else if (this.argv.type === 'commit') { | ||
return item.authoredDate; | ||
} | ||
} | ||
async getResults(ids) { | ||
const { cache, cli, argv, request } = this; | ||
|
||
let latestContrib = cache ? readJson(cache.temp) : {}; | ||
const total = ids.length; | ||
let counter = 1; | ||
|
||
for (const user of ids) { | ||
cli.startSpinner(`Grabbing data for @${user}, ${counter++}/${total}..`); | ||
let data = latestContrib[user]; | ||
if (data) { | ||
cli.updateSpinner(`Skip grabbing cached data for @${user}`); | ||
} else { | ||
data = await this.getLatestContributionForId(user); | ||
latestContrib[user] = data; | ||
if (cache && counter % 11 === 10) { | ||
writeJson(cache.temp, latestContrib); | ||
} | ||
} | ||
|
||
cli.stopSpinner(this.formatContribution(user, data)); | ||
} | ||
|
||
if (cache) { | ||
writeJson(cache.temp, latestContrib); | ||
} | ||
|
||
const sorted = ids.sort((a, b) => { | ||
const aa = latestContrib[a]; | ||
const bb = latestContrib[b]; | ||
return this.getDate(aa) < this.getDate(bb) ? -1 : 1; | ||
}).map((user) => { | ||
const data = latestContrib[user]; | ||
return this.getResult(user, data); | ||
}); | ||
return sorted; | ||
} | ||
|
||
async getLatestContributionForIds(ids) { | ||
const { cache, cli, argv, request } = this; | ||
let sorted; | ||
|
||
if (this.cache && fs.existsSync(this.cache.result)) { | ||
sorted = readJson(this.cache.result); | ||
const existingIds = new Set(sorted.map(item => item.user)); | ||
if (ids.every(id => existingIds.has(id))) { | ||
for (const data of sorted) { | ||
cli.log(this.formatContribution(data.user, data)); | ||
} | ||
return; | ||
} | ||
} | ||
|
||
sorted = await this.getResults(ids); | ||
|
||
if (cache) { | ||
writeJson(cache.result, sorted); | ||
} | ||
} | ||
} | ||
|
||
module.exports = ContributionAnalyzer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
query SearchCommitByAuthor($repo: String!, $owner: String!, $branch: String!, $authorId: ID!, $after: String) { | ||
repository(name: $repo, owner: $owner) { | ||
ref(qualifiedName: $branch) { | ||
target { | ||
... on Commit { | ||
history(first: 100, after: $after, author: { id: $authorId } ) { | ||
totalCount | ||
pageInfo { hasNextPage, endCursor } | ||
nodes { | ||
oid | ||
authoredDate | ||
messageHeadline | ||
author { | ||
name | ||
user { | ||
login | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.