Skip to content

Commit

Permalink
[Github] Last commit date and commit activity (#1112)
Browse files Browse the repository at this point in the history
Reopen of #928 by mskonovalov. Closes #897.
  • Loading branch information
paulmelnikow authored Oct 2, 2017
1 parent c62534b commit 8e08b37
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 8 deletions.
63 changes: 57 additions & 6 deletions lib/color-formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
*/
'use strict';

function versionFormatter(version) {
const moment = require('moment');

function version(version) {
let first = version[0];
if (first === 'v') {
first = version[1];
Expand All @@ -17,20 +19,17 @@ function versionFormatter(version) {
return { version: version, color: 'blue' };
}
}
exports.version = versionFormatter;

function downloadCount(downloads) {
return floorCount(downloads, 10, 100, 1000);
}
exports.downloadCount = downloadCount;

function coveragePercentage(percentage) {
return floorCount(percentage, 80, 90, 100);
}
exports.coveragePercentage = coveragePercentage;

function floorCount(value, yellow, yellowgreen, green) {
if (value === 0) {
if (value <= 0) {
return 'red';
} else if (value < yellow) {
return 'yellow';
Expand All @@ -42,4 +41,56 @@ function floorCount(value, yellow, yellowgreen, green) {
return 'brightgreen';
}
}
exports.floorCount = floorCount;

function colorScale(steps, colors, reversed) {
if (steps === undefined) {
throw Error('When invoking colorScale, steps should be provided.');
}

const defaultColors = {
1: ['red', 'brightgreen'],
2: ['red', 'yellow', 'brightgreen'],
3: ['red', 'yellow', 'green', 'brightgreen'],
4: ['red', 'yellow', 'yellowgreen', 'green', 'brightgreen'],
5: ['red', 'orange', 'yellow', 'yellowgreen', 'green', 'brightgreen'],
};

if (typeof colors === 'undefined') {
if (steps.length in defaultColors) {
colors = defaultColors[steps.length];
} else {
throw Error(`No default colors for ${steps.length} steps.`);
}
}

if (steps.length !== colors.length - 1) {
throw Error('When colors are provided, there should be n + 1 colors for n steps.');
}

if (reversed) {
colors = Array.from(colors).reverse();
}

return value => {
const stepIndex = steps.findIndex(step => value < step);

// For the final step, stepIndex is -1, so in all cases this expression
// works swimmingly.
return colors.slice(stepIndex)[0];
};
}

function age(date) {
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, true);
const daysElapsed = moment().diff(moment(date), 'days');
return colorByAge(daysElapsed);
}

module.exports = {
version,
downloadCount,
coveragePercentage,
floorCount,
colorScale,
age
};
64 changes: 64 additions & 0 deletions lib/color-formatters.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

const assert = require('assert');
const {
coveragePercentage,
colorScale,
age
} = require('./color-formatters');

describe('Color formatters', function() {
it('should step appropriately', function() {
const byPercentage = colorScale([Number.EPSILON, 80, 90, 100]);
assert.equal(byPercentage(-1), 'red');
assert.equal(byPercentage(0), 'red');
assert.equal(byPercentage(0.5), 'yellow');
assert.equal(byPercentage(1), 'yellow');
assert.equal(byPercentage(50), 'yellow');
assert.equal(byPercentage(80), 'yellowgreen');
assert.equal(byPercentage(85), 'yellowgreen');
assert.equal(byPercentage(90), 'green');
assert.equal(byPercentage(100), 'brightgreen');
assert.equal(byPercentage(101), 'brightgreen');
});

it('should have parity with coveragePercentage', function() {
const byPercentage = colorScale([Number.EPSILON, 80, 90, 100]);
assert.equal(byPercentage(-1), coveragePercentage(-1));
assert.equal(byPercentage(0), coveragePercentage(0));
assert.equal(byPercentage(0.5), coveragePercentage(0.5));
assert.equal(byPercentage(1), coveragePercentage(1));
assert.equal(byPercentage(50), coveragePercentage(50));
assert.equal(byPercentage(80), coveragePercentage(80));
assert.equal(byPercentage(85), coveragePercentage(85));
assert.equal(byPercentage(90), coveragePercentage(90));
assert.equal(byPercentage(100), coveragePercentage(100));
assert.equal(byPercentage(101), coveragePercentage(101));
});

it('should step in reverse', function() {
const byAge = colorScale([7, 30, 180, 365, 730], undefined, true);
assert.equal(byAge(3), 'brightgreen');
assert.equal(byAge(7), 'green');
assert.equal(byAge(10), 'green');
assert.equal(byAge(60), 'yellowgreen');
assert.equal(byAge(250), 'yellow');
assert.equal(byAge(400), 'orange');
assert.equal(byAge(800), 'red');
});

it('should generate correct color for ages', function() {
const monthsAgo = months => {
const result = new Date();
// This looks wack but it works.
result.setMonth(result.getMonth() - months);
return result;
};

assert.equal(age(Date.now()), 'brightgreen');
assert.equal(age(new Date()), 'brightgreen');
assert.equal(age(new Date(2001, 1, 1)), 'red');
assert.equal(age(monthsAgo(2)), 'yellowgreen');
assert.equal(age(monthsAgo(15)), 'orange');
});
});
18 changes: 17 additions & 1 deletion lib/text-formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*/
'use strict';

const moment = require('moment');
moment().format();

function starRating(rating) {
let stars = '';
while (stars.length < rating) { stars += '★'; }
Expand Down Expand Up @@ -60,11 +63,24 @@ function maybePluralize(singular, countable, plural) {
}
}

function formatDate(d) {
const date = moment(d);
const dateString = date.calendar(null, {
lastDay: '[yesterday]',
sameDay: '[today]',
lastWeek: '[last] dddd',
sameElse: 'MMMM YYYY'
});
// Trim current year from date string
return dateString.replace(` ${moment().year()}`, '').toLowerCase();
}

module.exports = {
starRating,
currencyFromCode,
ordinalNumber,
metric,
omitv,
maybePluralize
maybePluralize,
formatDate
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"pretty-bytes": "^3.0.1",
"gm": "^1.23.0",
"json-autosave": "~1.1.2",
"moment": "^2.18.1",
"pdfkit": "~0.8.0",
"redis": "~2.6.2",
"request": "~2.81.0",
Expand Down
88 changes: 87 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ const {
ordinalNumber,
starRating,
omitv,
maybePluralize
maybePluralize,
formatDate
} = require('./lib/text-formatters');
const {
coveragePercentage: coveragePercentageColor,
downloadCount: downloadCountColor,
floorCount: floorCountColor,
version: versionColor,
age: ageColor
} = require('./lib/color-formatters');
const {
analyticsAutoLoad,
Expand Down Expand Up @@ -3714,6 +3716,90 @@ cache(function(data, match, sendBadge, request) {
});
}));

// GitHub commit statistics integration.
camp.route(/^\/github\/commit-activity\/(y|4w|w)\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/,
cache(function(data, match, sendBadge, request) {
const interval = match[1];
const user = match[2];
const repo = match[3];
const format = match[4];
const apiUrl = `${githubApiUrl}/repos/${user}/${repo}/stats/commit_activity`;
const badgeData = getBadgeData('commit activity', data);
if (badgeData.template === 'social') {
badgeData.logo = getLogo('github', data);
badgeData.links = [`https://github.com/${user}/${repo}`];
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err !== null) {
badgeData.text[1] = 'inaccessible';
sendBadge(format, badgeData);
return;
}
try {
const parsedData = JSON.parse(buffer);
let value;
let intervalLabel;
switch (interval) {
case 'y':
value = parsedData.reduce((sum, weekInfo) => sum + weekInfo.total, 0);
intervalLabel = '/year';
break;
case '4w':
value = parsedData.slice(-4).reduce((sum, weekInfo) => sum + weekInfo.total, 0);
intervalLabel = '/4 weeks';
break;
case 'w':
value = parsedData.slice(-1)[0].total;
intervalLabel = '/week';
break;
default:
throw Error('Unhandled case');
}
badgeData.text[1] = `${metric(value)}${intervalLabel}`;
badgeData.colorscheme = 'blue';
sendBadge(format, badgeData);
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
}
});
}));

// GitHub last commit integration.
camp.route(/^\/github\/last-commit\/([^\/]+)\/([^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/,
cache(function(data, match, sendBadge, request) {
const user = match[1]; // eg, mashape
const repo = match[2]; // eg, apistatus
const branch = match[3];
const format = match[4];
let apiUrl = `${githubApiUrl}/repos/${user}/${repo}/commits`;
if (branch) {
apiUrl += `?sha=${branch}`;
}
const badgeData = getBadgeData('last commit', data);
if (badgeData.template === 'social') {
badgeData.logo = getLogo('github', data);
badgeData.links = [`https://github.com/${user}/${repo}`];
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err !== null) {
badgeData.text[1] = 'inaccessible';
sendBadge(format, badgeData);
return;
}
try {
const parsedData = JSON.parse(buffer);
const commitDate = parsedData[0].commit.author.date;
badgeData.text[1] = formatDate(commitDate);
badgeData.colorscheme = ageColor(Date.parse(commitDate));
sendBadge(format, badgeData);
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
}
});
}));

// Bitbucket issues integration.
camp.route(/^\/bitbucket\/issues(-raw)?\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/,
cache(function(data, match, sendBadge, request) {
Expand Down
42 changes: 42 additions & 0 deletions service-tests/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,45 @@ t.create('hit counter for nonexistent repo')
name: Joi.equal('goto counter'),
value: Joi.string().regex(/^repo not found$/),
}));

t.create('commit activity (1 year)')
.get('/commit-activity/y/eslint/eslint.json')
.expectJSONTypes(Joi.object().keys({
name: Joi.equal('commit activity'),
value: Joi.string().regex(/^[0-9]+[kMGTPEZY]?\/year$/),
}));

t.create('commit activity (4 weeks)')
.get('/commit-activity/4w/eslint/eslint.json')
.expectJSONTypes(Joi.object().keys({
name: Joi.equal('commit activity'),
value: Joi.string().regex(/^[0-9]+[kMGTPEZY]?\/4 weeks$/),
}));

t.create('commit activity (1 week)')
.get('/commit-activity/w/eslint/eslint.json')
.expectJSONTypes(Joi.object().keys({
name: Joi.equal('commit activity'),
value: Joi.string().regex(/^[0-9]+[kMGTPEZY]?\/week$/),
}));

t.create('last commit (recent)')
.get('/last-commit/eslint/eslint.json')
.expectJSONTypes(Joi.object().keys({
name: Joi.equal('last commit'),
value: Joi.string().regex(/^today|yesterday|last (?:sun|mon|tues|wednes|thurs|fri|satur)day/),
}));

t.create('last commit (ancient)')
.get('/last-commit/badges/badgr.co.json')
.expectJSONTypes(Joi.object().keys({
name: Joi.equal('last commit'),
value: Joi.equal('january 2014'),
}));

t.create('last commit (on branch)')
.get('/last-commit/badges/badgr.co/shielded.json')
.expectJSONTypes(Joi.object().keys({
name: Joi.equal('last commit'),
value: Joi.equal('july 2013'),
}));
11 changes: 11 additions & 0 deletions try.html
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,17 @@ <h3 id="miscellaneous"> Miscellaneous </h3>
<tr><th data-keywords='GitHub search hit counter' data-doc='githubDoc'> Github search hit counter: </th>
<td><img src='/github/search/torvalds/linux/goto.svg' alt=''/></td>
<td><code>https://img.shields.io/github/search/torvalds/linux/goto.svg</code></td>
<tr><th data-keywords='GitHub commit commits activity' data-doc='githubDoc'> GitHub commit activity the past week, 4 weeks, year </th>
<td><img src='/github/commit-activity/y/eslint/eslint.svg' alt=''/></td>
<td><code>https://img.shields.io/github/commit-activity/y/eslint/eslint.svg</code></td>
</tr>
<tr><th data-keywords='GitHub last latest commit' data-doc='githubDoc'> GitHub last commit: </th>
<td><img src='/github/last-commit/google/skia.svg' alt=''/></td>
<td><code>https://img.shields.io/github/commits/google/skia/last.svg</code></td>
</tr>
<tr><th data-keywords='GitHub last latest commit' data-doc='githubDoc'> GitHub last commit (branch): </th>
<td><img src='/github/last-commit/google/skia/infra/config.svg' alt=''/></td>
<td><code>https://img.shields.io/github/commits/google/skia/infra/config/last.svg</code></td>
</tr>
<tr><th> Bitbucket issues: </th>
<td><img src='/bitbucket/issues/atlassian/python-bitbucket.svg' alt=''/></td>
Expand Down

0 comments on commit 8e08b37

Please sign in to comment.