├── .gitignore ├── LICENSE ├── README.md ├── app ├── github.js ├── router.js └── templates │ └── index.html ├── index.js ├── package.json └── processes.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 31 | node_modules 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Wang Dàpéng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # first-commit 2 | a service to find the first commit of a repo, built with koa.js 3 | 4 | http://first-commit.com 5 | -------------------------------------------------------------------------------- /app/github.js: -------------------------------------------------------------------------------- 1 | var rp = require('request-promise'); 2 | var cheerio = require('cheerio'); 3 | var Promise = require("bluebird"); 4 | 5 | const BASE_URL = 'https://github.com'; 6 | 7 | 8 | function fetch(url) { 9 | var options = { 10 | uri: url, 11 | headers: { 12 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) Gecko/20100101 Firefox/40.1' 13 | }, 14 | transform: function(body) { 15 | return cheerio.load(body); 16 | } 17 | }; 18 | return rp(options); 19 | } 20 | 21 | 22 | function getRepoCommitsCount(repo) { 23 | return new Promise((resolve, reject) => { 24 | var url = `${BASE_URL}/${repo}`; 25 | console.log('Fetching ', url); 26 | fetch(url) 27 | .then($ => { 28 | var num_str = $('.numbers-summary .commits .num').text().trim(); 29 | var num = parseInt(num_str.replace(/,/g, ''), 10); 30 | resolve(num); 31 | console.log('Got commit count', num); 32 | }) 33 | .catch(reject); 34 | }); 35 | } 36 | 37 | function getFirstPageUrl(repo, num_of_commits) { 38 | const COMMITS_PER_PAGE = 35; 39 | var first_page = Math.ceil(num_of_commits / COMMITS_PER_PAGE); 40 | return `${BASE_URL}/${repo}/commits?page=${first_page}`; 41 | } 42 | 43 | function extractFirstCommitInfo($) { 44 | var li = $('.commit.commits-list-item').last(); 45 | 46 | var repo = $('meta[property="og:title"]').attr('content'); 47 | var sha = li.find('.commit-links-cell .zeroclipboard-button').data('clipboard-text'); 48 | 49 | var author_id = li.find('.commit-author-section a').text().trim(); 50 | var commit_title = li.find('.commit-title a').text().trim(); 51 | var time_str = li.find('relative-time').attr('datetime'); 52 | 53 | return { 54 | repo: repo, 55 | sha: sha, 56 | title: commit_title, 57 | time: new Date(time_str), 58 | url: `${BASE_URL}/${repo}/commit/${sha}`, 59 | browse_url: `${BASE_URL}/${repo}/tree/${sha}`, 60 | author: author_id 61 | }; 62 | } 63 | 64 | function getFirstCommit(repo) { 65 | return new Promise((resolve, reject) => { 66 | getRepoCommitsCount(repo) 67 | .then(count => getFirstPageUrl(repo, count)) 68 | .then(fetch) 69 | .then(extractFirstCommitInfo) 70 | .then(resolve) 71 | .catch(reject); 72 | }); 73 | } 74 | 75 | 76 | exports.getFirstCommit = getFirstCommit; 77 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | var router = require('koa-router')(); 2 | var send = require('koa-send'); 3 | var github = require('./github') 4 | 5 | 6 | var serveIndex = function *(next) { 7 | yield send(this, 'index.html', { 8 | root: __dirname + '/templates' 9 | }); 10 | }; 11 | 12 | router.get('/', serveIndex); 13 | router.get('/:user/:repo', serveIndex); 14 | 15 | 16 | router.get('/api/:user/:repo', function *(next) { 17 | var params = this.params; 18 | var repo = params.user + '/' + params.repo; 19 | 20 | console.log('Got request for', repo); 21 | 22 | if (yield* this.cashed()) { 23 | console.log(repo, ' is cached'); 24 | return; 25 | } 26 | 27 | this.body = yield github.getFirstCommit(repo); 28 | }); 29 | 30 | 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |