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 May 14, 2018
1 parent 6b5223d commit bfa287b
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 4 deletions.
84 changes: 84 additions & 0 deletions bin/ncu-contrib
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env node

'use strict';

const Request = require('../lib/request');
const auth = require('../lib/auth');
const { writeFile } = require('../lib/file');
const { runPromise } = require('../lib/run');
const CLI = require('../lib/cli');
const ContributionAnalyzer = require('../lib/contribution-analyzer');

const yargs = require('yargs');
// eslint-disable-next-line no-unused-vars
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')
.string('output')
.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 config = require('../lib/config').getMergedConfig();
argv = Object.assign(argv, config);
const analyzer = new ContributionAnalyzer(request, cli, argv);

const [ command ] = argv._;
let result;
switch (command) {
case 'collaborators':
result = await analyzer.getLatestContributionForCollaborators();
break;
case 'tsc':
result = await analyzer.getLatestContributionForTSC();
break;
case 'for':
result = await analyzer.getLatestContributionForIds(argv.ids);
break;
default:
throw new Error(`Unknown command ${command}`);
}
if (argv.output) {
const txt = analyzer.formatContributionList(result);
writeFile(argv.output, txt);
}
}

function handler(argv) {
runPromise(main(argv));
}
223 changes: 223 additions & 0 deletions lib/contribution-analyzer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
'use strict';

const SEARCH_ISSUE = 'SearchIssue';
const SEARCH_COMMIT = 'SearchCommit';
const USER = 'User';

const { getCollaborators } = require('./collaborators');
const Cache = require('./cache');
const { ascending } = require('./comp');
const { isTheSamePerson } = require('./user');

class ContributionAnalyzer {
constructor(request, cli, argv) {
this.request = request;
this.cli = cli;
this.argv = argv;
}

async getCommits(user) {
const { 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 getParticipation(user) {
const { request } = this;
const results = await request.gql(SEARCH_ISSUE, {
queryString: `involves:${user} repo:nodejs/node`,
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;
}

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 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);
if (contributions.length) {
return Object.assign({ user }, contributions[0]);
}
return {
user: user,
url: 'N/A',
date: 'N/A',
isRelavent: false,
type: 'N/A'
};
}

formatContribution(data) {
if (this.argv.type === 'participation') {
const type =
data.type.padEnd(8).toUpperCase();
const date = data.date.slice(0, 10);
return `${date} ${type} @${data.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} @${data.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 getLatestContributionForIds(ids) {
const { cli } = this;
const total = ids.length;
let counter = 1;
const latestContrib = {};
for (const user of ids) {
cli.startSpinner(`Grabbing data for @${user}, ${counter++}/${total}..`);
const data = await this.getLatestContributionForId(user);
latestContrib[user] = data;
cli.stopSpinner(this.formatContribution(data));
}

const sorted = ids.sort((a, b) => {
const aa = latestContrib[a];
const bb = latestContrib[b];
return ascending(this.getDate(aa), this.getDate(bb));
}).map((user) => {
const data = latestContrib[user];
return this.getResult(user, data);
});
return sorted;
}

formatContributionList(list) {
let txt = '';
for (const item of list) {
txt += this.formatContribution(item) + '\n';
}
return txt;
}

async getLatestContributionForCollaborators() {
const { cli, argv, request } = this;
const collaborators = await getCollaborators(cli, request, argv);
const ids = [...collaborators.keys()];
return this.getLatestContributionForIds(ids);
}

async getLatestContributionForTSC() {
const { cli, argv, request } = this;
const collaborators = await getCollaborators(cli, request, argv);
const tsc = [...collaborators.values()].filter((user) => user.isTSC());
const ids = tsc.map(user => user.login);
return this.getLatestContributionForIds(ids);
}
}

const contribCache = new Cache();
contribCache.wrap(ContributionAnalyzer, {
getCommits(user) {
return { key: `commits-${user}`, ext: '.json' };
},
getParticipation(user) {
return { key: `participation-${user}`, ext: '.json' };
}
});
contribCache.enable();

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
}
}
}
}
}
}
}
}
}
13 changes: 9 additions & 4 deletions lib/queries/SearchIssue.gql
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
query SearchIssueByUser($queryString: String!, $isCommenter: Boolean!, $after: String) {
query SearchIssueByUser($queryString: String!, $mustBeAuthor: Boolean!, $after: String) {
search(query: $queryString, type: ISSUE, first: 100, after: $after) {
issueCount
pageInfo {
hasNextPage
endCursor
}
nodes {
... on PullRequest {
url
Expand All @@ -8,7 +13,7 @@ query SearchIssueByUser($queryString: String!, $isCommenter: Boolean!, $after: S
login
}
title
reviews(last: 100) @include(if: $isCommenter) {
reviews(last: 100) @skip(if: $mustBeAuthor) {
nodes {
publishedAt
author {
Expand All @@ -21,7 +26,7 @@ query SearchIssueByUser($queryString: String!, $isCommenter: Boolean!, $after: S
name
}
}
comments(last: 100) @include(if: $isCommenter) {
comments(last: 100) @skip(if: $mustBeAuthor) {
nodes {
publishedAt
author {
Expand All @@ -37,7 +42,7 @@ query SearchIssueByUser($queryString: String!, $isCommenter: Boolean!, $after: S
login
}
title
comments(last: 100) @include(if: $isCommenter) {
comments(last: 100) @skip(if: $mustBeAuthor) {
nodes {
publishedAt
author {
Expand Down
7 changes: 7 additions & 0 deletions lib/queries/User.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query User($login: String!) {
user(login: $login) {
login,
email,
id
}
}
1 change: 1 addition & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Request {
this.credentials = credentials;
}

// TODO: cache this
loadQuery(file) {
const filePath = path.resolve(__dirname, 'queries', `${file}.gql`);
return fs.readFileSync(filePath, 'utf8');
Expand Down
Loading

0 comments on commit bfa287b

Please sign in to comment.