big commands refactor in prep for hackathon

This commit is contained in:
Peter Cottle 2013-07-27 23:49:27 -07:00
parent 5ec8a2916f
commit e422bdb181
11 changed files with 1071 additions and 1077 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

1
build/bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -157,6 +157,7 @@ module.exports = function(grunt) {
runs: true,
waitsFor: true,
exports: true,
module: true,
process: true
}
},

View file

@ -439,7 +439,7 @@
For a much easier time perusing the source, see the individual files at:
https://github.com/pcottle/learnGitBranching
-->
<script src="build/bundle.js"></script>
<script src="build/bundle.min.f16f04ad.js"></script>
<!-- The advantage of github pages: super-easy, simple, slick static hostic.
The downside? No raw logs to parse for analytics, so I have to include

View file

@ -29,7 +29,8 @@ var expectTreeAsync = function(command, expectedJSON) {
if (diff > TIME - 50 && !haveReported) {
haveReported = true;
console.log('not going to match', command);
console.log('expected', loadTree(expectedJSON), 'actual', headless.gitEngine.exportTree());
console.log('expected\n>>>>>>>>\n', loadTree(expectedJSON));
console.log('\n<<<<<<<<<<<\nactual', headless.gitEngine.exportTree());
}
return compareAnswer(headless, expectedJSON);
}, 'trees should be equal', 100);
@ -57,6 +58,27 @@ describe('GitEngine', function() {
);
});
it('handles branch options', function() {
expectTreeAsync(
'git branch banana C0; git commit; git checkout -b side banana; git branch -d banana;git branch -f another C1; git commit',
'{"branches":{"master":{"target":"C2","id":"master"},"side":{"target":"C3","id":"side"},"another":{"target":"C1","id":"another"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C0"],"id":"C3"}},"HEAD":{"target":"side","id":"HEAD"}}'
);
});
it('does add', function() {
expectTreeAsync(
'git add; git commit',
oneCommit
);
});
it('resets with all options', function() {
expectTreeAsync(
'git commit;git reset --soft HEAD~1;git reset --hard HEAD~1;gc;go C1;git reset --hard C3;',
'{"branches":{"master":{"target":"C3","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"}},"HEAD":{"target":"C1","id":"HEAD"}}'
);
});
it('Checkouts', function() {
expectTreeAsync(
'git checkout -b side',
@ -115,7 +137,7 @@ describe('GitEngine', function() {
it('Cherry picks', function() {
expectTreeAsync(
'git checkout -b side C0; gc; git cherry-pick C1',
'git checkout -b side C0; gc; git cherry-pick C11; git cherry-pick C1',
'%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C1%27%22%2C%22id%22%3A%22side%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C1%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C1%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22side%22%2C%22id%22%3A%22HEAD%22%7D%7D'
);
});

View file

@ -19,14 +19,15 @@ var expectTreeAsync = function(headless, levelBlob) {
// dont do interactive rebase levels
return;
}
var start = Date.now();
var start;
runs(function() {
start = Date.now();
headless.sendCommand(command);
});
waitsFor(function() {
var diff = (Date.now() - start);
if (diff > TIME - 50) {
if (diff > TIME - 10) {
console.log('not going to match', command);
}
var result = compareLevelTree(headless, levelBlob);

View file

@ -37,7 +37,9 @@ var Commands = {
msg = args[0];
}
var newCommit = engine.commit();
var newCommit = engine.commit({
isAmend: commandOptions['--amend']
});
if (msg) {
msg = msg
.replace(/&quot;/g, '"')
@ -54,6 +56,239 @@ var Commands = {
engine.animationQueue.thenFinish(promise);
},
cherrypick: function(engine, command) {
var commandOptions = command.getSupportedMap();
var generalArgs = command.getGeneralArgs();
command.validateArgBounds(generalArgs, 1, Number.MAX_VALUE);
var set = engine.getUpstreamSet('HEAD');
// first resolve all the refs (as an error check)
var toCherrypick = _.map(generalArgs, function(arg) {
var commit = engine.getCommitFromRef(arg);
// and check that its not upstream
if (set[commit.get('id')]) {
throw new GitError({
msg: intl.str(
'git-error-already-exists',
{ commit: commit.get('id') }
)
});
}
return commit;
}, this);
engine.setupCherrypickChain(toCherrypick);
},
pull: function(engine, command) {
if (!engine.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
var commandOptions = command.getSupportedMap();
command.acceptNoGeneralArgs();
engine.pull({
isRebase: commandOptions['--rebase']
});
},
fakeTeamwork: function(engine, command) {
var generalArgs = command.getGeneralArgs();
if (!engine.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
command.validateArgBounds(generalArgs, 0, 2);
// allow formats of: git Faketeamwork 2 or git Faketeamwork side 3
var branch = (engine.origin.refs[generalArgs[0]]) ?
generalArgs[0] : 'master';
var numToMake = parseInt(generalArgs[0], 10) || generalArgs[1] || 1;
// make sure its a branch and exists
var destBranch = engine.origin.resolveID(branch);
if (destBranch.get('type') !== 'branch') {
throw new GitError({
msg: intl.str('git-error-options')
});
}
engine.fakeTeamwork(numToMake, branch);
},
clone: function(engine, command) {
command.acceptNoGeneralArgs();
engine.makeOrigin(engine.printTree());
},
fetch: function(engine, command) {
if (!engine.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
command.acceptNoGeneralArgs();
engine.fetch();
},
branch: function(engine, command) {
var commandOptions = command.getSupportedMap();
var generalArgs = command.getGeneralArgs();
var args = null;
// handle deletion first
if (commandOptions['-d'] || commandOptions['-D']) {
var names = commandOptions['-d'] || commandOptions['-D'];
command.validateArgBounds(names, 1, Number.MAX_VALUE, '-d');
_.each(names, function(name) {
engine.deleteBranch(name);
});
return;
}
if (commandOptions['--contains']) {
args = commandOptions['--contains'];
command.validateArgBounds(args, 1, 1, '--contains');
engine.printBranchesWithout(args[0]);
return;
}
if (commandOptions['-f']) {
args = commandOptions['-f'];
command.twoArgsImpliedHead(args, '-f');
// we want to force a branch somewhere
engine.forceBranch(args[0], args[1]);
return;
}
if (generalArgs.length === 0) {
var branches;
if (commandOptions['-a']) {
branches = engine.getBranches();
} else if (commandOptions['-r']) {
branches = engine.getRemoteBranches();
} else {
branches = engine.getLocalBranches();
}
engine.printBranches(branches);
return;
}
command.twoArgsImpliedHead(generalArgs);
engine.branch(generalArgs[0], generalArgs[1]);
},
add: function() {
throw new CommandResult({
msg: intl.str('git-error-staging')
});
},
reset: function(engine, command) {
var commandOptions = command.getSupportedMap();
var generalArgs = command.getGeneralArgs();
if (commandOptions['--soft']) {
throw new GitError({
msg: intl.str('git-error-staging')
});
}
if (commandOptions['--hard']) {
command.addWarning(
intl.str('git-warning-hard')
);
// dont absorb the arg off of --hard
generalArgs = generalArgs.concat(commandOptions['--hard']);
}
command.validateArgBounds(generalArgs, 1, 1);
if (engine.getDetachedHead()) {
throw new GitError({
msg: intl.str('git-error-reset-detached')
});
}
engine.reset(generalArgs[0]);
},
revert: function(engine, command) {
var generalArgs = command.getGeneralArgs();
command.validateArgBounds(generalArgs, 1, Number.MAX_VALUE);
engine.revert(generalArgs);
},
merge: function(engine, command) {
var generalArgs = command.getGeneralArgs();
command.validateArgBounds(generalArgs, 1, 1);
var newCommit = engine.merge(generalArgs[0]);
if (newCommit === undefined) {
// its just a fast forwrard
engine.animationFactory.refreshTree(
engine.animationQueue, engine.gitVisuals
);
return;
}
engine.animationFactory.genCommitBirthAnimation(
engine.animationQueue, newCommit, engine.gitVisuals
);
},
log: function(engine, command) {
var generalArgs = command.getGeneralArgs();
if (generalArgs.length == 2) {
// do fancy git log branchA ^branchB
if (generalArgs[1][0] == '^') {
engine.logWithout(generalArgs[0], generalArgs[1]);
} else {
throw new GitError({
msg: intl.str('git-error-options')
});
}
}
command.oneArgImpliedHead(generalArgs);
engine.log(generalArgs[0]);
},
show: function(engine, command) {
var generalArgs = command.getGeneralArgs();
command.oneArgImpliedHead(generalArgs);
engine.show(generalArgs[0]);
},
rebase: function(engine, command) {
var commandOptions = command.getSupportedMap();
var generalArgs = command.getGeneralArgs();
if (commandOptions['-i']) {
var args = commandOptions['-i'];
command.twoArgsImpliedHead(args, ' -i');
engine.rebaseInteractive(args[0], args[1]);
return;
}
command.twoArgsImpliedHead(generalArgs);
engine.rebase(generalArgs[0], generalArgs[1]);
},
status: function(engine) {
// no parsing at all
engine.status();
},
checkout: function(engine, command) {
var commandOptions = command.getSupportedMap();
var generalArgs = command.getGeneralArgs();
@ -102,6 +337,16 @@ var Commands = {
engine.checkout(engine.crappyUnescape(generalArgs[0]));
},
push: function(engine, command) {
if (!engine.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
command.acceptNoGeneralArgs();
engine.push();
},
noComma: 123
};

View file

@ -35,11 +35,6 @@ function GitEngine(options) {
this.animationFactory = (options.animationFactory) ?
options.animationFactory : AnimationFactory;
// global variable to keep track of the options given
// along with the command call.
this.commandOptions = {};
this.generalArgs = [];
this.initUniqueID();
}
@ -546,68 +541,6 @@ GitEngine.prototype.makeCommit = function(parents, id, options) {
return commit;
};
GitEngine.prototype.acceptNoGeneralArgs = function() {
if (this.generalArgs.length) {
throw new GitError({
msg: intl.str('git-error-no-general-args')
});
}
};
GitEngine.prototype.validateArgBounds = function(args, lower, upper, option) {
// this is a little utility class to help arg validation that happens over and over again
var what = (option === undefined) ?
'git ' + this.command.get('method') :
this.command.get('method') + ' ' + option + ' ';
what = 'with ' + what;
if (args.length < lower) {
throw new GitError({
msg: intl.str(
'git-error-args-few',
{
lower: String(lower),
what: what
}
)
});
}
if (args.length > upper) {
throw new GitError({
msg: intl.str(
'git-error-args-many',
{
upper: String(upper),
what: what
}
)
});
}
};
GitEngine.prototype.oneArgImpliedHead = function(args, option) {
// for log, show, etc
this.validateArgBounds(args, 0, 1, option);
if (args.length === 0) {
args.push('HEAD');
}
};
GitEngine.prototype.twoArgsImpliedHead = function(args, option) {
// our args we expect to be between 1 and 2
this.validateArgBounds(args, 1, 2, option);
// and if it's one, add a HEAD to the back
if (args.length == 1) {
args.push('HEAD');
}
};
GitEngine.prototype.revertStarter = function() {
this.validateArgBounds(this.generalArgs, 1, NaN);
this.revert(this.generalArgs);
};
GitEngine.prototype.revert = function(whichCommits) {
// resolve the commits we will rebase
var toRevert = _.map(whichCommits, function(stringRef) {
@ -659,54 +592,11 @@ GitEngine.prototype.revert = function(whichCommits) {
this.animationQueue.thenFinish(chain, deferred);
};
GitEngine.prototype.resetStarter = function() {
if (this.commandOptions['--soft']) {
throw new GitError({
msg: intl.str('git-error-staging')
});
}
if (this.commandOptions['--hard']) {
this.command.addWarning(
intl.str('git-warning-hard')
);
// dont absorb the arg off of --hard
this.generalArgs = this.generalArgs.concat(this.commandOptions['--hard']);
}
this.validateArgBounds(this.generalArgs, 1, 1);
if (this.getDetachedHead()) {
throw new GitError({
msg: intl.str('git-error-reset-detached')
});
}
this.reset(this.generalArgs[0]);
};
GitEngine.prototype.reset = function(target) {
this.setTargetLocation('HEAD', this.getCommitFromRef(target));
};
GitEngine.prototype.cherrypickStarter = function() {
this.validateArgBounds(this.generalArgs, 1, Number.MAX_VALUE);
var set = this.getUpstreamSet('HEAD');
// first resolve all the refs (as an error check)
var toCherrypick = _.map(this.generalArgs, function(arg) {
var commit = this.getCommitFromRef(arg);
// and check that its not upstream
if (set[commit.get('id')]) {
throw new GitError({
msg: intl.str(
'git-error-already-exists',
{ commit: commit.get('id') }
)
});
}
return commit;
}, this);
GitEngine.prototype.setupCherrypickChain = function(toCherrypick) {
// error checks are all good, lets go!
var deferred = Q.defer();
var chain = deferred.promise;
@ -827,16 +717,6 @@ GitEngine.prototype.getTargetGraphDifference = function(
});
};
GitEngine.prototype.pushStarter = function(options) {
if (!this.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
this.acceptNoGeneralArgs();
this.push();
};
GitEngine.prototype.push = function(options) {
options = options || {};
var localBranch = this.refs['master'];
@ -926,16 +806,6 @@ GitEngine.prototype.push = function(options) {
}
};
GitEngine.prototype.fetchStarter = function() {
if (!this.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
this.acceptNoGeneralArgs();
this.fetch();
};
GitEngine.prototype.fetch = function(options) {
options = options || {};
var localBranch = this.refs['o/master'];
@ -1026,18 +896,8 @@ GitEngine.prototype.fetch = function(options) {
};
};
GitEngine.prototype.pullStarter = function() {
if (!this.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
this.acceptNoGeneralArgs();
// eventually args go here
this.pull();
};
GitEngine.prototype.pull = function() {
GitEngine.prototype.pull = function(options) {
options = options || {};
var localBranch = this.refs['master'];
var remoteBranch = this.refs['o/master'];
@ -1047,7 +907,7 @@ GitEngine.prototype.pull = function() {
dontThrowOnNoFetch: true
});
// then either rebase or merge
if (this.commandOptions['--rebase']) {
if (options.isRebase) {
this.pullFinishWithRebase(pendingFetch, localBranch, remoteBranch);
} else {
this.pullFinishWithMerge(pendingFetch, localBranch, remoteBranch);
@ -1136,35 +996,6 @@ GitEngine.prototype.pullFinishWithMerge = function(
this.animationQueue.thenFinish(chain, deferred);
};
GitEngine.prototype.cloneStarter = function() {
this.acceptNoGeneralArgs();
this.makeOrigin(this.printTree());
};
GitEngine.prototype.fakeTeamworkStarter = function() {
if (!this.hasOrigin()) {
throw new GitError({
msg: intl.str('git-error-origin-required')
});
}
this.validateArgBounds(this.generalArgs, 0, 2);
// allow formats of: git Faketeamwork 2 or git Faketeamwork side 3
var branch = (this.origin.refs[this.generalArgs[0]]) ?
this.generalArgs[0] : 'master';
var numToMake = parseInt(this.generalArgs[0], 10) || this.generalArgs[1] || 1;
// make sure its a branch and exists
var destBranch = this.origin.resolveID(branch);
if (destBranch.get('type') !== 'branch') {
throw new GitError({
msg: intl.str('git-error-options')
});
}
this.fakeTeamwork(numToMake, branch);
};
GitEngine.prototype.fakeTeamwork = function(numToMake, branch) {
var makeOriginCommit = _.bind(function() {
var id = this.getUniqueID();
@ -1209,12 +1040,13 @@ GitEngine.prototype.cherrypick = function(commit) {
return newCommit;
};
GitEngine.prototype.commit = function() {
GitEngine.prototype.commit = function(options) {
options = options || {};
var targetCommit = this.getCommitFromRef(this.HEAD);
var id = null;
// if we want to ammend, go one above
if (this.commandOptions['--amend']) {
if (options.isAmend) {
targetCommit = this.resolveID('HEAD~1');
id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
}
@ -1511,23 +1343,6 @@ GitEngine.prototype.dateSortFunc = function(cA, cB) {
return dateA - dateB;
};
GitEngine.prototype.rebaseInteractiveStarter = function() {
var args = this.commandOptions['-i'];
this.twoArgsImpliedHead(args, ' -i');
this.rebaseInteractive(args[0], args[1]);
};
GitEngine.prototype.rebaseStarter = function() {
if (this.commandOptions['-i']) {
this.rebaseInteractiveStarter();
return;
}
this.twoArgsImpliedHead(this.generalArgs);
this.rebase(this.generalArgs[0], this.generalArgs[1]);
};
GitEngine.prototype.rebase = function(targetSource, currentLocation, options) {
// first some conditions
if (this.isUpstreamOf(targetSource, currentLocation)) {
@ -1755,20 +1570,6 @@ GitEngine.prototype.rebaseFinish = function(
return chain;
};
GitEngine.prototype.mergeStarter = function() {
this.validateArgBounds(this.generalArgs, 1, 1);
var newCommit = this.merge(this.generalArgs[0]);
if (newCommit === undefined) {
// its just a fast forwrard
this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
return;
}
this.animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
};
GitEngine.prototype.merge = function(targetSource) {
var currentLocation = 'HEAD';
@ -1837,53 +1638,6 @@ GitEngine.prototype.checkout = function(idOrTarget) {
this.HEAD.set('target', target);
};
GitEngine.prototype.branchStarter = function() {
var args = null;
// handle deletion first
if (this.commandOptions['-d'] || this.commandOptions['-D']) {
var names = this.commandOptions['-d'] || this.commandOptions['-D'];
this.validateArgBounds(names, 1, NaN, '-d');
_.each(names, function(name) {
this.deleteBranch(name);
}, this);
return;
}
if (this.commandOptions['--contains']) {
args = this.commandOptions['--contains'];
this.validateArgBounds(args, 1, 1, '--contains');
this.printBranchesWithout(args[0]);
return;
}
if (this.commandOptions['-f']) {
args = this.commandOptions['-f'];
this.twoArgsImpliedHead(args, '-f');
// we want to force a branch somewhere
this.forceBranch(args[0], args[1]);
return;
}
if (this.generalArgs.length === 0) {
var branches;
if (this.commandOptions['-a']) {
branches = this.getBranches();
} else if (this.commandOptions['-r']) {
branches = this.getRemoteBranches();
} else {
branches = this.getLocalBranches();
}
this.printBranches(branches);
return;
}
this.twoArgsImpliedHead(this.generalArgs);
this.branch(this.generalArgs[0], this.generalArgs[1]);
};
GitEngine.prototype.forceBranch = function(branchName, where) {
branchName = this.crappyUnescape(branchName);
// if branchname doesn't exist...
@ -1966,11 +1720,7 @@ GitEngine.prototype.externalRefresh = function() {
};
GitEngine.prototype.dispatch = function(command, deferred) {
// current command, options, and args are stored in the gitEngine
// for easy reference during processing.
this.command = command;
this.commandOptions = command.get('supportedMap');
this.generalArgs = command.get('generalArgs');
// set up the animation queue
var whenDone = _.bind(function() {
@ -1982,13 +1732,7 @@ GitEngine.prototype.dispatch = function(command, deferred) {
try {
var methodName = command.get('method').replace(/-/g, '');
// first check out module
if (Commands[methodName]) {
Commands[methodName](this, this.command);
} else {
var startName = methodName + 'Starter';
this[startName]();
}
} catch (err) {
this.filterError(err);
// short circuit animation by just setting error and returning
@ -2011,12 +1755,6 @@ GitEngine.prototype.dispatch = function(command, deferred) {
}
};
GitEngine.prototype.showStarter = function() {
this.oneArgImpliedHead(this.generalArgs);
this.show(this.generalArgs[0]);
};
GitEngine.prototype.show = function(ref) {
var commit = this.getCommitFromRef(ref);
@ -2025,7 +1763,8 @@ GitEngine.prototype.show = function(ref) {
});
};
GitEngine.prototype.statusStarter = function() {
GitEngine.prototype.status = function() {
// UGLY todo
var lines = [];
if (this.getDetachedHead()) {
lines.push(intl.str('git-status-detached'));
@ -2049,22 +1788,6 @@ GitEngine.prototype.statusStarter = function() {
});
};
GitEngine.prototype.logStarter = function() {
if (this.generalArgs.length == 2) {
// do fancy git log branchA ^branchB
if (this.generalArgs[1][0] == '^') {
this.logWithout(this.generalArgs[0], this.generalArgs[1]);
} else {
throw new GitError({
msg: intl.str('git-error-options')
});
}
}
this.oneArgImpliedHead(this.generalArgs);
this.log(this.generalArgs[0]);
};
GitEngine.prototype.logWithout = function(ref, omitBranch) {
// slice off the ^branch
omitBranch = omitBranch.slice(1);
@ -2108,12 +1831,6 @@ GitEngine.prototype.log = function(ref, omitSet) {
});
};
GitEngine.prototype.addStarter = function() {
throw new CommandResult({
msg: intl.str('git-error-staging')
});
};
GitEngine.prototype.getCommonAncestor = function(ancestor, cousin) {
if (this.isUpstreamOf(cousin, ancestor)) {
throw new Error('Dont use common ancestor if we are upstream!');

View file

@ -69,6 +69,14 @@ var Command = Backbone.Model.extend({
}
},
oneArgImpliedHead: function(args, option) {
this.validateArgBounds(args, 0, 1, option);
// and if it's one, add a HEAD to the back
if (args.length === 0) {
args.push('HEAD');
}
},
twoArgsImpliedHead: function(args, option) {
// our args we expect to be between 1 and 2
this.validateArgBounds(args, 1, 2, option);

View file

@ -2,17 +2,9 @@ Mega Things
~~~~~~~~~~~~~~~~~~~~~~~~
[-] origin support
Before everything else:
~~~~~~~~~~~~~~~~~~~~~~~~~~
Intl TODO
~~~~~~~~~~~~~~~~~~~
[ ] translation script? might be too late...
Big Things
~~~~~~~~~~~~~~~~~~~~~~~~~
[ ] compare settings for a level!!! integrated into builder...
[ ] get goal visualization to show origin / remote.... hrm :O
[ ] tree pruning
Easier origin things:
@ -30,6 +22,7 @@ Origin things:
Medium things:
~~~~~~~~~~~~~~~~~~~~~~~~~~~
[ ] factor regexes and instant commands into one module
Cases to handle / things to edit
=======================
@ -49,6 +42,10 @@ Ideas for cleaning
Done things:
(I only started this on Dec 17th 2012 to get a better sense of what was done)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[x] get demonstration view to show origin / remote -- z index ugliness
[x] get goal visualization to show origin / remote.... hrm :O
[x] MASSIVE EFFIN REFACTOR of all command options outside of git engine and into separate module
[x] facebook page link :D
[x] tree comparison with origin.... done! not too bad
[x] green refactor tree compare to have map
[x] increase test coverage over everything