Skip to content

Commit

Permalink
ncu-contrib: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
joyeecheung committed Apr 6, 2018
1 parent 694103d commit ca9516c
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 5 deletions.
79 changes: 79 additions & 0 deletions bin/ncu-contrib
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));
}
254 changes: 254 additions & 0 deletions lib/contribution-analyzer.js
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;
26 changes: 26 additions & 0 deletions lib/queries/SearchCommit.gql
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 {
email
name
user {
login
}
}
}
}
}
}
}
}
}
Loading

0 comments on commit ca9516c

Please sign in to comment.