#!/usr/bin/env node 'use strict'; var util = require('util'); var cp = require('child_process'); var Q = require('q'); var _ = require('lodash'); var semver = require('semver'); var exec = function(cmd) { return function() { var args = Array.prototype.slice.call(arguments, 0); args.unshift(cmd); var fullCmd = util.format.apply(util, args); return Q.nfcall(cp.exec, fullCmd).then(function(out) { return out[0].split('\n'); }); }; }; var andThen = function(fn, after) { return /** @this */ function() { return fn.apply(this, arguments).then(after); }; }; var oneArg = function(fn) { return function(arg) { return fn(arg); }; }; var oneLine = function(lines) { return lines[0].trim(); }; var noArgs = function(fn) { return function() { return fn(); }; }; var identity = function(i) { return i; }; // like Q.all, but runs the commands in series // useful for ensuring env state (like which branch is checked out) var allInSeries = function(fn) { return function(args) { var results = []; var def; while (args.length > 0) { (function(arg) { if (def) { def = def.then(function() { return fn(arg); }); } else { def = fn(arg); } def = def.then(function(res) { results.push(res); }); })(args.pop()); } return def.then(function() { return results; }); }; }; var compareBranches = function(left, right) { console.log('# These commits are in ' + left.name + ' but not in ' + right.name + '\n'); console.log(_(left.log). difference(right.log). map(function(line) { return left.full[left.log.indexOf(line)]; // lol O(n^2) }). value(). join('\n')); }; var checkout = oneArg(exec('git checkout %s')); var getCurrentBranch = andThen(noArgs(exec('git rev-parse --abbrev-ref HEAD')), oneLine); var getTags = noArgs(exec('git tag')); var getTheLog = oneArg(exec('git log --pretty=oneline %s..HEAD | cat')); // remember this so we can restore state var currentBranch; getCurrentBranch(). then(function(branch) { currentBranch = branch; }). then(getTags). then(function(tags) { return tags. filter(semver.valid). map(semver.clean). sort(semver.rcompare); }). then(function(tags) { var major = semver(tags[0]).major; return tags. filter(function(ver) { return semver(ver).major === major; }); }). then(function(tags) { return _(tags). groupBy(function(tag) { return tag.split('.')[1]; }). map(function(group) { return _.first(group); }). map(function(tag) { return 'v' + tag; }). value(); }). then(function(tags) { var master = tags.pop(); var stable = tags.pop(); return [ { name: stable.replace(/\d+$/, 'x'), tag: stable }, { name: 'master', tag: master} ]; }). then(allInSeries(function(branch) { return checkout(branch.name). then(function() { return getTheLog(branch.tag); }). then(function(log) { return log. filter(identity); }). then(function(log) { branch.full = log.map(function(line) { line = line.split(' '); var sha = line.shift(); var msg = line.join(' '); return sha + ((/fix\([^)]+\):/i.test(msg)) ? ' * ' : ' ') + msg; }); branch.log = log.map(function(line) { return line.substr(41); }); return branch; }); })). then(function(pairs) { compareBranches(pairs[0], pairs[1]); console.log('\n'); compareBranches(pairs[1], pairs[0]); return pairs; }). then(function() { return checkout(currentBranch); }). catch(function(e) { console.log(e.stack); });