UNPKG

pushhub

Version:

A github inspired interface to browse your Git repositories, built on top of expressjs and pushover

378 lines (328 loc) 10.2 kB
"use strict"; var cp = require('child_process'); var fs = require('fs'); var Commits = require('./commits'); var Cache = require('./cache'); var utils = require('./utils'); var spawn = cp.spawn; var basename = require('path').basename; var join = require('path').join; var debug = require('debug')('pushhub'); function sortByName(a, b) { if(a.name < b.name) return -1; if(a.name === b.name) return 0; if(a.name > b.name) return 1; } /** * Initialize a new `Repo` with the given `path` * * @param {String} path * @api public */ var Repo = module.exports = function Repo(path) { this.name = basename(path); this.path = path; this.cache = new Cache(); this.bare = !utils.isDirectory(join(path, '.git')); }; /** * Parse text coming from `git ls-tree` into an `Array` of `Object`s * * @param {String} text * @api private * @return {Array} */ Repo.prototype.parse = function parse(text) { text = text.toString(); return text.split('\n').map(function(line) { var o = { 'mode': line.slice(0, 6), 'type': line.slice(7, 11), 'hash': line.slice(12, 52), 'path': line.slice(53) }; // Useful to distinguish folders from revision/refs o.path += (o.type == 'tree') ? '/' : ''; o.name = basename(o.path); return o; }, this); }; /** * Proxy method to git commands, we don't use cp.exec here because the * stdout it returns is a `String` as it cause problems with binary * files * * @param {Array} commands * @param {Function} cb * @api private */ Repo.prototype.gitExec = function gitExec(commands, cb) { var child = spawn("git", commands, {cwd: this.path}); var stdout = [], stderr = []; debug('[SPAWN] %s', "git " + commands.join(" ")); child.stdout.on('data', function (text) { stdout[stdout.length] = text; }); child.stderr.on('data', function (text) { stderr[stderr.length] = text; }); child.on('close', function (code) { if (code > 0) { var err = new Error("git " + commands.join(" ") + "\n" + utils.bufferConcat(stderr, 'utf8')); cb(err); return; } cb(null, utils.bufferConcat(stdout)); }); child.stdin.end(); }; Repo.prototype.dirStat = function dirStat(ref, path, entries, cb) { var directories, files, commits = [], expected = entries.length; entries.forEach(function(entry) { this.stats(ref, entry, { maxcount: 1 }, function(err, commit) { if(err) throw err; commits.push(commit); if(commits.length == expected) { directories = commits.filter(function(a) { return a.type == 'tree'; }); files = commits.filter(function(a) { return a.type == 'blob'; }); directories.sort(sortByName); files.sort(sortByName); cb(null, directories.concat(files)); } }); }, this); return commits; }; /** * If passed `str`, writes `str` to the repo description file, else reads the latter and returns it * * @param {String} str * @param {String} str * @api public * @return {String} */ Repo.prototype.description = function description(str) { var path = this.bare ? join(this.path, 'description') : join(this.path, '.git', 'description'); if(str) { fs.writeFileSync(path, str); } else { return fs.readFileSync(path).toString(); } }; /** * Returns a Stat object for the given `entry` at the the given `ref` * Options are: * - maxcount: Commit history to retrieve * - * * @param {String} ref * @param {String, Object} entry * @param {Object} options * @api public */ Repo.prototype.stats = function stats(ref, entry, options /*, cb */) { options = options || { maxcount: -1, skip: 0 }; if(typeof entry === 'string') entry = { path: entry }; var cb = (typeof options === 'function') ? options : arguments[3]; this.gitExec(['log', ref, '--date=relative', '--max-count=' + (options.maxcount || -1), '--skip=' + (options.skip || 0), '--',entry.path ], function(err, stdout) { if(err) { return cb(err); } else { entry.commits = new Commits(stdout); entry.tip = entry.commits.tip(); entry.url = utils.url('', this.name, entry.type, ref, entry.path); return cb(null, entry); } }.bind(this)); }; /** * Calls `cb` passing (err, list) where `list` is an array containing commit objects for * each `entry` in the folder described by `path` for `ref`. * * @param {String} ref * @param {String} path * @param {Function} cb * @api public */ Repo.prototype.tree = function tree(ref, path, cb) { var entries; debug('tree for path "%s"', path); this.gitExec(['ls-tree', ref, path], function(err, stdout) { if(err) { return cb(err); } entries = this.parse(stdout); this.dirStat(ref, path, entries, function(err, tree) { if(err) { return cb(err); } cb(null, tree); }); }.bind(this)); }; /** * Calls `cb` passing (err, buf) where `buf` is a buffer which is the content * of the file match by `path` for `ref`. * * @param {String} ref * @param {String} path * @param {Function} cb * @api public */ Repo.prototype.blob = function blob(ref, path, cb) { debug('blob for path "%s"', path); this.gitExec(['show', ref + ':' + path], cb); }; /** * Calls `cb` passing (err, buf) where `buf` is a `format` archive of `ref` at its current state * * @param {String} ref * @param {String} format * @param {Function} cb * @api public */ Repo.prototype.archive = function archive(ref, format, cb) { debug('archiving ref "%s" in %s format', ref, format); this.gitExec(['archive', ref, '--format=' + format], function(err, stdout) { if(err) { return cb(err); } else { return cb(null, stdout); } }.bind(this)); }; /** * Calls `cb` passing (err, list) where `list` is an array of the tags for this repo * * @param {Function} cb * @api public */ Repo.prototype.tags = function tags(cb) { var out; if(this.cache.has('tags')) { debug('[CACHE] tags: fetching from cache'); return cb(null, this.cache.get('tags')); } this.gitExec(['tag', '-l'], function(err, stdout) { if(err) { return cb(err); } else { out = stdout.toString().split('\n').filter(utils.not('')); debug('[CACHE] tags: storing in cache, [%s]', out.join()); this.cache.store('tags', out); return cb(null, out); } }.bind(this)); }; /** * Calls `cb` passing (err, list) where `list` is an array of the branches for this repo * * @param {Function} cb * @api public */ Repo.prototype.branches = function branches(cb) { var lines; if(this.cache.has('branches')) { debug('[CACHE] branches: fetching from cache'); return cb(null, this.cache.get('branches')); } this.gitExec(['branch'], function(err, stdout) { if(err) { return cb(err); } else { lines = stdout.toString().split('\n'); lines.pop(); lines = lines.map(function(line) { return line.replace(/^\*\s/, '').trim(); }); debug('[CACHE] branches: storing in cache, [%s]', lines.join()); this.cache.store('branches', lines); return cb(null, lines); } }.bind(this)); }; /** * Calls `cb` passing (err, obj) where `obj` is an hash, the last commit entry for `ref` * * @param {String} ref * @param {Function} cb * @api public */ Repo.prototype.tip = function tip(ref, cb) { var c; if(this.cache.has('tip:' + ref)) { debug('[CACHE] tip: fetching [%s] from cache', ref); return cb(null, this.cache.get('tip:' + ref)); } this.gitExec(['log', ref, '-n1', '--date=relative', '--', '.'], function(err, stdout) { if(err) { return cb(err); } else { c = new Commits(stdout); debug('[CACHE] tip: storing [%s] in cache', ref); this.cache.store('tip:' + ref, c.tip()); return cb(null, c.tip()); } }.bind(this)); }; /** * Calls `cb` passing (err, num) where `num` is the number of commits in the given `ref` * `num` is 0 if something horrible happened * * @param {String} ref * @param {Function} cb * @api public */ Repo.prototype.total = function total(ref, cb) { var num = 0; var global = /^.*\((\d+)\)\:/gm; var number = /\((\d+)\)/; if(this.cache.has('total:' + ref)) { debug('[CACHE] total: fetching [%s] from cache', ref); return cb(null, this.cache.get('total:' + ref)); } this.gitExec(['shortlog', ref, '--'], function(err, stdout) { if(err) { return cb(err); } else { var str = stdout.toString(); var matches = str.match(global); if(matches) { num = matches.reduce(function(memo, value) { var num = value.match(number); return memo + parseInt(num[1], 10); }, 0); } debug('[CACHE] total: storing [%s] in cache', ref); this.cache.store('total:' + ref, num); return cb(null, num); } }.bind(this)); }; /** * Calls `cb` passing (err, date) where `date` is a Date instance * * @param {Function} cb * @api public */ Repo.prototype.mtime = function mtime(cb) { if(this.cache.has('mtime')) { debug('[CACHE] mtime: fetching from cache'); return cb(null, this.cache.get('mtime')); } fs.stat(this.path, function(err, stat) { if(err) { return cb(err); } else { debug('[CACHE] mtime: storing in cache'); this.cache.store('mtime', stat.mtime); return cb(null, stat.mtime); } }.bind(this)); };