diff --git a/server.js b/server.js index 6de49e94b56a0..0cba5f2323e79 100644 --- a/server.js +++ b/server.js @@ -3906,6 +3906,71 @@ cache(function(data, match, sendBadge, request) { }); })); +// GitHub languages integration. +camp.route(/^\/github\/languages\/(top|count|code-size)\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var type = match[1]; + var user = match[2]; + var repo = match[3]; + var format = match[4]; + var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo + '/languages'; + var badgeData = getBadgeData('', data); + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data); + } + 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); + var sumBytes = 0; + switch(type) { + case 'top': + var topLanguage = 'language'; + var maxBytes = 0; + for (const language of Object.keys(parsedData)) { + const bytes = parseInt(parsedData[language]); + if (bytes >= maxBytes) { + maxBytes = bytes; + topLanguage = language; + } + sumBytes += bytes; + } + badgeData.text[0] = topLanguage; + if (sumBytes === 0) { // eg, empty repo, only .md files, etc. + badgeData.text[1] = 'none'; + badgeData.colorscheme = 'blue'; + } else { + badgeData.text[1] = (maxBytes / sumBytes * 100).toFixed(1) + '%'; // eg, 9.1% + } + break; + case 'count': + badgeData.text[0] = 'languages'; + badgeData.text[1] = Object.keys(parsedData).length; + badgeData.colorscheme = 'blue'; + break; + case 'code-size': + for (const language of Object.keys(parsedData)) { + sumBytes += parseInt(parsedData[language]); + } + badgeData.text[0] = 'code size'; + badgeData.text[1] = prettyBytes(sumBytes); + badgeData.colorscheme = 'blue'; + break; + default: + throw Error('Unreachable due to regex'); + } + 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) { diff --git a/service-tests/github.js b/service-tests/github.js index 535af892011fb..bd3c778c0b493 100644 --- a/service-tests/github.js +++ b/service-tests/github.js @@ -447,3 +447,28 @@ t.create('github pull request check state') t.create('github pull request check contexts') .get('/status/contexts/pulls/badges/shields/1110.json') .expectJSONTypes(Joi.object().keys({ name: 'checks', value: '1 failure' })); + +t.create('top language') +.get('/languages/top/badges/shields.json') +.expectJSONTypes(Joi.object().keys({ + name: Joi.equal('JavaScript'), + value: Joi.string().regex(/^([1-9]?[0-9]\.[0-9]|100\.0)%$/), +})); + +t.create('top language with empty repository') +.get('/languages/top/pyvesb/emptyrepo.json') +.expectJSON({ name: 'language', value: 'none' }); + +t.create('language count') +.get('/languages/count/badges/shields.json') +.expectJSONTypes(Joi.object().keys({ + name: Joi.equal('languages'), + value: Joi.number().integer().positive(), +})); + +t.create('code size in bytes for all languages') +.get('/languages/code-size/badges/shields.json') +.expectJSONTypes(Joi.object().keys({ + name: Joi.equal('code size'), + value: isFileSize, +})); diff --git a/try.html b/try.html index a551cf155368d..e790bf73b1525 100644 --- a/try.html +++ b/try.html @@ -955,6 +955,18 @@

Miscellaneous

https://img.shields.io/github/commits/google/skia/infra/config/last.svg + GitHub top language: + + https://img.shields.io/github/languages/top/badges/shields.svg + + GitHub language count: + + https://img.shields.io/github/languages/count/badges/shields.svg + + GitHub code size in bytes: + + https://img.shields.io/github/languages/code-size/badges/shields.svg + Bitbucket issues: https://img.shields.io/bitbucket/issues/atlassian/python-bitbucket.svg