github-stats-cli
Advanced tools
Comparing version 1.0.1 to 2.0.0
33
index.js
@@ -6,8 +6,11 @@ #!/usr/bin/env node | ||
const app = require('./package.json'); | ||
const { normalizeStates } = require('./src/utils'); | ||
const { getFromDate, getToDate } = require('./src/utils'); | ||
const getStats = require('./src/main'); | ||
const [cmdString] = Object.keys(app.bin); | ||
const defaults = { | ||
num: 10, | ||
states: 'OPEN', | ||
state: 'merged', | ||
fromDate: getFromDate(), | ||
toDate: getToDate(), | ||
}; | ||
@@ -29,13 +32,13 @@ | ||
\n | ||
Minimal usage - get last 10 PRs from facebook/react that are currently open: | ||
Minimal usage - get PRs from facebook/react repo merged in last 7 days, paginate 10 at a time and save to ./prdata.csv: | ||
\n | ||
$ ${app.name} -o facebook -r react | ||
$ ${cmdString} -o facebook -r react | ||
\n | ||
Get last 20 PRs that are currently merged, closed or open: | ||
Get PRs from facebook/react repo merged in last 7 days, paginate 20 at a time and save to ./prdata.csv: | ||
\n | ||
$ ${app.name} -o facebook -r react -n 20 -s MERGED,CLOSED,OPEN | ||
$ ${cmdString} -o facebook -r react -n 20 | ||
\n | ||
Get last 20 closed PRs and return only the ones that match user name bvaughn: | ||
Get PRs from facebook/react repo created on or after 2018-07-01 merged on or before 2018-09-30, paginate 20 at a time and save to ./prdata.csv: | ||
\n | ||
$ ${app.name} -o facebook -r react -n 20 -s MERGED -u bvaughn | ||
$ ${cmdString} -o facebook -r react -n 20 -u bvaughn -f 2018-07-01 -t 2018-09-30 | ||
\n | ||
@@ -51,9 +54,5 @@ `) | ||
.option('-u, --user <user>', 'optional author name', undefined) | ||
.option('-n, --num <num>', 'optional number of pull requests to return', defaults.num) | ||
.option( | ||
'-s, --states <states>', | ||
'comma separated MERGED|CLOSED|OPEN', | ||
defaults.states, | ||
normalizeStates | ||
) | ||
.option('-n, --num <num>', 'optional number of pull requests to return per page', defaults.num) | ||
.option('-f, --from <from>', 'YYYY-MM-DD date, e.g. 2018-12-21', defaults.fromDate) | ||
.option('-t, --to <to>', 'YYYY-MM-DD date, e.g. 2018-12-25', defaults.toDate) | ||
.parse(process.argv); | ||
@@ -75,5 +74,7 @@ | ||
num: Number(program.num), | ||
states: normalizeStates(program.states), | ||
state: defaults.state, | ||
fromDate: program.from, | ||
toDate: program.to, | ||
}; | ||
getStats(queryParams); |
{ | ||
"name": "github-stats-cli", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"description": "Github stats cli", | ||
@@ -28,6 +28,7 @@ "keywords": [ | ||
"graphql-client": "^2.0.1", | ||
"json2csv": "^4.2.1", | ||
"moment": "^2.22.1" | ||
}, | ||
"bin": { | ||
"github-stats-cli": "index.js" | ||
"ghs": "index.js" | ||
}, | ||
@@ -34,0 +35,0 @@ "devDependencies": { |
@@ -21,4 +21,5 @@ # github-stats-cli | ||
## Usage | ||
``` | ||
Usage: github-stats-cli [options] | ||
Usage: ghs [options] | ||
@@ -35,18 +36,18 @@ Options: | ||
Environment variables: | ||
GH_STATS_TOKEN set export GH_STATS_TOKEN=<your generated github token> | ||
Examples: | ||
Minimal usage - get last 10 PRs from facebook/react that are currently open: | ||
$ github-stats-cli -o facebook -r react | ||
Get last 20 PRs that are currently merged, closed or open: | ||
$ github-stats-cli -o facebook -r react -n 20 -s MERGED,CLOSED,OPEN | ||
Get last 20 closed PRs and return only the ones that match user name bvaughn: | ||
$ github-stats-cli -o facebook -r react -n 20 -s MERGED -u bvaughn | ||
Minimal usage - get PRs from facebook/react repo merged in last 7 days, paginate 10 at a time and save to ./prdata.csv: | ||
$ ghs -o facebook -r react | ||
Get PRs from facebook/react repo merged in last 7 days, paginate 20 at a time and save to ./prdata.csv: | ||
$ ghs -o facebook -r react -n 20 | ||
Get PRs from facebook/react repo created on or after 2018-07-01 merged on or before 2018-09-30, paginate 20 at a time and save to ./prdata.csv: | ||
$ ghs -o facebook -r react -n 20 -u bvaughn -f 2018-07-01 -t 2018-09-30 | ||
``` |
@@ -1,7 +0,5 @@ | ||
const { getRelativeDate, getDaysOpen } = require('./utils'); | ||
const { getRelativeDate, getHoursOpen } = require('./utils'); | ||
const getMappedPrData = ({ | ||
data: { repository: { pullRequests: { edges = [] } = {} } = {} } = {}, | ||
} = {}) => | ||
edges.map(({ node }) => { | ||
const getMappedPrData = ({ data: { search: { pageInfo, nodes = [] } = {} } = {} } = {}) => | ||
nodes.map(node => { | ||
const { | ||
@@ -13,6 +11,7 @@ merged, | ||
mergedAt, | ||
resourcePath, | ||
permalink, | ||
} = node; | ||
return Object.assign({}, node, { | ||
return { | ||
merged, | ||
author: login, | ||
@@ -22,13 +21,11 @@ createdAt: getRelativeDate(createdAt), | ||
mergedAt: getRelativeDate(mergedAt), | ||
resourcePath: `https://github.com${resourcePath}`, | ||
daysOpen: !merged ? getDaysOpen(createdAt, Date.now()) : getDaysOpen(createdAt, mergedAt), | ||
}); | ||
permalink, | ||
hoursOpen: !merged | ||
? getHoursOpen(createdAt, Date.now()).toFixed(2) | ||
: getHoursOpen(createdAt, mergedAt).toFixed(2), | ||
}; | ||
}); | ||
const filterByAuthor = (data, queriedAuthor) => | ||
data.filter(({ author }) => author === queriedAuthor); | ||
module.exports = { | ||
filterByAuthor, | ||
getMappedPrData, | ||
}; |
@@ -0,1 +1,2 @@ | ||
/* istanbul ignore file */ | ||
const gqlclient = require('graphql-client'); | ||
@@ -7,4 +8,4 @@ const { token } = require('../config'); | ||
headers: { | ||
Authorization: `Bearer ${token}` | ||
} | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}); |
const client = require('./graphqlClient'); | ||
const { getMappedPrData, filterByAuthor } = require('./dataTransformations'); | ||
const { printTable } = require('./utils'); | ||
const { getMappedPrData } = require('./dataTransformations'); | ||
const { appendToFile, constructSearchQueryString, printTable } = require('./utils'); | ||
const { pullRequestsQuery } = require('./queries'); | ||
const json2csv = require('json2csv').parse; | ||
module.exports = (queryParams) => { | ||
client | ||
.query(pullRequestsQuery, queryParams, (req, res) => { | ||
if (res.status === 401) { | ||
throw new Error('Not authorized'); | ||
} | ||
}) | ||
const getData = params => { | ||
return client.query(pullRequestsQuery, params, (req, res) => { | ||
if (res.status === 401) { | ||
throw new Error('Not authorized'); | ||
} | ||
}); | ||
}; | ||
const saveCsv = (data, options) => { | ||
const fields = [ | ||
'number', | ||
'permalink', | ||
'createdAt', | ||
'closedAt', | ||
'mergedAt', | ||
'merged', | ||
'title', | ||
'lastEditedAt', | ||
'additions', | ||
'deletions', | ||
'changedFiles', | ||
'commits.totalCount', | ||
'comments.totalCount', | ||
'author.login', | ||
]; | ||
try { | ||
const csv = json2csv(data, { fields, ...options }); | ||
appendToFile(csv); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
}; | ||
const request = queryParams => { | ||
const searchQueryString = constructSearchQueryString(queryParams); | ||
getData(searchQueryString) | ||
.then(body => { | ||
const mappedData = getMappedPrData(body); | ||
const { author: queriedAuthor } = queryParams; | ||
const { hasPreviousPage, hasNextPage, endCursor } = body.data.search.pageInfo; | ||
if (queriedAuthor) { | ||
printTable(filterByAuthor(mappedData, queriedAuthor)); | ||
saveCsv(body.data.search.nodes, { header: !hasPreviousPage }); | ||
printTable(mappedData); | ||
if (hasNextPage) { | ||
request({ ...queryParams, after: endCursor }); | ||
} else { | ||
printTable(mappedData); | ||
console.log('File saved to ./prdata.csv'); | ||
} | ||
@@ -27,1 +63,3 @@ }) | ||
}; | ||
module.exports = request; |
@@ -0,18 +1,33 @@ | ||
/* istanbul ignore file */ | ||
const pullRequestsQuery = ` | ||
query repo($org: String!, $repo: String!, $num: Int!, $states: [PullRequestState!]) { | ||
repository(owner: $org, name: $repo) { | ||
pullRequests(last: $num, states: $states) { | ||
edges { | ||
node { | ||
author { | ||
login | ||
}, | ||
title, | ||
createdAt, | ||
lastEditedAt, | ||
mergedAt, | ||
merged, | ||
mergeable, | ||
resourcePath, | ||
query searchMergedPrsQuery($after: String, $num: Int!, $query: String!) { | ||
search (first: $num, type:ISSUE, after: $after, query: $query) { | ||
pageInfo { | ||
hasPreviousPage, | ||
hasNextPage, | ||
startCursor, | ||
endCursor | ||
} | ||
nodes { | ||
... on PullRequest { | ||
number | ||
permalink | ||
createdAt | ||
closedAt | ||
mergedAt | ||
merged | ||
title | ||
lastEditedAt | ||
additions | ||
deletions | ||
changedFiles | ||
commits { | ||
totalCount | ||
} | ||
comments { | ||
totalCount | ||
} | ||
author { | ||
login | ||
} | ||
} | ||
@@ -25,3 +40,3 @@ } | ||
module.exports = { | ||
pullRequestsQuery | ||
pullRequestsQuery, | ||
}; |
const Table = require('easy-table'); | ||
const moment = require('moment'); | ||
const fs = require('fs'); | ||
const getRelativeDate = date => (date ? moment(date).fromNow() : '-'); | ||
const getDaysOpen = (fromDate, toDate) => moment(toDate).diff(fromDate, 'days'); | ||
const getHoursOpen = (fromDate, toDate) => moment(toDate).diff(fromDate, 'hours', true); | ||
const getFromDate = () => | ||
moment() | ||
.subtract(7, 'd') | ||
.format('YYYY-MM-DD'); | ||
const getToDate = () => moment().format('YYYY-MM-DD'); | ||
const printTable = tableData => { | ||
@@ -12,11 +20,29 @@ console.log(Table.print(tableData)); | ||
const normalizeStates = value => { | ||
return value.replace(/ /g, '').split(','); | ||
const constructSearchQueryString = params => { | ||
const { org, repo, state, fromDate, toDate, author } = params; | ||
return { | ||
...params, | ||
query: `repo:${org}/${repo} type:pr is:${state} created:>=${fromDate} merged:<=${toDate}${ | ||
author ? ` author:${author}` : '' | ||
}`, | ||
}; | ||
}; | ||
const appendToFile = (fileContent, filePathWithName = './prdata.csv') => { | ||
fs.appendFile(filePathWithName, fileContent, err => { | ||
if (err) { | ||
throw err; | ||
} | ||
}); | ||
}; | ||
module.exports = { | ||
getRelativeDate, | ||
getDaysOpen, | ||
normalizeStates, | ||
getHoursOpen, | ||
getFromDate, | ||
getToDate, | ||
printTable, | ||
constructSearchQueryString, | ||
appendToFile, | ||
}; |
const utils = require('../../src/utils'); | ||
const Table = require('easy-table'); | ||
const fs = require('fs'); | ||
jest.mock('fs', () => ({ | ||
appendFile: jest.fn(), | ||
})); | ||
describe('src/utils', () => { | ||
beforeEach(() => { | ||
window.Date.now = jest.fn(() => '2018-05-30T23:23:53Z'); | ||
}); | ||
describe('#getRelativeDate', () => { | ||
test('returns number of days passed', () => { | ||
window.Date.now = jest.fn(() => '2018-05-30T23:23:53Z'); | ||
expect(utils.getRelativeDate('2018-05-20T23:23:53Z')).toEqual('10 days ago'); | ||
@@ -17,5 +24,7 @@ }); | ||
describe('#getDaysOpen', () => { | ||
test('returns number of days passed between 2 dates', () => { | ||
expect(utils.getDaysOpen('2018-05-20T23:23:53Z', '2018-05-25T23:40:09Z')).toEqual(5); | ||
describe('#getHoursOpen', () => { | ||
test('returns number of hours passed between 2 dates', () => { | ||
expect(utils.getHoursOpen('2018-05-20T23:23:53Z', '2018-05-25T23:40:09Z')).toEqual( | ||
120.27111111111111 | ||
); | ||
}); | ||
@@ -43,19 +52,52 @@ }); | ||
describe('#normalizeStates', () => { | ||
test('returns single value', () => { | ||
expect(utils.normalizeStates('HELLO')).toEqual(['HELLO']); | ||
describe('#getFromDate', () => { | ||
test('returns YYYY-MM-DD format of date 7 days ago', () => { | ||
expect(utils.getFromDate()).toEqual('2018-05-23'); | ||
}); | ||
}); | ||
test('returns array from comma separated string and removes spaces', () => { | ||
expect(utils.normalizeStates('HELLO, WORLD')).toEqual(['HELLO', 'WORLD']); | ||
describe('#getToDate', () => { | ||
test('returns YYYY-MM-DD format of present day', () => { | ||
expect(utils.getToDate()).toEqual('2018-05-30'); | ||
}); | ||
}); | ||
test('returns array from comma separated string', () => { | ||
expect(utils.normalizeStates('HELLO,WORLD')).toEqual(['HELLO', 'WORLD']); | ||
describe('#constructSearchQueryString', () => { | ||
const params = { | ||
org: 'facebook', | ||
repo: 'react', | ||
state: 'merged', | ||
fromDate: '2018-01-01', | ||
toDate: '2018-01-02', | ||
foo: 'bar', | ||
}; | ||
test('returns query without author', () => { | ||
expect(utils.constructSearchQueryString(params)).toMatchObject({ | ||
...params, | ||
query: 'repo:facebook/react type:pr is:merged created:>=2018-01-01 merged:<=2018-01-02', | ||
}); | ||
}); | ||
test('returns array from empty string', () => { | ||
expect(utils.normalizeStates('')).toEqual(['']); | ||
test('returns query with author', () => { | ||
const paramsWithAuthor = { ...params, author: 'authorName' }; | ||
expect(utils.constructSearchQueryString({ ...paramsWithAuthor })).toMatchObject({ | ||
...paramsWithAuthor, | ||
query: | ||
'repo:facebook/react type:pr is:merged created:>=2018-01-01 merged:<=2018-01-02 author:authorName', | ||
}); | ||
}); | ||
}); | ||
describe('#appendToFile', () => { | ||
test('calls appendFile', () => { | ||
const fileContent = 'fileContentText'; | ||
const fileName = './prdata.csv'; | ||
utils.appendToFile(fileContent, fileName); | ||
expect(fs.appendFile).toHaveBeenCalledWith(fileName, fileContent, expect.any(Function)); | ||
}); | ||
}); | ||
}); |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
139148
16
385
52
6
2
+ Addedjson2csv@^4.2.1
+ Addedjson2csv@4.5.4(transitive)
+ Addedjsonparse@1.3.1(transitive)
+ Addedlodash.get@4.4.2(transitive)