diff --git a/build/bundle.js b/build/bundle.js
index 443b9be3..646a2b8b 100644
--- a/build/bundle.js
+++ b/build/bundle.js
@@ -402,7 +402,7 @@ require.define("/animationFactory.js",function(require,module,exports,__dirname,
*/
// essentially a static class
-function AnimationFactory() {
+var AnimationFactory = function() {
}
@@ -645,10 +645,2819 @@ AnimationFactory.prototype.genFromToSnapshotAnimation = function(
exports.AnimationFactory = AnimationFactory;
+});
+
+require.define("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit;
+var Branch = require('./git').Branch;
+
+var CommitCollection = Backbone.Collection.extend({
+ model: Commit
+});
+
+var CommandCollection = Backbone.Collection.extend({
+ model: Command,
+});
+
+var BranchCollection = Backbone.Collection.extend({
+ model: Branch
+});
+
+var CommandEntryCollection = Backbone.Collection.extend({
+ model: CommandEntry,
+ localStorage: new Backbone.LocalStorage('CommandEntries')
+});
+
+var CommandBuffer = Backbone.Model.extend({
+ defaults: {
+ collection: null,
+ },
+
+ initialize: function(options) {
+ events.on('gitCommandReady', _.bind(
+ this.addCommand, this
+ ));
+
+ options.collection.bind('add', this.addCommand, this);
+
+ this.buffer = [];
+ this.timeout = null;
+ },
+
+ addCommand: function(command) {
+ this.buffer.push(command);
+ this.touchBuffer();
+ },
+
+ touchBuffer: function() {
+ // touch buffer just essentially means we just check if our buffer is being
+ // processed. if it's not, we immediately process the first item
+ // and then set the timeout.
+ if (this.timeout) {
+ // timeout existence implies its being processed
+ return;
+ }
+ this.setTimeout();
+ },
+
+
+ setTimeout: function() {
+ this.timeout = setTimeout(_.bind(function() {
+ this.sipFromBuffer();
+ }, this), TIME.betweenCommandsDelay);
+ },
+
+ popAndProcess: function() {
+ var popped = this.buffer.shift(0);
+ var callback = _.bind(function() {
+ this.setTimeout();
+ }, this);
+
+ // find a command with no error
+ while (popped.get('error') && this.buffer.length) {
+ popped = this.buffer.pop();
+ }
+ if (!popped.get('error')) {
+ // pass in a callback, so when this command is "done" we will process the next.
+ events.trigger('processCommand', popped, callback);
+ } else {
+ this.clear();
+ }
+ },
+
+ clear: function() {
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ },
+
+ sipFromBuffer: function() {
+ if (!this.buffer.length) {
+ this.clear();
+ return;
+ }
+
+ this.popAndProcess();
+ },
+});
+
+exports.CommitCollection = CommitCollection;
+exports.CommandCollection = CommandCollection;
+exports.BranchCollection = BranchCollection;
+exports.CommandEntryCollection = CommandEntryCollection;
+exports.CommandBuffer = CommandBuffer;
+
+
+});
+
+require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var animationFactory = new require('./animationFactory').AnimationFactory();
+
+// backbone or something uses _.uniqueId, so we make our own here
+var uniqueId = (function() {
+ var n = 0;
+ return function(prepend) {
+ return prepend? prepend + n++ : n++;
+ };
+})();
+
+function GitEngine(options) {
+ this.rootCommit = null;
+ this.refs = {};
+ this.HEAD = null;
+
+ this.branchCollection = options.branches;
+ this.commitCollection = options.collection;
+ this.gitVisuals = options.gitVisuals;
+
+ // global variable to keep track of the options given
+ // along with the command call.
+ this.commandOptions = {};
+ this.generalArgs = [];
+
+ events.on('processCommand', _.bind(this.dispatch, this));
+}
+
+GitEngine.prototype.defaultInit = function() {
+ var defaultTree = JSON.parse(unescape("%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22master%22%2C%22type%22%3A%22general%20ref%22%7D%7D"));
+ this.loadTree(defaultTree);
+};
+
+GitEngine.prototype.init = function() {
+ // make an initial commit and a master branch
+ this.rootCommit = this.makeCommit(null, null, {rootCommit: true});
+ this.commitCollection.add(this.rootCommit);
+
+ var master = this.makeBranch('master', this.rootCommit);
+ this.HEAD = new Ref({
+ id: 'HEAD',
+ target: master
+ });
+ this.refs[this.HEAD.get('id')] = this.HEAD;
+
+ // commit once to get things going
+ this.commit();
+};
+
+GitEngine.prototype.exportTree = function() {
+ // need to export all commits, their connectivity / messages, branches, and state of head.
+ // this would be simple if didn't have circular structures.... :P
+ // thus, we need to loop through and "flatten" our graph of objects referencing one another
+ var totalExport = {
+ branches: {},
+ commits: {},
+ HEAD: null
+ };
+
+ _.each(this.branchCollection.toJSON(), function(branch) {
+ branch.target = branch.target.get('id');
+ branch.visBranch = undefined;
+
+ totalExport.branches[branch.id] = branch;
+ });
+
+ _.each(this.commitCollection.toJSON(), function(commit) {
+ // clear out the fields that reference objects and create circular structure
+ _.each(Commit.prototype.constants.circularFields, function(field) {
+ commit[field] = undefined;
+ }, this);
+
+ // convert parents
+ var parents = [];
+ _.each(commit.parents, function(par) {
+ parents.push(par.get('id'));
+ });
+ commit.parents = parents;
+
+ totalExport.commits[commit.id] = commit;
+ }, this);
+
+ var HEAD = this.HEAD.toJSON();
+ HEAD.visBranch = undefined;
+ HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = undefined;
+ HEAD.target = HEAD.target.get('id');
+ totalExport.HEAD = HEAD;
+
+ return totalExport;
+};
+
+GitEngine.prototype.printTree = function() {
+ var str = escape(JSON.stringify(this.exportTree()));
+ return str;
+};
+
+GitEngine.prototype.printAndCopyTree = function() {
+ window.prompt('Copy the tree string below', this.printTree());
+};
+
+GitEngine.prototype.loadTree = function(tree) {
+ // deep copy in case we use it a bunch
+ tree = $.extend(true, {}, tree);
+
+ // first clear everything
+ this.removeAll();
+
+ this.instantiateFromTree(tree);
+
+ this.reloadGraphics();
+};
+
+GitEngine.prototype.loadTreeFromString = function(treeString) {
+ this.loadTree(JSON.parse(unescape(treeString)));
+};
+
+GitEngine.prototype.instantiateFromTree = function(tree) {
+ // now we do the loading part
+ var createdSoFar = {};
+
+ _.each(tree.commits, function(commitJSON) {
+ var commit = this.getOrMakeRecursive(tree, createdSoFar, commitJSON.id);
+ this.commitCollection.add(commit);
+ }, this);
+
+ _.each(tree.branches, function(branchJSON) {
+ var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id);
+
+ this.branchCollection.add(branch, {silent: true});
+ }, this);
+
+ var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id);
+ this.HEAD = HEAD;
+
+ this.rootCommit = createdSoFar['C0'];
+ if (!this.rootCommit) {
+ throw new Error('Need root commit of C0 for calculations');
+ }
+ this.refs = createdSoFar;
+
+ this.branchCollection.each(function(branch) {
+ this.gitVisuals.addBranch(branch);
+ }, this);
+};
+
+GitEngine.prototype.reloadGraphics = function() {
+ // get the root commit, no better way to do it
+ var rootCommit = null;
+ this.commitCollection.each(function(commit) {
+ if (commit.get('id') == 'C0') {
+ rootCommit = commit;
+ }
+ });
+ this.gitVisuals.rootCommit = rootCommit;
+
+ // this just basically makes the HEAD branch. the head branch really should have been
+ // a member of a collection and not this annoying edge case stuff... one day
+ this.gitVisuals.initHeadBranch();
+
+ // when the paper is ready
+ this.gitVisuals.drawTreeFromReload();
+
+ this.gitVisuals.refreshTreeHarsh();
+};
+
+GitEngine.prototype.getOrMakeRecursive = function(tree, createdSoFar, objID) {
+ if (createdSoFar[objID]) {
+ // base case
+ return createdSoFar[objID];
+ }
+
+ var getType = function(tree, id) {
+ if (tree.commits[id]) {
+ return 'commit';
+ } else if (tree.branches[id]) {
+ return 'branch';
+ } else if (id == 'HEAD') {
+ return 'HEAD';
+ }
+ throw new Error("bad type for " + id);
+ };
+
+ // figure out what type
+ var type = getType(tree, objID);
+
+ if (type == 'HEAD') {
+ var headJSON = tree.HEAD;
+ var HEAD = new Ref(_.extend(
+ tree.HEAD,
+ {
+ target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target)
+ }
+ ));
+ createdSoFar[objID] = HEAD;
+ return HEAD;
+ }
+
+ if (type == 'branch') {
+ var branchJSON = tree.branches[objID];
+
+ var branch = new Branch(_.extend(
+ tree.branches[objID],
+ {
+ target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target)
+ }
+ ));
+ createdSoFar[objID] = branch;
+ return branch;
+ }
+
+ if (type == 'commit') {
+ // for commits, we need to grab all the parents
+ var commitJSON = tree.commits[objID];
+
+ var parentObjs = [];
+ _.each(commitJSON.parents, function(parentID) {
+ parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID));
+ }, this);
+
+ var commit = new Commit(_.extend(
+ commitJSON,
+ {
+ parents: parentObjs,
+ gitVisuals: this.gitVisuals
+ }
+ ));
+ createdSoFar[objID] = commit;
+ return commit;
+ }
+
+ throw new Error('ruh rho!! unsupported tyep for ' + objID);
+};
+
+GitEngine.prototype.removeAll = function() {
+ this.branchCollection.reset();
+ this.commitCollection.reset();
+ this.refs = {};
+ this.HEAD = null;
+ this.rootCommit = null;
+
+ this.gitVisuals.resetAll();
+};
+
+GitEngine.prototype.getDetachedHead = function() {
+ // detached head is if HEAD points to a commit instead of a branch...
+ var target = this.HEAD.get('target');
+ var targetType = target.get('type');
+ return targetType !== 'branch';
+};
+
+GitEngine.prototype.validateBranchName = function(name) {
+ name = name.replace(/\s/g, '');
+ if (!/^[a-zA-Z0-9]+$/.test(name)) {
+ throw new GitError({
+ msg: 'woah bad branch name!! This is not ok: ' + name
+ });
+ }
+ if (/[hH][eE][aA][dD]/.test(name)) {
+ throw new GitError({
+ msg: 'branch name of "head" is ambiguous, dont name it that'
+ });
+ }
+ if (name.length > 9) {
+ name = name.slice(0, 9);
+ this.command.addWarning(
+ 'Sorry, we need to keep branch names short for the visuals. Your branch ' +
+ 'name was truncated to 9 characters, resulting in ' + name
+ );
+ }
+ return name;
+};
+
+GitEngine.prototype.makeBranch = function(id, target) {
+ id = this.validateBranchName(id);
+ if (this.refs[id]) {
+ throw new GitError({
+ msg: 'that branch id either matches a commit hash or already exists!'
+ });
+ }
+
+ var branch = new Branch({
+ target: target,
+ id: id
+ });
+ this.branchCollection.add(branch);
+ this.refs[branch.get('id')] = branch;
+ return branch;
+};
+
+GitEngine.prototype.getHead = function() {
+ return _.clone(this.HEAD);
+};
+
+GitEngine.prototype.getBranches = function() {
+ var toReturn = [];
+ this.branchCollection.each(function(branch) {
+ toReturn.push({
+ id: branch.get('id'),
+ selected: this.HEAD.get('target') === branch,
+ target: branch.get('target'),
+ obj: branch
+ });
+ }, this);
+ return toReturn;
+};
+
+GitEngine.prototype.printBranchesWithout = function(without) {
+ var commitToBranches = this.getUpstreamBranchSet();
+ var commitID = this.getCommitFromRef(without).get('id');
+
+ var toPrint = [];
+ _.each(commitToBranches[commitID], function(branchJSON) {
+ branchJSON.selected = this.HEAD.get('target').get('id') == branchJSON.id;
+ toPrint.push(branchJSON);
+ }, this);
+ this.printBranches(toPrint);
+};
+
+GitEngine.prototype.printBranches = function(branches) {
+ var result = '';
+ _.each(branches, function(branch) {
+ result += (branch.selected ? '* ' : '') + branch.id + '\n';
+ });
+ throw new CommandResult({
+ msg: result
+ });
+};
+
+GitEngine.prototype.makeCommit = function(parents, id, options) {
+ // ok we need to actually manually create commit IDs now because
+ // people like nikita (thanks for finding this!) could
+ // make branches named C2 before creating the commit C2
+ if (!id) {
+ id = uniqueId('C');
+ while (this.refs[id]) {
+ id = uniqueId('C');
+ }
+ }
+
+ var commit = new Commit(_.extend({
+ parents: parents,
+ id: id,
+ gitVisuals: this.gitVisuals
+ },
+ options || {}
+ ));
+
+ this.refs[commit.get('id')] = commit;
+ this.commitCollection.add(commit);
+ return commit;
+};
+
+GitEngine.prototype.acceptNoGeneralArgs = function() {
+ if (this.generalArgs.length) {
+ throw new GitError({
+ msg: "That command accepts no general arguments"
+ });
+ }
+};
+
+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: 'I expect at least ' + String(lower) + ' argument(s) ' + what
+ });
+ }
+ if (args.length > upper) {
+ throw new GitError({
+ msg: 'I expect at most ' + String(upper) + ' argument(s) ' + 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);
+
+ var response = this.revert(this.generalArgs);
+
+ if (response) {
+ animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
+ }
+};
+
+GitEngine.prototype.revert = function(whichCommits) {
+ // for each commit, we want to revert it
+ var toRebase = [];
+ _.each(whichCommits, function(stringRef) {
+ toRebase.push(this.getCommitFromRef(stringRef));
+ }, this);
+
+ // we animate reverts now!! we use the rebase animation though so that's
+ // why the terminology is like it is
+ var animationResponse = {};
+ animationResponse.destinationBranch = this.resolveID(toRebase[0]);
+ animationResponse.toRebaseArray = toRebase.slice(0);
+ animationResponse.rebaseSteps = [];
+
+ beforeSnapshot = this.gitVisuals.genSnapshot();
+ var afterSnapshot;
+
+ // now make a bunch of commits on top of where we are
+ var base = this.getCommitFromRef('HEAD');
+ _.each(toRebase, function(oldCommit) {
+ var newId = this.rebaseAltID(oldCommit.get('id'));
+
+ var newCommit = this.makeCommit([base], newId, {
+ commitMessage: 'Reverting ' + this.resolveName(oldCommit) +
+ ': "' + oldCommit.get('commitMessage') + '"'
+ });
+
+ base = newCommit;
+
+ // animation stuff
+ afterSnapshot = this.gitVisuals.genSnapshot();
+ animationResponse.rebaseSteps.push({
+ oldCommit: oldCommit,
+ newCommit: newCommit,
+ beforeSnapshot: beforeSnapshot,
+ afterSnapshot: afterSnapshot
+ });
+ beforeSnapshot = afterSnapshot;
+ }, this);
+ // done! update our location
+ this.setTargetLocation('HEAD', base);
+
+ // animation
+ return animationResponse;
+};
+
+GitEngine.prototype.resetStarter = function() {
+ if (this.commandOptions['--soft']) {
+ throw new GitError({
+ msg: "You can't use --soft because there is no concept of stashing" +
+ " changes or staging files, so you will lose your progress." +
+ " Try using interactive rebasing (or just rebasing) to move commits."
+ });
+ }
+ if (this.commandOptions['--hard']) {
+ this.command.addWarning(
+ 'Nice! You are using --hard. The default behavior is a hard reset in ' +
+ "this demo, so don't worry about specifying the option explicity"
+ );
+ // 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: "Cant reset in detached head! Use checkout if you want to move"
+ });
+ }
+
+ 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, 1);
+ var newCommit = this.cherrypick(this.generalArgs[0]);
+
+ animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
+};
+
+GitEngine.prototype.cherrypick = function(ref) {
+ var commit = this.getCommitFromRef(ref);
+ // check if we already have that
+ var set = this.getUpstreamSet('HEAD');
+ if (set[commit.get('id')]) {
+ throw new GitError({
+ msg: "We already have that commit in our changes history! You can't cherry-pick it " +
+ "if it shows up in git log."
+ });
+ }
+
+ // alter the ID slightly
+ var id = this.rebaseAltID(commit.get('id'));
+
+ // now commit with that id onto HEAD
+ var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id);
+ this.setTargetLocation(this.HEAD, newCommit);
+ return newCommit;
+};
+
+GitEngine.prototype.commitStarter = function() {
+ this.acceptNoGeneralArgs();
+ if (this.commandOptions['-am'] && (
+ this.commandOptions['-a'] || this.commandOptions['-m'])) {
+ throw new GitError({
+ msg: "You can't have -am with another -m or -a!"
+ });
+ }
+
+ var msg = null;
+ if (this.commandOptions['-a']) {
+ this.command.addWarning('No need to add files in this demo');
+ }
+
+ if (this.commandOptions['-am']) {
+ var args = this.commandOptions['-am'];
+ this.validateArgBounds(args, 1, 1, '-am');
+
+ this.command.addWarning("Don't worry about adding files in this demo. I'll take " +
+ "down your commit message anyways, but you can commit without a message " +
+ "in this demo as well");
+ msg = args[0];
+ }
+
+ if (this.commandOptions['-m']) {
+ var args = this.commandOptions['-m'];
+ this.validateArgBounds(args, 1, 1, '-m');
+ msg = args[0];
+ }
+
+ var newCommit = this.commit();
+ if (msg) {
+ msg = msg
+ .replace(/"/g, '"')
+ .replace(/^"/g, '')
+ .replace(/"$/g, '');
+
+ newCommit.set('commitMessage', msg);
+ }
+ animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
+};
+
+GitEngine.prototype.commit = function() {
+ var targetCommit = this.getCommitFromRef(this.HEAD);
+ var id = undefined;
+
+ // if we want to ammend, go one above
+ if (this.commandOptions['--amend']) {
+ targetCommit = this.resolveID('HEAD~1');
+ id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
+ }
+
+ var newCommit = this.makeCommit([targetCommit], id);
+ if (this.getDetachedHead()) {
+ this.command.addWarning('Warning!! Detached HEAD state');
+ }
+
+ this.setTargetLocation(this.HEAD, newCommit);
+ return newCommit;
+};
+
+GitEngine.prototype.resolveName = function(someRef) {
+ // first get the obj
+ var obj = this.resolveID(someRef);
+ if (obj.get('type') == 'commit') {
+ return 'commit ' + obj.get('id');
+ }
+ if (obj.get('type') == 'branch') {
+ return 'branch "' + obj.get('id') + '"';
+ }
+ // we are dealing with HEAD
+ return this.resolveName(obj.get('target'));
+};
+
+GitEngine.prototype.resolveID = function(idOrTarget) {
+ if (idOrTarget === null || idOrTarget === undefined) {
+ throw new Error('Dont call this with null / undefined');
+ }
+
+ if (typeof idOrTarget !== 'string') {
+ return idOrTarget;
+ }
+ return this.resolveStringRef(idOrTarget);
+};
+
+GitEngine.prototype.resolveStringRef = function(ref) {
+ if (this.refs[ref]) {
+ return this.refs[ref];
+ }
+
+ // may be something like HEAD~2 or master^^
+ var relativeRefs = [
+ [/^([a-zA-Z0-9]+)~(\d+)\s*$/, function(matches) {
+ return parseInt(matches[2]);
+ }],
+ [/^([a-zA-Z0-9]+)(\^+)\s*$/, function(matches) {
+ return matches[2].length;
+ }]
+ ];
+
+ var startRef = null;
+ var numBack = null;
+ _.each(relativeRefs, function(config) {
+ var regex = config[0];
+ var parse = config[1];
+ if (regex.test(ref)) {
+ var matches = regex.exec(ref);
+ numBack = parse(matches);
+ startRef = matches[1];
+ }
+ }, this);
+
+ if (!startRef) {
+ throw new GitError({
+ msg: 'unknown ref ' + ref
+ });
+ }
+ if (!this.refs[startRef]) {
+ throw new GitError({
+ msg: 'the ref ' + startRef +' does not exist.'
+ });
+ }
+ var commit = this.getCommitFromRef(startRef);
+
+ return this.numBackFrom(commit, numBack);
+};
+
+GitEngine.prototype.getCommitFromRef = function(ref) {
+ var start = this.resolveID(ref);
+
+ // works for both HEAD and just a single layer. aka branch
+ while (start.get('type') !== 'commit') {
+ start = start.get('target');
+ }
+ return start;
+};
+
+GitEngine.prototype.getType = function(ref) {
+ return this.resolveID(ref).get('type');
+};
+
+GitEngine.prototype.setTargetLocation = function(ref, target) {
+ if (this.getType(ref) == 'commit') {
+ // nothing to do
+ return;
+ }
+
+ // sets whatever ref is (branch, HEAD, etc) to a target. so if
+ // you pass in HEAD, and HEAD is pointing to a branch, it will update
+ // the branch to that commit, not the HEAD
+ var ref = this.getOneBeforeCommit(ref);
+ ref.set('target', target);
+};
+
+GitEngine.prototype.getUpstreamBranchSet = function() {
+ // this is expensive!! so only call once in a while
+ var commitToSet = {};
+
+ var inArray = function(arr, id) {
+ var found = false;
+ _.each(arr, function(wrapper) {
+ if (wrapper.id == id) {
+ found = true;
+ }
+ });
+
+ return found;
+ };
+
+ var bfsSearch = function(commit) {
+ var set = [];
+ var pQueue = [commit];
+ while (pQueue.length) {
+ var popped = pQueue.pop();
+ set.push(popped.get('id'));
+
+ if (popped.get('parents') && popped.get('parents').length) {
+ pQueue = pQueue.concat(popped.get('parents'));
+ }
+ }
+ return set;
+ };
+
+ this.branchCollection.each(function(branch) {
+ var set = bfsSearch(branch.get('target'));
+ _.each(set, function(id) {
+ commitToSet[id] = commitToSet[id] || [];
+
+ // only add it if it's not there, so hue blending is ok
+ if (!inArray(commitToSet[id], branch.get('id'))) {
+ commitToSet[id].push({
+ obj: branch,
+ id: branch.get('id')
+ });
+ }
+ });
+ });
+
+ return commitToSet;
+};
+
+GitEngine.prototype.getUpstreamHeadSet = function() {
+ var set = this.getUpstreamSet('HEAD');
+ var including = this.getCommitFromRef('HEAD').get('id');
+
+ set[including] = true;
+ return set;
+};
+
+GitEngine.prototype.getOneBeforeCommit = function(ref) {
+ // you can call this command on HEAD in detached, HEAD, or on a branch
+ // and it will return the ref that is one above a commit. aka
+ // it resolves HEAD to something that we can move the ref with
+ var start = this.resolveID(ref);
+ if (start === this.HEAD && !this.getDetachedHead()) {
+ start = start.get('target');
+ }
+ return start;
+};
+
+GitEngine.prototype.numBackFrom = function(commit, numBack) {
+ // going back '3' from a given ref is not trivial, for you might have
+ // a bunch of merge commits and such. like this situation:
+ //
+ // * merge master into new
+ // |\
+ // | \* commit here
+ // |* \ commit there
+ // | |* commit here
+ // \ /
+ // | * root
+ //
+ //
+ // hence we need to do a BFS search, with the commit date being the
+ // value to sort off of (rather than just purely the level)
+ if (numBack == 0) {
+ return commit;
+ }
+
+ // we use a special sorting function here that
+ // prefers the later commits over the earlier ones
+ var sortQueue = _.bind(function(queue) {
+ queue.sort(this.idSortFunc);
+ queue.reverse();
+ }, this);
+
+ var pQueue = [].concat(commit.get('parents') || []);
+ sortQueue(pQueue);
+ numBack--;
+
+ while (pQueue.length && numBack !== 0) {
+ var popped = pQueue.shift(0);
+ var parents = popped.get('parents');
+
+ if (parents && parents.length) {
+ pQueue = pQueue.concat(parents);
+ }
+
+ sortQueue(pQueue);
+ numBack--;
+ }
+
+ if (numBack !== 0 || pQueue.length == 0) {
+ throw new GitError({
+ msg: "Sorry, I can't go that many commits back"
+ });
+ }
+ return pQueue.shift(0);
+};
+
+GitEngine.prototype.scrapeBaseID = function(id) {
+ var results = /^C(\d+)/.exec(id);
+
+ if (!results) {
+ throw new Error('regex failed on ' + id);
+ }
+
+ return 'C' + results[1];
+};
+
+GitEngine.prototype.rebaseAltID = function(id) {
+ // this function alters an ID to add a quote to the end,
+ // indicating that it was rebased. it also checks existence
+ var regexMap = [
+ [/^C(\d+)[']{0,2}$/, function(bits) {
+ // this id can use another quote, so just add it
+ return bits[0] + "'";
+ }],
+ [/^C(\d+)[']{3}$/, function(bits) {
+ // here we switch from C''' to C'^4
+ return bits[0].slice(0, -3) + "'^4";
+ }],
+ [/^C(\d+)['][^](\d+)$/, function(bits) {
+ return 'C' + String(bits[1]) + "'^" + String(Number(bits[2]) + 1);
+ }]
+ ];
+
+ for (var i = 0; i < regexMap.length; i++) {
+ var regex = regexMap[i][0];
+ var func = regexMap[i][1];
+ var results = regex.exec(id);
+ if (results) {
+ var newId = func(results);
+ // if this id exists, continue down the rabbit hole
+ if (this.refs[newId]) {
+ return this.rebaseAltID(newId);
+ } else {
+ return newId;
+ }
+ }
+ }
+ throw new Error('could not modify the id ' + id);
+};
+
+GitEngine.prototype.idSortFunc = function(cA, cB) {
+ // commit IDs can come in many forms:
+ // C4
+ // C4' (from a rebase)
+ // C4'' (from multiple rebases)
+ // C4'^3 (from a BUNCH of rebases)
+
+ var scale = 1000;
+
+ var regexMap = [
+ [/^C(\d+)$/, function(bits) {
+ // return the 4 from C4
+ return scale * bits[1];
+ }],
+ [/^C(\d+)([']+)$/, function(bits) {
+ // return the 4 from C4, plus the length of the quotes
+ return scale * bits[1] + bits[2].length;
+ }],
+ [/^C(\d+)['][^](\d+)$/, function(bits) {
+ return scale * bits[1] + Number(bits[2]);
+ }]
+ ];
+
+ var getNumToSort = function(id) {
+ for (var i = 0; i < regexMap.length; i++) {
+ var regex = regexMap[i][0];
+ var func = regexMap[i][1];
+ var results = regex.exec(id);
+ if (results) {
+ return func(results);
+ }
+ }
+ throw new Error('Could not parse commit ID ' + id);
+ }
+
+ return getNumToSort(cA.get('id')) - getNumToSort(cB.get('id'));
+};
+
+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);
+
+ var response = this.rebase(this.generalArgs[0], this.generalArgs[1]);
+
+ if (response === undefined) {
+ // was a fastforward or already up to date. returning now
+ // will trigger the refresh animation by not adding anything to
+ // the animation queue
+ return;
+ }
+
+ animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
+};
+
+GitEngine.prototype.rebase = function(targetSource, currentLocation) {
+ // first some conditions
+ if (this.isUpstreamOf(targetSource, currentLocation)) {
+ this.command.setResult('Branch already up-to-date');
+
+ // git for some reason always checks out the branch you are rebasing,
+ // no matter the result of the rebase
+ this.checkout(currentLocation);
+
+ // returning instead of throwing makes a tree refresh
+ return;
+ }
+
+ if (this.isUpstreamOf(currentLocation, targetSource)) {
+ // just set the target of this current location to the source
+ this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
+ // we need the refresh tree animation to happen, so set the result directly
+ // instead of throwing
+ this.command.setResult('Fast-forwarding...');
+
+ this.checkout(currentLocation);
+ return;
+ }
+
+ // now the part of actually rebasing.
+ // We need to get the downstream set of targetSource first.
+ // then we BFS from currentLocation, using the downstream set as our stopping point.
+ // we need to BFS because we need to include all commits below
+ // pop these commits on top of targetSource and modify their ids with quotes
+ var stopSet = this.getUpstreamSet(targetSource)
+
+ // now BFS from here on out
+ var toRebaseRough = [];
+ var pQueue = [this.getCommitFromRef(currentLocation)];
+
+ while (pQueue.length) {
+ var popped = pQueue.pop();
+
+ // if its in the set, dont add it
+ if (stopSet[popped.get('id')]) {
+ continue;
+ }
+
+ // it's not in the set, so we need to rebase this commit
+ toRebaseRough.push(popped);
+ toRebaseRough.sort(this.idSortFunc);
+ toRebaseRough.reverse();
+ // keep searching
+ pQueue = pQueue.concat(popped.get('parents'));
+ }
+
+ return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation);
+};
+
+GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation) {
+ // there are a reduced set of checks now, so we can't exactly use parts of the rebase function
+ // but it will look similar.
+
+ // first if we are upstream of the target
+ if (this.isUpstreamOf(currentLocation, targetSource)) {
+ throw new GitError({
+ msg: 'Nothing to do... (git throws a "noop" status here); ' +
+ 'Your source is upstream of your rebase target'
+ });
+ }
+
+ // now get the stop set
+ var stopSet = this.getUpstreamSet(targetSource);
+
+ var toRebaseRough = [];
+ // standard BFS
+ var pQueue = [this.getCommitFromRef(currentLocation)];
+
+ while (pQueue.length) {
+ var popped = pQueue.pop();
+
+ if (stopSet[popped.get('id')]) {
+ continue;
+ }
+
+ toRebaseRough.push(popped);
+ pQueue = pQueue.concat(popped.get('parents'));
+ pQueue.sort(this.idSortFunc);
+ }
+
+ // throw our merge's real fast and see if we have anything to do
+ var toRebase = [];
+ _.each(toRebaseRough, function(commit) {
+ if (commit.get('parents').length == 1) {
+ toRebase.push(commit);
+ }
+ });
+
+ if (!toRebase.length) {
+ throw new GitError({
+ msg: 'No commits to rebase! Everything is a merge commit'
+ });
+ }
+
+ // now do stuff :D since all our validation checks have passed, we are going to defer animation
+ // and actually launch the dialog
+ this.animationQueue.set('defer', true);
+
+ var callback = _.bind(function(userSpecifiedRebase) {
+ // first, they might have dropped everything (annoying)
+ if (!userSpecifiedRebase.length) {
+ this.command.setResult('Nothing to do...');
+ this.animationQueue.start();
+ return;
+ }
+
+ // finish the rebase crap and animate!
+ var animationData = this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation);
+ animationFactory.rebaseAnimation(this.animationQueue, animationData, this, this.gitVisuals);
+ this.animationQueue.start();
+ }, this);
+
+ new InteractiveRebaseView({
+ callback: callback,
+ toRebase: toRebase,
+ el: $('#dialogHolder')
+ });
+};
+
+GitEngine.prototype.rebaseFinish = function(toRebaseRough, stopSet, targetSource, currentLocation) {
+ // now we have the all the commits between currentLocation and the set of target to rebase.
+ var animationResponse = {};
+ animationResponse.destinationBranch = this.resolveID(targetSource);
+
+ // we need to throw out merge commits
+ var toRebase = [];
+ _.each(toRebaseRough, function(commit) {
+ if (commit.get('parents').length == 1) {
+ toRebase.push(commit);
+ }
+ });
+
+ // we ALSO need to throw out commits that will do the same changes. like
+ // if the upstream set has a commit C4 and we have C4', we dont rebase the C4' again.
+ // get this by doing ID scraping
+ var changesAlreadyMade = {};
+ _.each(stopSet, function(val, key) {
+ changesAlreadyMade[this.scrapeBaseID(key)] = val; // val == true
+ }, this);
+
+ // now get rid of the commits that will redo same changes
+ toRebaseRough = toRebase;
+ toRebase = [];
+ _.each(toRebaseRough, function(commit) {
+ var baseID = this.scrapeBaseID(commit.get('id'));
+ if (!changesAlreadyMade[baseID]) {
+ toRebase.push(commit);
+ }
+ }, this);
+
+ if (!toRebase.length) {
+ throw new GitError({
+ msg: 'No Commits to Rebase! Everything else is merge commits or changes already have been applied'
+ });
+ }
+
+ // now reverse it once more to get it in the right order
+ toRebase.reverse();
+ animationResponse.toRebaseArray = toRebase.slice(0);
+
+ // now pop all of these commits onto targetLocation
+ var base = this.getCommitFromRef(targetSource);
+
+ // do the rebase, and also maintain all our animation info during this
+ animationResponse.rebaseSteps = [];
+ var beforeSnapshot = this.gitVisuals.genSnapshot();
+ var afterSnapshot;
+ _.each(toRebase, function(old) {
+ var newId = this.rebaseAltID(old.get('id'));
+
+ var newCommit = this.makeCommit([base], newId);
+ base = newCommit;
+
+ // animation info
+ afterSnapshot = this.gitVisuals.genSnapshot();
+ animationResponse.rebaseSteps.push({
+ oldCommit: old,
+ newCommit: newCommit,
+ beforeSnapshot: beforeSnapshot,
+ afterSnapshot: afterSnapshot
+ });
+ beforeSnapshot = afterSnapshot;
+ }, this);
+
+ if (this.resolveID(currentLocation).get('type') == 'commit') {
+ // we referenced a commit like git rebase C2 C1, so we have
+ // to manually check out C1'
+
+ var steps = animationResponse.rebaseSteps;
+ var newestCommit = steps[steps.length - 1].newCommit;
+
+ this.checkout(newestCommit);
+ } else {
+ // now we just need to update the rebased branch is
+ this.setTargetLocation(currentLocation, base);
+ this.checkout(currentLocation);
+ }
+
+ // for animation
+ return animationResponse;
+};
+
+GitEngine.prototype.mergeStarter = function() {
+ this.twoArgsImpliedHead(this.generalArgs);
+
+ var newCommit = this.merge(this.generalArgs[0], this.generalArgs[1]);
+
+ if (newCommit === undefined) {
+ // its just a fast forwrard
+ animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
+ return;
+ }
+
+ animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
+};
+
+GitEngine.prototype.merge = function(targetSource, currentLocation) {
+ // first some conditions
+ if (this.isUpstreamOf(targetSource, currentLocation) ||
+ this.getCommitFromRef(targetSource) === this.getCommitFromRef(currentLocation)) {
+ throw new CommandResult({
+ msg: 'Branch already up-to-date'
+ });
+ }
+
+ if (this.isUpstreamOf(currentLocation, targetSource)) {
+ // just set the target of this current location to the source
+ this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
+ // get fresh animation to happen
+ this.command.setResult('Fast-forwarding...');
+ return;
+ }
+
+ // now the part of making a merge commit
+ var parent1 = this.getCommitFromRef(currentLocation);
+ var parent2 = this.getCommitFromRef(targetSource);
+
+ // we need a fancy commit message
+ var msg = 'Merge ' + this.resolveName(targetSource) +
+ ' into ' + this.resolveName(currentLocation);
+
+ // since we specify parent 1 as the first parent, it is the "main" parent
+ // and the node will be displayed below that branch / commit / whatever
+ var mergeCommit = this.makeCommit(
+ [parent1, parent2],
+ null,
+ {
+ commitMessage: msg
+ }
+ );
+
+ this.setTargetLocation(currentLocation, mergeCommit)
+ return mergeCommit;
+};
+
+GitEngine.prototype.checkoutStarter = function() {
+ if (this.commandOptions['-b']) {
+ // the user is really trying to just make a branch and then switch to it. so first:
+ var args = this.commandOptions['-b'];
+ this.twoArgsImpliedHead(args, '-b');
+
+ var validId = this.validateBranchName(args[0]);
+ this.branch(validId, args[1]);
+ this.checkout(validId);
+ return;
+ }
+
+ if (this.commandOptions['-']) {
+ // get the heads last location
+ var lastPlace = this.HEAD.get('lastLastTarget');
+ if (!lastPlace) {
+ throw new GitError({
+ msg: 'Need a previous location to do - switching'
+ });
+ }
+ this.HEAD.set('target', lastPlace);
+ return;
+ }
+
+ if (this.commandOptions['-B']) {
+ var args = this.commandOptions['-B'];
+ this.twoArgsImpliedHead(args, '-B');
+
+ this.forceBranch(args[0], args[1]);
+ this.checkout(args[0]);
+ return;
+ }
+
+ this.validateArgBounds(this.generalArgs, 1, 1);
+
+ this.checkout(this.unescapeQuotes(this.generalArgs[0]));
+};
+
+GitEngine.prototype.checkout = function(idOrTarget) {
+ var target = this.resolveID(idOrTarget);
+ if (target.get('id') === 'HEAD') {
+ // git checkout HEAD is a
+ // meaningless command but i used to do this back in the day
+ return;
+ }
+
+ var type = target.get('type');
+ if (type !== 'branch' && type !== 'commit') {
+ throw new GitError({
+ msg: 'can only checkout branches and commits!'
+ });
+ }
+
+ this.HEAD.set('target', target);
+};
+
+GitEngine.prototype.branchStarter = function() {
+ // 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']) {
+ var args = this.commandOptions['--contains'];
+ this.validateArgBounds(args, 1, 1, '--contains');
+ this.printBranchesWithout(args[0]);
+ return;
+ }
+
+ if (this.commandOptions['-f']) {
+ var 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) {
+ this.printBranches(this.getBranches());
+ return;
+ }
+
+ this.twoArgsImpliedHead(this.generalArgs);
+ this.branch(this.generalArgs[0], this.generalArgs[1]);
+};
+
+GitEngine.prototype.forceBranch = function(branchName, where) {
+ // if branchname doesn't exist...
+ if (!this.refs[branchName]) {
+ this.branch(branchName, where);
+ }
+
+ var branch = this.resolveID(branchName);
+ if (branch.get('type') !== 'branch') {
+ throw new GitError({
+ msg: "Can't force move anything but a branch!!"
+ });
+ }
+
+ var whereCommit = this.getCommitFromRef(where);
+
+ this.setTargetLocation(branch, whereCommit);
+};
+
+GitEngine.prototype.branch = function(name, ref) {
+ var target = this.getCommitFromRef(ref);
+ this.makeBranch(name, target);
+};
+
+GitEngine.prototype.deleteBranch = function(name) {
+ // trying to delete, lets check our refs
+ var target = this.resolveID(name);
+ if (target.get('type') !== 'branch') {
+ throw new GitError({
+ msg: "You can't delete things that arent branches with branch command"
+ });
+ }
+ if (target.get('id') == 'master') {
+ throw new GitError({
+ msg: "You can't delete the master branch!"
+ });
+ }
+ if (this.HEAD.get('target') === target) {
+ throw new GitError({
+ msg: "Cannot delete the branch you are currently on"
+ });
+ }
+
+ // now we know it's a branch
+ var branch = target;
+
+ this.branchCollection.remove(branch);
+ this.refs[branch.get('id')] = undefined;
+ delete this.refs[branch.get('id')];
+
+ if (branch.get('visBranch')) {
+ branch.get('visBranch').remove();
+ }
+};
+
+GitEngine.prototype.unescapeQuotes = function(str) {
+ return str.replace(/'/g, "'");
+}
+
+GitEngine.prototype.dispatch = function(command, callback) {
+ // 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() {
+ command.set('status', 'finished');
+ callback();
+ }, this);
+ this.animationQueue = new AnimationQueue({
+ callback: whenDone
+ });
+
+ command.set('status', 'processing');
+ try {
+ var methodName = command.get('method').replace(/-/g, '') + 'Starter';
+ this[methodName]();
+ } catch (err) {
+ if (err instanceof GitError ||
+ err instanceof CommandResult) {
+ // short circuit animation by just setting error and returning
+ command.set('error', err);
+ callback();
+ return;
+ } else {
+ throw err;
+ }
+ }
+
+ // only add the refresh if we didn't do manual animations
+ if (!this.animationQueue.get('animations').length && !this.animationQueue.get('defer')) {
+ animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
+ }
+
+ // animation queue will call the callback when its done
+ if (!this.animationQueue.get('defer')) {
+ this.animationQueue.start();
+ }
+};
+
+GitEngine.prototype.showStarter = function() {
+ this.oneArgImpliedHead(this.generalArgs);
+
+ this.show(this.generalArgs[0]);
+};
+
+GitEngine.prototype.show = function(ref) {
+ var commit = this.getCommitFromRef(ref);
+
+ throw new CommandResult({
+ msg: commit.getShowEntry()
+ });
+};
+
+GitEngine.prototype.statusStarter = function() {
+ var lines = [];
+ if (this.getDetachedHead()) {
+ lines.push('Detached Head!');
+ } else {
+ var branchName = this.HEAD.get('target').get('id');
+ lines.push('On branch ' + branchName);
+ }
+ lines.push('Changes to be committed:');
+ lines.push('');
+ lines.push(' modified: cal/OskiCostume.stl');
+ lines.push('');
+ lines.push('Ready to commit! (as always in this demo)');
+
+ var msg = '';
+ _.each(lines, function(line) {
+ msg += '# ' + line + '\n';
+ });
+
+ throw new CommandResult({
+ msg: msg
+ });
+};
+
+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: 'I need a not branch (^branchName) when getting two arguments!'
+ });
+ }
+ }
+
+ this.oneArgImpliedHead(this.generalArgs);
+ this.log(this.generalArgs[0]);
+};
+
+GitEngine.prototype.logWithout = function(ref, omitBranch) {
+ // slice off the ^branch
+ omitBranch = omitBranch.slice(1);
+ this.log(ref, this.getUpstreamSet(omitBranch));
+};
+
+GitEngine.prototype.log = function(ref, omitSet) {
+ // omit set is for doing stuff like git log branchA ^branchB
+ omitSet = omitSet || {};
+ // first get the commit we referenced
+ var commit = this.getCommitFromRef(ref);
+
+ // then get as many far back as we can from here, order by commit date
+ var toDump = [];
+ var pQueue = [commit];
+
+ var seen = {};
+
+ while (pQueue.length) {
+ var popped = pQueue.shift(0);
+ if (seen[popped.get('id')] || omitSet[popped.get('id')]) {
+ continue;
+ }
+ seen[popped.get('id')] = true;
+
+ toDump.push(popped);
+
+ if (popped.get('parents') && popped.get('parents').length) {
+ pQueue = pQueue.concat(popped.get('parents'));
+ }
+ }
+
+ // now go through and collect logs
+ var bigLogStr = '';
+ _.each(toDump, function(c) {
+ bigLogStr += c.getLogEntry();
+ }, this);
+
+ throw new CommandResult({
+ msg: bigLogStr
+ });
+};
+
+GitEngine.prototype.addStarter = function() {
+ throw new CommandResult({
+ msg: "This demo is meant to demonstrate git branching, so don't worry about " +
+ "adding / staging files. Just go ahead and commit away!"
+ });
+};
+
+GitEngine.prototype.getCommonAncestor = function(ancestor, cousin) {
+ if (this.isUpstreamOf(cousin, ancestor)) {
+ throw new Error('Dont use common ancestor if we are upstream!');
+ }
+
+ var upstreamSet = this.getUpstreamSet(ancestor);
+ // now BFS off of cousin until you find something
+
+ var queue = [this.getCommitFromRef(cousin)];
+ while (queue.length) {
+ var here = queue.pop();
+ if (upstreamSet[here.get('id')]) {
+ return here;
+ }
+ queue = queue.concat(here.get('parents'));
+ }
+ throw new Error('something has gone very wrong... two nodes arent connected!');
+};
+
+GitEngine.prototype.isUpstreamOf = function(child, ancestor) {
+ child = this.getCommitFromRef(child);
+
+ // basically just do a completely BFS search on ancestor to the root, then
+ // check for membership of child in that set of explored nodes
+ var upstream = this.getUpstreamSet(ancestor);
+ return upstream[child.get('id')] !== undefined;
+};
+
+GitEngine.prototype.getUpstreamSet = function(ancestor) {
+ var commit = this.getCommitFromRef(ancestor);
+ var ancestorID = commit.get('id');
+ var queue = [commit];
+
+ var exploredSet = {};
+ exploredSet[ancestorID] = true;
+ while (queue.length) {
+ var here = queue.pop();
+ var rents = here.get('parents');
+
+ _.each(rents, function(rent) {
+ exploredSet[rent.get('id')] = true;
+ queue.push(rent);
+ });
+ }
+ return exploredSet;
+};
+
+
+var Ref = Backbone.Model.extend({
+ initialize: function() {
+ if (!this.get('target')) {
+ throw new Error('must be initialized with target');
+ }
+ if (!this.get('id')) {
+ throw new Error('must be given an id');
+ }
+ this.set('type', 'general ref');
+
+ if (this.get('id') == 'HEAD') {
+ this.set('lastLastTarget', null);
+ this.set('lastTarget', this.get('target'));
+ // have HEAD remember where it is for checkout -
+ this.on('change:target', this.targetChanged, this);
+ }
+ },
+
+ targetChanged: function(model, targetValue, ev) {
+ // push our little 3 stack back. we need to do this because
+ // backbone doesn't give you what the value WAS, only what it was changed
+ // TO
+ this.set('lastLastTarget', this.get('lastTarget'));
+ this.set('lastTarget', targetValue);
+ },
+
+ toString: function() {
+ return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target'));
+ }
+});
+
+var Branch = Ref.extend({
+ defaults: {
+ visBranch: null,
+ },
+
+ initialize: function() {
+ Ref.prototype.initialize.call(this);
+ this.set('type', 'branch');
+ }
+});
+
+var Commit = Backbone.Model.extend({
+ defaults: {
+ type: 'commit',
+ children: null,
+ parents: null,
+ author: 'Peter Cottle',
+ createTime: null,
+ commitMessage: null,
+ visNode: null,
+ gitVisuals: null
+ },
+
+ constants: {
+ circularFields: ['gitVisuals', 'visNode', 'children']
+ },
+
+ getLogEntry: function() {
+ // for now we are just joining all these things with newlines which
+ // will get placed by paragraph tags. Not really a fan of this, but
+ // it's better than making an entire template and all that jazz
+ return [
+ 'Author: ' + this.get('author'),
+ 'Date: ' + this.get('createTime'),
+ '
',
+ this.get('commitMessage'),
+ '
',
+ 'Commit: ' + this.get('id')
+ ].join('\n' ) + '\n';
+ },
+
+ getShowEntry: function() {
+ // same deal as above, show log entry and some fake changes
+ return [
+ this.getLogEntry(),
+ 'diff --git a/bigGameResults.html b/bigGameResults.html',
+ '--- bigGameResults.html',
+ '+++ bigGameResults.html',
+ '@@ 13,27 @@ Winner, Score',
+ '- Stanfurd, 14-7',
+ '+ Cal, 21-14',
+ ].join('\n') + '\n';
+ },
+
+ validateAtInit: function() {
+ if (!this.get('id')) {
+ throw new Error('Need ID!!');
+ }
+
+ if (!this.get('createTime')) {
+ this.set('createTime', new Date().toString());
+ }
+ if (!this.get('commitMessage')) {
+ this.set('commitMessage', 'Quick Commit. Go Bears!');
+ }
+
+ this.set('children', []);
+
+ // root commits have no parents
+ if (!this.get('rootCommit')) {
+ if (!this.get('parents') || !this.get('parents').length) {
+ throw new Error('needs parents');
+ }
+ }
+ },
+
+ addNodeToVisuals: function() {
+ var visNode = this.get('gitVisuals').addNode(this.get('id'), this);
+ this.set('visNode', visNode);
+ },
+
+ addEdgeToVisuals: function(parent) {
+ this.get('gitVisuals').addEdge(this.get('id'), parent.get('id'));
+ },
+
+ isMainParent: function(parent) {
+ var index = this.get('parents').indexOf(parent);
+ return index === 0;
+ },
+
+ initialize: function(options) {
+ this.validateAtInit();
+ this.addNodeToVisuals();
+
+ _.each(this.get('parents'), function(parent) {
+ parent.get('children').push(this);
+ this.addEdgeToVisuals(parent);
+ }, this);
+ }
+});
+
+exports.GitEngine = GitEngine;
+exports.Commit = Commit;
+exports.Branch = Branch;
+exports.Ref = Ref;
+
+
+});
+
+require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection;
+
+var CommandPromptView = Backbone.View.extend({
+ initialize: function(options) {
+ this.collection = options.collection;
+
+ // uses local storage
+ this.commands = new CommandEntryCollection();
+ this.commands.fetch({
+ success: _.bind(function() {
+ // reverse the commands. this is ugly but needs to be done...
+ var commands = [];
+ this.commands.each(function(c) {
+ commands.push(c);
+ });
+
+ commands.reverse();
+ this.commands.reset();
+
+ _.each(commands, function(c) {
+ this.commands.add(c);
+ }, this);
+ }, this)
+ });
+
+ this.index = -1;
+
+ this.commandSpan = this.$('#prompt span.command')[0];
+ this.commandCursor = this.$('#prompt span.cursor')[0];
+
+ // this is evil, but we will refer to HTML outside the document
+ // and attach a click event listener so we can focus / unfocus
+ $(document).delegate('#commandLineHistory', 'click', _.bind(function() {
+ this.focus();
+ }, this));
+
+
+ $(document).delegate('#commandTextField', 'blur', _.bind(function() {
+ this.blur();
+ }, this));
+
+ events.on('processCommandFromEvent', this.addToCollection, this);
+ events.on('submitCommandValueFromEvent', this.submitValue, this);
+ events.on('rollupCommands', this.rollupCommands, this);
+
+ // hacky timeout focus
+ setTimeout(_.bind(function() {
+ this.focus();
+ }, this), 100);
+ },
+
+ events: {
+ 'keydown #commandTextField': 'onKey',
+ 'keyup #commandTextField': 'onKeyUp',
+ 'blur #commandTextField': 'hideCursor',
+ 'focus #commandTextField': 'showCursor'
+ },
+
+ blur: function() {
+ $(this.commandCursor).toggleClass('shown', false);
+ },
+
+ focus: function() {
+ this.$('#commandTextField').focus();
+ this.showCursor();
+ },
+
+ hideCursor: function() {
+ this.toggleCursor(false);
+ },
+
+ showCursor: function() {
+ this.toggleCursor(true);
+ },
+
+ toggleCursor: function(state) {
+ $(this.commandCursor).toggleClass('shown', state);
+ },
+
+ onKey: function(e) {
+ var el = e.srcElement;
+ this.updatePrompt(el)
+ },
+
+ onKeyUp: function(e) {
+ this.onKey(e);
+
+ // we need to capture some of these events.
+ // WARNING: this key map is not internationalized :(
+ var keyMap = {
+ // enter
+ 13: _.bind(function() {
+ this.submit();
+ }, this),
+ // up
+ 38: _.bind(function() {
+ this.commandSelectChange(1);
+ }, this),
+ // down
+ 40: _.bind(function() {
+ this.commandSelectChange(-1);
+ }, this)
+ };
+
+ if (keyMap[e.which] !== undefined) {
+ e.preventDefault();
+ keyMap[e.which]();
+ this.onKey(e);
+ }
+ },
+
+ badHtmlEncode: function(text) {
+ return text.replace(/&/g,'&')
+ .replace(/= this.commands.length || this.index < 0) {
+ this.clear();
+ this.index = -1;
+ return;
+ }
+
+ // yay! we actually can display something
+ var commandEntry = this.commands.toArray()[this.index].get('text');
+ this.setTextField(commandEntry);
+ },
+
+ clearLocalStorage: function() {
+ this.commands.each(function(c) {
+ Backbone.sync('delete', c, function() { });
+ }, this);
+ localStorage.setItem('CommandEntries', '');
+ },
+
+ setTextField: function(value) {
+ this.$('#commandTextField').val(value);
+ },
+
+ clear: function() {
+ this.setTextField('');
+ },
+
+ submit: function() {
+ var value = this.$('#commandTextField').val().replace('\n', '');
+ this.clear();
+ this.submitValue(value);
+ },
+
+ rollupCommands: function(numBack) {
+ var which = this.commands.toArray().slice(1, Number(numBack) + 1);
+ which.reverse();
+
+ var str = '';
+ _.each(which, function(commandEntry) {
+ str += commandEntry.get('text') + ';';
+ }, this);
+
+ console.log('the str', str);
+
+ var rolled = new CommandEntry({text: str});
+ this.commands.unshift(rolled);
+ Backbone.sync('create', rolled, function() { });
+ },
+
+ submitValue: function(value) {
+ // we should add if it's not a blank line and this is a new command...
+ // or if we edited the command
+ var shouldAdd = (value.length && this.index == -1) ||
+ ((value.length && this.index !== -1 &&
+ this.commands.toArray()[this.index].get('text') !== value));
+
+ if (shouldAdd) {
+ var commandEntry = new CommandEntry({text: value});
+ this.commands.unshift(commandEntry);
+
+ // store to local storage
+ Backbone.sync('create', commandEntry, function() { });
+
+ // if our length is too egregious, reset
+ if (this.commands.length > 100) {
+ this.clearLocalStorage();
+ }
+ }
+ this.index = -1;
+
+ // split commands on semicolon
+ _.each(value.split(';'), _.bind(function(command, index) {
+ command = _.escape(command);
+
+ command = command
+ .replace(/^(\s+)/, '')
+ .replace(/(\s+)$/, '')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'");
+
+ if (index > 0 && !command.length) {
+ return;
+ }
+
+ this.addToCollection(command);
+ }, this));
+ },
+
+ addToCollection: function(value) {
+ var command = new Command({
+ rawStr: value
+ });
+ this.collection.add(command);
+ }
+});
+
+
+// This is the view for all commands -- it will represent
+// their status (inqueue, processing, finished, error),
+// their value ("git commit --amend"),
+// and the result (either errors or warnings or whatever)
+var CommandView = Backbone.View.extend({
+ tagName: 'div',
+ model: Command,
+ template: _.template($('#command-template').html()),
+
+ events: {
+ 'click': 'clicked'
+ },
+
+ clicked: function(e) {
+ },
+
+ initialize: function() {
+ this.model.bind('change', this.wasChanged, this);
+ this.model.bind('destroy', this.remove, this);
+ },
+
+ wasChanged: function(model, changeEvent) {
+ // for changes that are just comestic, we actually only want to toggle classes
+ // with jquery rather than brutally delete a html of HTML
+ var changes = changeEvent.changes;
+ var changeKeys = _.keys(changes);
+ if (_.difference(changeKeys, ['status']) == 0) {
+ this.updateStatus();
+ } else if (_.difference(changeKeys, ['error']) == 0) {
+ // the above will
+ this.render();
+ } else {
+ this.render();
+ }
+ },
+
+ updateStatus: function() {
+ var statuses = ['inqueue', 'processing', 'finished'];
+ var toggleMap = {};
+ _.each(statuses, function(status) {
+ toggleMap[status] = false;
+ });
+ toggleMap[this.model.get('status')] = true;
+
+ var query = this.$('p.commandLine');
+
+ _.each(toggleMap, function(value, key) {
+ query.toggleClass(key, value);
+ });
+ },
+
+ render: function() {
+ var json = _.extend(
+ {
+ resultType: '',
+ result: '',
+ formattedWarnings: this.model.getFormattedWarnings()
+ },
+ this.model.toJSON()
+ );
+
+ this.$el.html(this.template(json));
+ return this;
+ },
+
+ remove: function() {
+ $(this.el).hide();
+ }
+});
+
+
+var CommandLineHistoryView = Backbone.View.extend({
+ initialize: function(options) {
+ this.collection = options.collection;
+
+ this.collection.on('add', this.addOne, this);
+ this.collection.on('reset', this.addAll, this);
+ this.collection.on('all', this.render, this);
+
+ this.collection.on('change', this.scrollDown, this);
+
+ events.on('issueWarning', this.addWarning, this);
+ events.on('commandScrollDown', this.scrollDown, this);
+ },
+
+ addWarning: function(msg) {
+ var err = new Warning({
+ msg: msg
+ });
+
+ var command = new Command({
+ error: err,
+ rawStr: 'Warning:'
+ });
+
+ this.collection.add(command);
+ },
+
+ scrollDown: function() {
+ // if commandDisplay is ever bigger than #terminal, we need to
+ // add overflow-y to terminal and scroll down
+ var cD = $('#commandDisplay')[0];
+ var t = $('#terminal')[0];
+
+ if ($(t).hasClass('scrolling')) {
+ t.scrollTop = t.scrollHeight;
+ return;
+ }
+ if (cD.clientHeight > t.clientHeight) {
+ $(t).css('overflow-y', 'scroll');
+ $(t).css('overflow-x', 'hidden');
+ $(t).addClass('scrolling');
+ t.scrollTop = t.scrollHeight;
+ }
+ },
+
+ addOne: function(command) {
+ var view = new CommandView({
+ model: command
+ });
+ this.$('#commandDisplay').append(view.render().el);
+ this.scrollDown();
+ },
+
+ addAll: function() {
+ this.collection.each(this.addOne);
+ }
+});
+
+exports.CommandPromptView = CommandPromptView;
+exports.CommandLineHistoryView = CommandLineHistoryView;
+
+
+});
+
+require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var CommitCollection = require('./collections').CommitCollection;
+var BranchCollection = require('./collections').BranchCollection;
+var GitEngine = require('./git').GitEngine;
+
+var Visualization = Backbone.View.extend({
+ initialize: function(options) {
+ var _this = this;
+ Raphael(10, 10, 200, 200, function() {
+
+ // for some reason raphael calls this function with a predefined
+ // context...
+ // so switch it
+ _this.paperInitialize(this);
+ });
+ },
+
+ paperInitialize: function(paper, options) {
+ this.paper = paper;
+
+ this.commitCollection = new CommitCollection();
+ this.branchCollection = new BranchCollection();
+
+ this.gitVisuals = new GitVisuals({
+ commitCollection: this.commitCollection,
+ branchCollection: this.branchCollection,
+ paper: this.paper
+ });
+
+ this.gitEngine = new GitEngine({
+ collection: this.commitCollection,
+ branches: this.branchCollection,
+ gitVisuals: this.gitVisuals
+ });
+ this.gitEngine.init();
+ this.gitVisuals.assignGitEngine(this.gitEngine);
+
+ this.myResize();
+ $(window).on('resize', _.bind(this.myResize, this));
+ this.gitVisuals.drawTreeFirstTime();
+ },
+
+ myResize: function() {
+ var smaller = 1;
+ var el = this.el;
+
+ var left = el.offsetLeft;
+ var top = el.offsetTop;
+ var width = el.clientWidth - smaller;
+ var height = el.clientHeight - smaller;
+
+ $(this.paper.canvas).css({
+ left: left + 'px',
+ top: top + 'px'
+ });
+ this.paper.setSize(width, height);
+ this.gitVisuals.canvasResize(width, height);
+ }
+
+});
+
+function GitVisuals(options) {
+ this.commitCollection = options.commitCollection;
+ this.branchCollection = options.branchCollection;
+ this.visNodeMap = {};
+
+ this.visEdgeCollection = new VisEdgeCollection();
+ this.visBranchCollection = new VisBranchCollection();
+ this.commitMap = {};
+
+ this.rootCommit = null;
+ this.branchStackMap = null;
+ this.upstreamBranchSet = null;
+ this.upstreamHeadSet = null;
+
+ this.paper = options.paper;
+ this.gitReady = false;
+
+ this.branchCollection.on('add', this.addBranchFromEvent, this);
+ this.branchCollection.on('remove', this.removeBranch, this);
+ this.deferred = [];
+
+ events.on('refreshTree', _.bind(
+ this.refreshTree, this
+ ));
+}
+
+GitVisuals.prototype.defer = function(action) {
+ this.deferred.push(action);
+};
+
+GitVisuals.prototype.deferFlush = function() {
+ _.each(this.deferred, function(action) {
+ action();
+ }, this);
+ this.deferred = [];
+};
+
+GitVisuals.prototype.resetAll = function() {
+ // make sure to copy these collections because we remove
+ // items in place and underscore is too dumb to detect length change
+ var edges = this.visEdgeCollection.toArray();
+ _.each(edges, function(visEdge) {
+ visEdge.remove();
+ }, this);
+
+ var branches = this.visBranchCollection.toArray();
+ _.each(branches, function(visBranch) {
+ visBranch.remove();
+ }, this);
+
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.remove();
+ }, this);
+
+ this.visEdgeCollection.reset();
+ this.visBranchCollection.reset();
+
+ this.visNodeMap = {};
+ this.rootCommit = null;
+ this.commitMap = {};
+};
+
+GitVisuals.prototype.assignGitEngine = function(gitEngine) {
+ this.gitEngine = gitEngine;
+ this.initHeadBranch();
+ this.deferFlush();
+};
+
+GitVisuals.prototype.initHeadBranch = function() {
+ // it's unfortaunte we have to do this, but the head branch
+ // is an edge case because it's not part of a collection so
+ // we can't use events to load or unload it. thus we have to call
+ // this ugly method which will be deleted one day
+
+ // seed this with the HEAD pseudo-branch
+ this.addBranchFromEvent(this.gitEngine.HEAD);
+};
+
+GitVisuals.prototype.getScreenBounds = function() {
+ // for now we return the node radius subtracted from the walls
+ return {
+ widthPadding: GRAPHICS.nodeRadius * 1.5,
+ heightPadding: GRAPHICS.nodeRadius * 1.5
+ };
+};
+
+GitVisuals.prototype.toScreenCoords = function(pos) {
+ if (!this.paper.width) {
+ throw new Error('being called too early for screen coords');
+ }
+ var bounds = this.getScreenBounds();
+
+ var shrink = function(frac, total, padding) {
+ return padding + frac * (total - padding * 2);
+ };
+
+ return {
+ x: shrink(pos.x, this.paper.width, bounds.widthPadding),
+ y: shrink(pos.y, this.paper.height, bounds.heightPadding)
+ };
+};
+
+GitVisuals.prototype.animateAllFromAttrToAttr = function(fromSnapshot, toSnapshot, idsToOmit) {
+ var animate = function(obj) {
+ var id = obj.getID();
+ if (_.include(idsToOmit, id)) {
+ return;
+ }
+
+ if (!fromSnapshot[id] || !toSnapshot[id]) {
+ // its actually ok it doesnt exist yet
+ return;
+ }
+ obj.animateFromAttrToAttr(fromSnapshot[id], toSnapshot[id]);
+ };
+
+ this.visBranchCollection.each(function(visBranch) {
+ animate(visBranch);
+ });
+ this.visEdgeCollection.each(function(visEdge) {
+ animate(visEdge);
+ });
+ _.each(this.visNodeMap, function(visNode) {
+ animate(visNode);
+ });
+};
+
+/***************************************
+ == BEGIN Tree Calculation Parts ==
+ _ __ __ _
+ \\/ / \ \//_
+ \ \ / __| __
+ \ \___/ /_____/ /
+ | _______ \
+ \ ( ) / \_\
+ \ /
+ | |
+ | |
+ ____+-_=+-^ ^+-=_=__________
+
+^^ I drew that :D
+
+ **************************************/
+
+GitVisuals.prototype.genSnapshot = function() {
+ this.fullCalc();
+
+ var snapshot = {};
+ _.each(this.visNodeMap, function(visNode) {
+ snapshot[visNode.get('id')] = visNode.getAttributes();
+ }, this);
+
+ this.visBranchCollection.each(function(visBranch) {
+ snapshot[visBranch.getID()] = visBranch.getAttributes();
+ }, this);
+
+ this.visEdgeCollection.each(function(visEdge) {
+ snapshot[visEdge.getID()] = visEdge.getAttributes();
+ }, this);
+
+ return snapshot;
+};
+
+GitVisuals.prototype.refreshTree = function(speed) {
+ if (!this.gitReady) {
+ return;
+ }
+
+ // this method can only be called after graphics are rendered
+ this.fullCalc();
+
+ this.animateAll(speed);
+};
+
+GitVisuals.prototype.refreshTreeHarsh = function() {
+ this.fullCalc();
+
+ this.animateAll(0);
+};
+
+GitVisuals.prototype.animateAll = function(speed) {
+ this.zIndexReflow();
+
+ this.animateEdges(speed);
+ this.animateNodePositions(speed);
+ this.animateRefs(speed);
+};
+
+GitVisuals.prototype.fullCalc = function() {
+ this.calcTreeCoords();
+ this.calcGraphicsCoords();
+};
+
+GitVisuals.prototype.calcTreeCoords = function() {
+ // this method can only contain things that dont rely on graphics
+ if (!this.rootCommit) {
+ throw new Error('grr, no root commit!');
+ }
+
+ this.calcUpstreamSets();
+ this.calcBranchStacks();
+
+ this.calcDepth();
+ this.calcWidth();
+};
+
+GitVisuals.prototype.calcGraphicsCoords = function() {
+ this.visBranchCollection.each(function(visBranch) {
+ visBranch.updateName();
+ });
+};
+
+GitVisuals.prototype.calcUpstreamSets = function() {
+ this.upstreamBranchSet = this.gitEngine.getUpstreamBranchSet();
+ this.upstreamHeadSet = this.gitEngine.getUpstreamHeadSet();
+};
+
+GitVisuals.prototype.getCommitUpstreamBranches = function(commit) {
+ return this.branchStackMap[commit.get('id')];
+};
+
+GitVisuals.prototype.getBlendedHuesForCommit = function(commit) {
+ var branches = this.upstreamBranchSet[commit.get('id')];
+ if (!branches) {
+ throw new Error('that commit doesnt have upstream branches!');
+ }
+
+ return this.blendHuesFromBranchStack(branches);
+};
+
+GitVisuals.prototype.blendHuesFromBranchStack = function(branchStackArray) {
+ var hueStrings = [];
+ _.each(branchStackArray, function(branchWrapper) {
+ var fill = branchWrapper.obj.get('visBranch').get('fill');
+
+ if (fill.slice(0,3) !== 'hsb') {
+ // crap! convert
+ var color = Raphael.color(fill);
+ fill = 'hsb(' + String(color.h) + ',' + String(color.l);
+ fill = fill + ',' + String(color.s) + ')';
+ }
+
+ hueStrings.push(fill);
+ });
+
+ return blendHueStrings(hueStrings);
+};
+
+GitVisuals.prototype.getCommitUpstreamStatus = function(commit) {
+ if (!this.upstreamBranchSet) {
+ throw new Error("Can't calculate this yet!");
+ }
+
+ var id = commit.get('id');
+ var branch = this.upstreamBranchSet;
+ var head = this.upstreamHeadSet;
+
+ if (branch[id]) {
+ return 'branch';
+ } else if (head[id]) {
+ return 'head';
+ } else {
+ return 'none';
+ }
+};
+
+GitVisuals.prototype.calcBranchStacks = function() {
+ var branches = this.gitEngine.getBranches();
+ var map = {};
+ _.each(branches, function(branch) {
+ var thisId = branch.target.get('id');
+
+ map[thisId] = map[thisId] || [];
+ map[thisId].push(branch);
+ map[thisId].sort(function(a, b) {
+ var aId = a.obj.get('id');
+ var bId = b.obj.get('id');
+ if (aId == 'master' || bId == 'master') {
+ return aId == 'master' ? -1 : 1;
+ }
+ return aId.localeCompare(bId);
+ });
+ });
+ this.branchStackMap = map;
+};
+
+GitVisuals.prototype.calcWidth = function() {
+ this.maxWidthRecursive(this.rootCommit);
+
+ this.assignBoundsRecursive(this.rootCommit, 0, 1);
+};
+
+GitVisuals.prototype.maxWidthRecursive = function(commit) {
+ var childrenTotalWidth = 0;
+ _.each(commit.get('children'), function(child) {
+ // only include this if we are the "main" parent of
+ // this child
+ if (child.isMainParent(commit)) {
+ var childWidth = this.maxWidthRecursive(child);
+ childrenTotalWidth += childWidth;
+ }
+ }, this);
+
+ var maxWidth = Math.max(1, childrenTotalWidth);
+ commit.get('visNode').set('maxWidth', maxWidth);
+ return maxWidth;
+};
+
+GitVisuals.prototype.assignBoundsRecursive = function(commit, min, max) {
+ // I always center myself within my bounds
+ var myWidthPos = (min + max) / 2.0;
+ commit.get('visNode').get('pos').x = myWidthPos;
+
+ if (commit.get('children').length == 0) {
+ return;
+ }
+
+ // i have a certain length to divide up
+ var myLength = max - min;
+ // I will divide up that length based on my children's max width in a
+ // basic box-flex model
+ var totalFlex = 0;
+ var children = commit.get('children');
+ _.each(children, function(child) {
+ if (child.isMainParent(commit)) {
+ totalFlex += child.get('visNode').getMaxWidthScaled();
+ }
+ }, this);
+
+ var prevBound = min;
+
+ // now go through and do everything
+ // TODO: order so the max width children are in the middle!!
+ _.each(children, function(child) {
+ if (!child.isMainParent(commit)) {
+ return;
+ }
+
+ var flex = child.get('visNode').getMaxWidthScaled();
+ var portion = (flex / totalFlex) * myLength;
+ var childMin = prevBound;
+ var childMax = childMin + portion;
+ this.assignBoundsRecursive(child, childMin, childMax);
+ prevBound = childMax;
+ }, this);
+};
+
+GitVisuals.prototype.calcDepth = function() {
+ var maxDepth = this.calcDepthRecursive(this.rootCommit, 0);
+ if (maxDepth > 15) {
+ // issue warning
+ console.warn('graphics are degrading from too many layers');
+ }
+
+ var depthIncrement = this.getDepthIncrement(maxDepth);
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.setDepthBasedOn(depthIncrement);
+ }, this);
+};
+
+/***************************************
+ == END Tree Calculation ==
+ _ __ __ _
+ \\/ / \ \//_
+ \ \ / __| __
+ \ \___/ /_____/ /
+ | _______ \
+ \ ( ) / \_\
+ \ /
+ | |
+ | |
+ ____+-_=+-^ ^+-=_=__________
+
+^^ I drew that :D
+
+ **************************************/
+
+GitVisuals.prototype.animateNodePositions = function(speed) {
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.animateUpdatedPosition(speed);
+ }, this);
+};
+
+GitVisuals.prototype.turnOnPaper = function() {
+ this.gitReady = false;
+};
+
+// does making an accessor method make it any less hacky? that is the true question
+GitVisuals.prototype.turnOffPaper = function() {
+ this.gitReady = true;
+};
+
+GitVisuals.prototype.addBranchFromEvent = function(branch, collection, index) {
+ var action = _.bind(function() {
+ this.addBranch(branch);
+ }, this);
+
+ if (!this.gitEngine || !this.gitReady) {
+ this.defer(action);
+ } else {
+ action();
+ }
+};
+
+GitVisuals.prototype.addBranch = function(branch) {
+ var visBranch = new VisBranch({
+ branch: branch,
+ gitVisuals: this,
+ gitEngine: this.gitEngine
+ });
+
+ this.visBranchCollection.add(visBranch);
+ if (this.gitReady) {
+ visBranch.genGraphics(this.paper);
+ }
+};
+
+GitVisuals.prototype.removeVisBranch = function(visBranch) {
+ this.visBranchCollection.remove(visBranch);
+};
+
+GitVisuals.prototype.removeVisNode = function(visNode) {
+ this.visNodeMap[visNode.getID()] = undefined;
+};
+
+GitVisuals.prototype.removeVisEdge = function(visEdge) {
+ this.visEdgeCollection.remove(visEdge);
+};
+
+GitVisuals.prototype.animateRefs = function(speed) {
+ this.visBranchCollection.each(function(visBranch) {
+ visBranch.animateUpdatedPos(speed);
+ }, this);
+};
+
+GitVisuals.prototype.animateEdges = function(speed) {
+ this.visEdgeCollection.each(function(edge) {
+ edge.animateUpdatedPath(speed);
+ }, this);
+};
+
+GitVisuals.prototype.getDepthIncrement = function(maxDepth) {
+ // assume there are at least 7 layers until later
+ maxDepth = Math.max(maxDepth, 7);
+ var increment = 1.0 / maxDepth;
+ return increment;
+};
+
+GitVisuals.prototype.calcDepthRecursive = function(commit, depth) {
+ commit.get('visNode').setDepth(depth);
+
+ var children = commit.get('children');
+ var maxDepth = depth;
+ _.each(children, function(child) {
+ var d = this.calcDepthRecursive(child, depth + 1);
+ maxDepth = Math.max(d, maxDepth);
+ }, this);
+
+ return maxDepth;
+};
+
+GitVisuals.prototype.canvasResize = function(width, height) {
+ // refresh when we are ready
+ if (GLOBAL.isAnimating) {
+ events.trigger('processCommandFromEvent', 'refresh');
+ } else {
+ this.refreshTree();
+ }
+};
+
+GitVisuals.prototype.addCommit = function(commit) {
+ // TODO
+};
+
+GitVisuals.prototype.addNode = function(id, commit) {
+ this.commitMap[id] = commit;
+ if (commit.get('rootCommit')) {
+ this.rootCommit = commit;
+ }
+
+ var visNode = new VisNode({
+ id: id,
+ commit: commit,
+ gitVisuals: this,
+ gitEngine: this.gitEngine
+ });
+ this.visNodeMap[id] = visNode;
+
+ if (this.gitReady) {
+ visNode.genGraphics(this.paper);
+ }
+ return visNode;
+};
+
+GitVisuals.prototype.addEdge = function(idTail, idHead) {
+ var visNodeTail = this.visNodeMap[idTail];
+ var visNodeHead = this.visNodeMap[idHead];
+
+ if (!visNodeTail || !visNodeHead) {
+ throw new Error('one of the ids in (' + idTail +
+ ', ' + idHead + ') does not exist');
+ }
+
+ var edge = new VisEdge({
+ tail: visNodeTail,
+ head: visNodeHead,
+ gitVisuals: this,
+ gitEngine: this.gitEngine
+ });
+ this.visEdgeCollection.add(edge);
+
+ if (this.gitReady) {
+ edge.genGraphics(this.paper);
+ }
+};
+
+GitVisuals.prototype.collectionChanged = function() {
+ // TODO ?
+};
+
+GitVisuals.prototype.zIndexReflow = function() {
+ this.visNodesFront();
+ this.visBranchesFront();
+};
+
+GitVisuals.prototype.visNodesFront = function() {
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.toFront();
+ });
+};
+
+GitVisuals.prototype.visBranchesFront = function() {
+ this.visBranchCollection.each(function(vBranch) {
+ vBranch.nonTextToFront();
+ });
+
+ this.visBranchCollection.each(function(vBranch) {
+ vBranch.textToFront();
+ });
+};
+
+GitVisuals.prototype.drawTreeFromReload = function() {
+ this.gitReady = true;
+ // gen all the graphics we need
+ this.deferFlush();
+
+ this.calcTreeCoords();
+};
+
+GitVisuals.prototype.drawTreeFirstTime = function() {
+ this.gitReady = true;
+ this.calcTreeCoords();
+
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.genGraphics(this.paper);
+ }, this);
+
+ this.visEdgeCollection.each(function(edge) {
+ edge.genGraphics(this.paper);
+ }, this);
+
+ this.visBranchCollection.each(function(visBranch) {
+ visBranch.genGraphics(this.paper);
+ }, this);
+
+ this.zIndexReflow();
+};
+
+
+/************************
+ * Random util functions, some from liquidGraph
+ ***********************/
+function blendHueStrings(hueStrings) {
+ // assumes a sat of 0.7 and brightness of 1
+
+ var x = 0;
+ var y = 0;
+ var totalSat = 0;
+ var totalBright = 0;
+ var length = hueStrings.length;
+
+ _.each(hueStrings, function(hueString) {
+ var exploded = hueString.split('(')[1];
+ exploded = exploded.split(')')[0];
+ exploded = exploded.split(',');
+
+ totalSat += parseFloat(exploded[1]);
+ totalBright += parseFloat(exploded[2]);
+ var hue = parseFloat(exploded[0]);
+
+ var angle = hue * Math.PI * 2;
+ x += Math.cos(angle);
+ y += Math.sin(angle);
+ });
+
+ x = x / length;
+ y = y / length;
+ totalSat = totalSat / length;
+ totalBright = totalBright / length;
+
+ var hue = Math.atan2(y, x) / (Math.PI * 2); // could fail on 0's
+ if (hue < 0) {
+ hue = hue + 1;
+ }
+ return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')';
+}
+
+function randomHueString() {
+ var hue = Math.random();
+ var str = 'hsb(' + String(hue) + ',0.7,1)';
+ return str;
+};
+
+exports.Visualization = Visualization;
+
});
require.define("/main.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactory = require('./animationFactory').AnimationFactory;
+var CommandCollection = require('./collections').CommandCollection;
+var CommandBuffer = require('./collections').CommandBuffer;
+var CommandPromptView = require('./commandViews').CommandPromptView;
+var CommandLineHistoryView = require('./commandViews').CommandLineHistoryView;
+var Visualization = require('./visuals').Visualization;
/**
* Globals
@@ -716,7 +3525,7 @@ require.define("/animationFactory.js",function(require,module,exports,__dirname,
*/
// essentially a static class
-function AnimationFactory() {
+var AnimationFactory = function() {
}
@@ -959,7 +3768,2815 @@ AnimationFactory.prototype.genFromToSnapshotAnimation = function(
exports.AnimationFactory = AnimationFactory;
-
});
require("/animationFactory.js");
+
+require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var animationFactory = new require('./animationFactory').AnimationFactory();
+
+// backbone or something uses _.uniqueId, so we make our own here
+var uniqueId = (function() {
+ var n = 0;
+ return function(prepend) {
+ return prepend? prepend + n++ : n++;
+ };
+})();
+
+function GitEngine(options) {
+ this.rootCommit = null;
+ this.refs = {};
+ this.HEAD = null;
+
+ this.branchCollection = options.branches;
+ this.commitCollection = options.collection;
+ this.gitVisuals = options.gitVisuals;
+
+ // global variable to keep track of the options given
+ // along with the command call.
+ this.commandOptions = {};
+ this.generalArgs = [];
+
+ events.on('processCommand', _.bind(this.dispatch, this));
+}
+
+GitEngine.prototype.defaultInit = function() {
+ var defaultTree = JSON.parse(unescape("%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22master%22%2C%22type%22%3A%22general%20ref%22%7D%7D"));
+ this.loadTree(defaultTree);
+};
+
+GitEngine.prototype.init = function() {
+ // make an initial commit and a master branch
+ this.rootCommit = this.makeCommit(null, null, {rootCommit: true});
+ this.commitCollection.add(this.rootCommit);
+
+ var master = this.makeBranch('master', this.rootCommit);
+ this.HEAD = new Ref({
+ id: 'HEAD',
+ target: master
+ });
+ this.refs[this.HEAD.get('id')] = this.HEAD;
+
+ // commit once to get things going
+ this.commit();
+};
+
+GitEngine.prototype.exportTree = function() {
+ // need to export all commits, their connectivity / messages, branches, and state of head.
+ // this would be simple if didn't have circular structures.... :P
+ // thus, we need to loop through and "flatten" our graph of objects referencing one another
+ var totalExport = {
+ branches: {},
+ commits: {},
+ HEAD: null
+ };
+
+ _.each(this.branchCollection.toJSON(), function(branch) {
+ branch.target = branch.target.get('id');
+ branch.visBranch = undefined;
+
+ totalExport.branches[branch.id] = branch;
+ });
+
+ _.each(this.commitCollection.toJSON(), function(commit) {
+ // clear out the fields that reference objects and create circular structure
+ _.each(Commit.prototype.constants.circularFields, function(field) {
+ commit[field] = undefined;
+ }, this);
+
+ // convert parents
+ var parents = [];
+ _.each(commit.parents, function(par) {
+ parents.push(par.get('id'));
+ });
+ commit.parents = parents;
+
+ totalExport.commits[commit.id] = commit;
+ }, this);
+
+ var HEAD = this.HEAD.toJSON();
+ HEAD.visBranch = undefined;
+ HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = undefined;
+ HEAD.target = HEAD.target.get('id');
+ totalExport.HEAD = HEAD;
+
+ return totalExport;
+};
+
+GitEngine.prototype.printTree = function() {
+ var str = escape(JSON.stringify(this.exportTree()));
+ return str;
+};
+
+GitEngine.prototype.printAndCopyTree = function() {
+ window.prompt('Copy the tree string below', this.printTree());
+};
+
+GitEngine.prototype.loadTree = function(tree) {
+ // deep copy in case we use it a bunch
+ tree = $.extend(true, {}, tree);
+
+ // first clear everything
+ this.removeAll();
+
+ this.instantiateFromTree(tree);
+
+ this.reloadGraphics();
+};
+
+GitEngine.prototype.loadTreeFromString = function(treeString) {
+ this.loadTree(JSON.parse(unescape(treeString)));
+};
+
+GitEngine.prototype.instantiateFromTree = function(tree) {
+ // now we do the loading part
+ var createdSoFar = {};
+
+ _.each(tree.commits, function(commitJSON) {
+ var commit = this.getOrMakeRecursive(tree, createdSoFar, commitJSON.id);
+ this.commitCollection.add(commit);
+ }, this);
+
+ _.each(tree.branches, function(branchJSON) {
+ var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id);
+
+ this.branchCollection.add(branch, {silent: true});
+ }, this);
+
+ var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id);
+ this.HEAD = HEAD;
+
+ this.rootCommit = createdSoFar['C0'];
+ if (!this.rootCommit) {
+ throw new Error('Need root commit of C0 for calculations');
+ }
+ this.refs = createdSoFar;
+
+ this.branchCollection.each(function(branch) {
+ this.gitVisuals.addBranch(branch);
+ }, this);
+};
+
+GitEngine.prototype.reloadGraphics = function() {
+ // get the root commit, no better way to do it
+ var rootCommit = null;
+ this.commitCollection.each(function(commit) {
+ if (commit.get('id') == 'C0') {
+ rootCommit = commit;
+ }
+ });
+ this.gitVisuals.rootCommit = rootCommit;
+
+ // this just basically makes the HEAD branch. the head branch really should have been
+ // a member of a collection and not this annoying edge case stuff... one day
+ this.gitVisuals.initHeadBranch();
+
+ // when the paper is ready
+ this.gitVisuals.drawTreeFromReload();
+
+ this.gitVisuals.refreshTreeHarsh();
+};
+
+GitEngine.prototype.getOrMakeRecursive = function(tree, createdSoFar, objID) {
+ if (createdSoFar[objID]) {
+ // base case
+ return createdSoFar[objID];
+ }
+
+ var getType = function(tree, id) {
+ if (tree.commits[id]) {
+ return 'commit';
+ } else if (tree.branches[id]) {
+ return 'branch';
+ } else if (id == 'HEAD') {
+ return 'HEAD';
+ }
+ throw new Error("bad type for " + id);
+ };
+
+ // figure out what type
+ var type = getType(tree, objID);
+
+ if (type == 'HEAD') {
+ var headJSON = tree.HEAD;
+ var HEAD = new Ref(_.extend(
+ tree.HEAD,
+ {
+ target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target)
+ }
+ ));
+ createdSoFar[objID] = HEAD;
+ return HEAD;
+ }
+
+ if (type == 'branch') {
+ var branchJSON = tree.branches[objID];
+
+ var branch = new Branch(_.extend(
+ tree.branches[objID],
+ {
+ target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target)
+ }
+ ));
+ createdSoFar[objID] = branch;
+ return branch;
+ }
+
+ if (type == 'commit') {
+ // for commits, we need to grab all the parents
+ var commitJSON = tree.commits[objID];
+
+ var parentObjs = [];
+ _.each(commitJSON.parents, function(parentID) {
+ parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID));
+ }, this);
+
+ var commit = new Commit(_.extend(
+ commitJSON,
+ {
+ parents: parentObjs,
+ gitVisuals: this.gitVisuals
+ }
+ ));
+ createdSoFar[objID] = commit;
+ return commit;
+ }
+
+ throw new Error('ruh rho!! unsupported tyep for ' + objID);
+};
+
+GitEngine.prototype.removeAll = function() {
+ this.branchCollection.reset();
+ this.commitCollection.reset();
+ this.refs = {};
+ this.HEAD = null;
+ this.rootCommit = null;
+
+ this.gitVisuals.resetAll();
+};
+
+GitEngine.prototype.getDetachedHead = function() {
+ // detached head is if HEAD points to a commit instead of a branch...
+ var target = this.HEAD.get('target');
+ var targetType = target.get('type');
+ return targetType !== 'branch';
+};
+
+GitEngine.prototype.validateBranchName = function(name) {
+ name = name.replace(/\s/g, '');
+ if (!/^[a-zA-Z0-9]+$/.test(name)) {
+ throw new GitError({
+ msg: 'woah bad branch name!! This is not ok: ' + name
+ });
+ }
+ if (/[hH][eE][aA][dD]/.test(name)) {
+ throw new GitError({
+ msg: 'branch name of "head" is ambiguous, dont name it that'
+ });
+ }
+ if (name.length > 9) {
+ name = name.slice(0, 9);
+ this.command.addWarning(
+ 'Sorry, we need to keep branch names short for the visuals. Your branch ' +
+ 'name was truncated to 9 characters, resulting in ' + name
+ );
+ }
+ return name;
+};
+
+GitEngine.prototype.makeBranch = function(id, target) {
+ id = this.validateBranchName(id);
+ if (this.refs[id]) {
+ throw new GitError({
+ msg: 'that branch id either matches a commit hash or already exists!'
+ });
+ }
+
+ var branch = new Branch({
+ target: target,
+ id: id
+ });
+ this.branchCollection.add(branch);
+ this.refs[branch.get('id')] = branch;
+ return branch;
+};
+
+GitEngine.prototype.getHead = function() {
+ return _.clone(this.HEAD);
+};
+
+GitEngine.prototype.getBranches = function() {
+ var toReturn = [];
+ this.branchCollection.each(function(branch) {
+ toReturn.push({
+ id: branch.get('id'),
+ selected: this.HEAD.get('target') === branch,
+ target: branch.get('target'),
+ obj: branch
+ });
+ }, this);
+ return toReturn;
+};
+
+GitEngine.prototype.printBranchesWithout = function(without) {
+ var commitToBranches = this.getUpstreamBranchSet();
+ var commitID = this.getCommitFromRef(without).get('id');
+
+ var toPrint = [];
+ _.each(commitToBranches[commitID], function(branchJSON) {
+ branchJSON.selected = this.HEAD.get('target').get('id') == branchJSON.id;
+ toPrint.push(branchJSON);
+ }, this);
+ this.printBranches(toPrint);
+};
+
+GitEngine.prototype.printBranches = function(branches) {
+ var result = '';
+ _.each(branches, function(branch) {
+ result += (branch.selected ? '* ' : '') + branch.id + '\n';
+ });
+ throw new CommandResult({
+ msg: result
+ });
+};
+
+GitEngine.prototype.makeCommit = function(parents, id, options) {
+ // ok we need to actually manually create commit IDs now because
+ // people like nikita (thanks for finding this!) could
+ // make branches named C2 before creating the commit C2
+ if (!id) {
+ id = uniqueId('C');
+ while (this.refs[id]) {
+ id = uniqueId('C');
+ }
+ }
+
+ var commit = new Commit(_.extend({
+ parents: parents,
+ id: id,
+ gitVisuals: this.gitVisuals
+ },
+ options || {}
+ ));
+
+ this.refs[commit.get('id')] = commit;
+ this.commitCollection.add(commit);
+ return commit;
+};
+
+GitEngine.prototype.acceptNoGeneralArgs = function() {
+ if (this.generalArgs.length) {
+ throw new GitError({
+ msg: "That command accepts no general arguments"
+ });
+ }
+};
+
+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: 'I expect at least ' + String(lower) + ' argument(s) ' + what
+ });
+ }
+ if (args.length > upper) {
+ throw new GitError({
+ msg: 'I expect at most ' + String(upper) + ' argument(s) ' + 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);
+
+ var response = this.revert(this.generalArgs);
+
+ if (response) {
+ animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
+ }
+};
+
+GitEngine.prototype.revert = function(whichCommits) {
+ // for each commit, we want to revert it
+ var toRebase = [];
+ _.each(whichCommits, function(stringRef) {
+ toRebase.push(this.getCommitFromRef(stringRef));
+ }, this);
+
+ // we animate reverts now!! we use the rebase animation though so that's
+ // why the terminology is like it is
+ var animationResponse = {};
+ animationResponse.destinationBranch = this.resolveID(toRebase[0]);
+ animationResponse.toRebaseArray = toRebase.slice(0);
+ animationResponse.rebaseSteps = [];
+
+ beforeSnapshot = this.gitVisuals.genSnapshot();
+ var afterSnapshot;
+
+ // now make a bunch of commits on top of where we are
+ var base = this.getCommitFromRef('HEAD');
+ _.each(toRebase, function(oldCommit) {
+ var newId = this.rebaseAltID(oldCommit.get('id'));
+
+ var newCommit = this.makeCommit([base], newId, {
+ commitMessage: 'Reverting ' + this.resolveName(oldCommit) +
+ ': "' + oldCommit.get('commitMessage') + '"'
+ });
+
+ base = newCommit;
+
+ // animation stuff
+ afterSnapshot = this.gitVisuals.genSnapshot();
+ animationResponse.rebaseSteps.push({
+ oldCommit: oldCommit,
+ newCommit: newCommit,
+ beforeSnapshot: beforeSnapshot,
+ afterSnapshot: afterSnapshot
+ });
+ beforeSnapshot = afterSnapshot;
+ }, this);
+ // done! update our location
+ this.setTargetLocation('HEAD', base);
+
+ // animation
+ return animationResponse;
+};
+
+GitEngine.prototype.resetStarter = function() {
+ if (this.commandOptions['--soft']) {
+ throw new GitError({
+ msg: "You can't use --soft because there is no concept of stashing" +
+ " changes or staging files, so you will lose your progress." +
+ " Try using interactive rebasing (or just rebasing) to move commits."
+ });
+ }
+ if (this.commandOptions['--hard']) {
+ this.command.addWarning(
+ 'Nice! You are using --hard. The default behavior is a hard reset in ' +
+ "this demo, so don't worry about specifying the option explicity"
+ );
+ // 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: "Cant reset in detached head! Use checkout if you want to move"
+ });
+ }
+
+ 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, 1);
+ var newCommit = this.cherrypick(this.generalArgs[0]);
+
+ animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
+};
+
+GitEngine.prototype.cherrypick = function(ref) {
+ var commit = this.getCommitFromRef(ref);
+ // check if we already have that
+ var set = this.getUpstreamSet('HEAD');
+ if (set[commit.get('id')]) {
+ throw new GitError({
+ msg: "We already have that commit in our changes history! You can't cherry-pick it " +
+ "if it shows up in git log."
+ });
+ }
+
+ // alter the ID slightly
+ var id = this.rebaseAltID(commit.get('id'));
+
+ // now commit with that id onto HEAD
+ var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id);
+ this.setTargetLocation(this.HEAD, newCommit);
+ return newCommit;
+};
+
+GitEngine.prototype.commitStarter = function() {
+ this.acceptNoGeneralArgs();
+ if (this.commandOptions['-am'] && (
+ this.commandOptions['-a'] || this.commandOptions['-m'])) {
+ throw new GitError({
+ msg: "You can't have -am with another -m or -a!"
+ });
+ }
+
+ var msg = null;
+ if (this.commandOptions['-a']) {
+ this.command.addWarning('No need to add files in this demo');
+ }
+
+ if (this.commandOptions['-am']) {
+ var args = this.commandOptions['-am'];
+ this.validateArgBounds(args, 1, 1, '-am');
+
+ this.command.addWarning("Don't worry about adding files in this demo. I'll take " +
+ "down your commit message anyways, but you can commit without a message " +
+ "in this demo as well");
+ msg = args[0];
+ }
+
+ if (this.commandOptions['-m']) {
+ var args = this.commandOptions['-m'];
+ this.validateArgBounds(args, 1, 1, '-m');
+ msg = args[0];
+ }
+
+ var newCommit = this.commit();
+ if (msg) {
+ msg = msg
+ .replace(/"/g, '"')
+ .replace(/^"/g, '')
+ .replace(/"$/g, '');
+
+ newCommit.set('commitMessage', msg);
+ }
+ animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
+};
+
+GitEngine.prototype.commit = function() {
+ var targetCommit = this.getCommitFromRef(this.HEAD);
+ var id = undefined;
+
+ // if we want to ammend, go one above
+ if (this.commandOptions['--amend']) {
+ targetCommit = this.resolveID('HEAD~1');
+ id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
+ }
+
+ var newCommit = this.makeCommit([targetCommit], id);
+ if (this.getDetachedHead()) {
+ this.command.addWarning('Warning!! Detached HEAD state');
+ }
+
+ this.setTargetLocation(this.HEAD, newCommit);
+ return newCommit;
+};
+
+GitEngine.prototype.resolveName = function(someRef) {
+ // first get the obj
+ var obj = this.resolveID(someRef);
+ if (obj.get('type') == 'commit') {
+ return 'commit ' + obj.get('id');
+ }
+ if (obj.get('type') == 'branch') {
+ return 'branch "' + obj.get('id') + '"';
+ }
+ // we are dealing with HEAD
+ return this.resolveName(obj.get('target'));
+};
+
+GitEngine.prototype.resolveID = function(idOrTarget) {
+ if (idOrTarget === null || idOrTarget === undefined) {
+ throw new Error('Dont call this with null / undefined');
+ }
+
+ if (typeof idOrTarget !== 'string') {
+ return idOrTarget;
+ }
+ return this.resolveStringRef(idOrTarget);
+};
+
+GitEngine.prototype.resolveStringRef = function(ref) {
+ if (this.refs[ref]) {
+ return this.refs[ref];
+ }
+
+ // may be something like HEAD~2 or master^^
+ var relativeRefs = [
+ [/^([a-zA-Z0-9]+)~(\d+)\s*$/, function(matches) {
+ return parseInt(matches[2]);
+ }],
+ [/^([a-zA-Z0-9]+)(\^+)\s*$/, function(matches) {
+ return matches[2].length;
+ }]
+ ];
+
+ var startRef = null;
+ var numBack = null;
+ _.each(relativeRefs, function(config) {
+ var regex = config[0];
+ var parse = config[1];
+ if (regex.test(ref)) {
+ var matches = regex.exec(ref);
+ numBack = parse(matches);
+ startRef = matches[1];
+ }
+ }, this);
+
+ if (!startRef) {
+ throw new GitError({
+ msg: 'unknown ref ' + ref
+ });
+ }
+ if (!this.refs[startRef]) {
+ throw new GitError({
+ msg: 'the ref ' + startRef +' does not exist.'
+ });
+ }
+ var commit = this.getCommitFromRef(startRef);
+
+ return this.numBackFrom(commit, numBack);
+};
+
+GitEngine.prototype.getCommitFromRef = function(ref) {
+ var start = this.resolveID(ref);
+
+ // works for both HEAD and just a single layer. aka branch
+ while (start.get('type') !== 'commit') {
+ start = start.get('target');
+ }
+ return start;
+};
+
+GitEngine.prototype.getType = function(ref) {
+ return this.resolveID(ref).get('type');
+};
+
+GitEngine.prototype.setTargetLocation = function(ref, target) {
+ if (this.getType(ref) == 'commit') {
+ // nothing to do
+ return;
+ }
+
+ // sets whatever ref is (branch, HEAD, etc) to a target. so if
+ // you pass in HEAD, and HEAD is pointing to a branch, it will update
+ // the branch to that commit, not the HEAD
+ var ref = this.getOneBeforeCommit(ref);
+ ref.set('target', target);
+};
+
+GitEngine.prototype.getUpstreamBranchSet = function() {
+ // this is expensive!! so only call once in a while
+ var commitToSet = {};
+
+ var inArray = function(arr, id) {
+ var found = false;
+ _.each(arr, function(wrapper) {
+ if (wrapper.id == id) {
+ found = true;
+ }
+ });
+
+ return found;
+ };
+
+ var bfsSearch = function(commit) {
+ var set = [];
+ var pQueue = [commit];
+ while (pQueue.length) {
+ var popped = pQueue.pop();
+ set.push(popped.get('id'));
+
+ if (popped.get('parents') && popped.get('parents').length) {
+ pQueue = pQueue.concat(popped.get('parents'));
+ }
+ }
+ return set;
+ };
+
+ this.branchCollection.each(function(branch) {
+ var set = bfsSearch(branch.get('target'));
+ _.each(set, function(id) {
+ commitToSet[id] = commitToSet[id] || [];
+
+ // only add it if it's not there, so hue blending is ok
+ if (!inArray(commitToSet[id], branch.get('id'))) {
+ commitToSet[id].push({
+ obj: branch,
+ id: branch.get('id')
+ });
+ }
+ });
+ });
+
+ return commitToSet;
+};
+
+GitEngine.prototype.getUpstreamHeadSet = function() {
+ var set = this.getUpstreamSet('HEAD');
+ var including = this.getCommitFromRef('HEAD').get('id');
+
+ set[including] = true;
+ return set;
+};
+
+GitEngine.prototype.getOneBeforeCommit = function(ref) {
+ // you can call this command on HEAD in detached, HEAD, or on a branch
+ // and it will return the ref that is one above a commit. aka
+ // it resolves HEAD to something that we can move the ref with
+ var start = this.resolveID(ref);
+ if (start === this.HEAD && !this.getDetachedHead()) {
+ start = start.get('target');
+ }
+ return start;
+};
+
+GitEngine.prototype.numBackFrom = function(commit, numBack) {
+ // going back '3' from a given ref is not trivial, for you might have
+ // a bunch of merge commits and such. like this situation:
+ //
+ // * merge master into new
+ // |\
+ // | \* commit here
+ // |* \ commit there
+ // | |* commit here
+ // \ /
+ // | * root
+ //
+ //
+ // hence we need to do a BFS search, with the commit date being the
+ // value to sort off of (rather than just purely the level)
+ if (numBack == 0) {
+ return commit;
+ }
+
+ // we use a special sorting function here that
+ // prefers the later commits over the earlier ones
+ var sortQueue = _.bind(function(queue) {
+ queue.sort(this.idSortFunc);
+ queue.reverse();
+ }, this);
+
+ var pQueue = [].concat(commit.get('parents') || []);
+ sortQueue(pQueue);
+ numBack--;
+
+ while (pQueue.length && numBack !== 0) {
+ var popped = pQueue.shift(0);
+ var parents = popped.get('parents');
+
+ if (parents && parents.length) {
+ pQueue = pQueue.concat(parents);
+ }
+
+ sortQueue(pQueue);
+ numBack--;
+ }
+
+ if (numBack !== 0 || pQueue.length == 0) {
+ throw new GitError({
+ msg: "Sorry, I can't go that many commits back"
+ });
+ }
+ return pQueue.shift(0);
+};
+
+GitEngine.prototype.scrapeBaseID = function(id) {
+ var results = /^C(\d+)/.exec(id);
+
+ if (!results) {
+ throw new Error('regex failed on ' + id);
+ }
+
+ return 'C' + results[1];
+};
+
+GitEngine.prototype.rebaseAltID = function(id) {
+ // this function alters an ID to add a quote to the end,
+ // indicating that it was rebased. it also checks existence
+ var regexMap = [
+ [/^C(\d+)[']{0,2}$/, function(bits) {
+ // this id can use another quote, so just add it
+ return bits[0] + "'";
+ }],
+ [/^C(\d+)[']{3}$/, function(bits) {
+ // here we switch from C''' to C'^4
+ return bits[0].slice(0, -3) + "'^4";
+ }],
+ [/^C(\d+)['][^](\d+)$/, function(bits) {
+ return 'C' + String(bits[1]) + "'^" + String(Number(bits[2]) + 1);
+ }]
+ ];
+
+ for (var i = 0; i < regexMap.length; i++) {
+ var regex = regexMap[i][0];
+ var func = regexMap[i][1];
+ var results = regex.exec(id);
+ if (results) {
+ var newId = func(results);
+ // if this id exists, continue down the rabbit hole
+ if (this.refs[newId]) {
+ return this.rebaseAltID(newId);
+ } else {
+ return newId;
+ }
+ }
+ }
+ throw new Error('could not modify the id ' + id);
+};
+
+GitEngine.prototype.idSortFunc = function(cA, cB) {
+ // commit IDs can come in many forms:
+ // C4
+ // C4' (from a rebase)
+ // C4'' (from multiple rebases)
+ // C4'^3 (from a BUNCH of rebases)
+
+ var scale = 1000;
+
+ var regexMap = [
+ [/^C(\d+)$/, function(bits) {
+ // return the 4 from C4
+ return scale * bits[1];
+ }],
+ [/^C(\d+)([']+)$/, function(bits) {
+ // return the 4 from C4, plus the length of the quotes
+ return scale * bits[1] + bits[2].length;
+ }],
+ [/^C(\d+)['][^](\d+)$/, function(bits) {
+ return scale * bits[1] + Number(bits[2]);
+ }]
+ ];
+
+ var getNumToSort = function(id) {
+ for (var i = 0; i < regexMap.length; i++) {
+ var regex = regexMap[i][0];
+ var func = regexMap[i][1];
+ var results = regex.exec(id);
+ if (results) {
+ return func(results);
+ }
+ }
+ throw new Error('Could not parse commit ID ' + id);
+ }
+
+ return getNumToSort(cA.get('id')) - getNumToSort(cB.get('id'));
+};
+
+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);
+
+ var response = this.rebase(this.generalArgs[0], this.generalArgs[1]);
+
+ if (response === undefined) {
+ // was a fastforward or already up to date. returning now
+ // will trigger the refresh animation by not adding anything to
+ // the animation queue
+ return;
+ }
+
+ animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
+};
+
+GitEngine.prototype.rebase = function(targetSource, currentLocation) {
+ // first some conditions
+ if (this.isUpstreamOf(targetSource, currentLocation)) {
+ this.command.setResult('Branch already up-to-date');
+
+ // git for some reason always checks out the branch you are rebasing,
+ // no matter the result of the rebase
+ this.checkout(currentLocation);
+
+ // returning instead of throwing makes a tree refresh
+ return;
+ }
+
+ if (this.isUpstreamOf(currentLocation, targetSource)) {
+ // just set the target of this current location to the source
+ this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
+ // we need the refresh tree animation to happen, so set the result directly
+ // instead of throwing
+ this.command.setResult('Fast-forwarding...');
+
+ this.checkout(currentLocation);
+ return;
+ }
+
+ // now the part of actually rebasing.
+ // We need to get the downstream set of targetSource first.
+ // then we BFS from currentLocation, using the downstream set as our stopping point.
+ // we need to BFS because we need to include all commits below
+ // pop these commits on top of targetSource and modify their ids with quotes
+ var stopSet = this.getUpstreamSet(targetSource)
+
+ // now BFS from here on out
+ var toRebaseRough = [];
+ var pQueue = [this.getCommitFromRef(currentLocation)];
+
+ while (pQueue.length) {
+ var popped = pQueue.pop();
+
+ // if its in the set, dont add it
+ if (stopSet[popped.get('id')]) {
+ continue;
+ }
+
+ // it's not in the set, so we need to rebase this commit
+ toRebaseRough.push(popped);
+ toRebaseRough.sort(this.idSortFunc);
+ toRebaseRough.reverse();
+ // keep searching
+ pQueue = pQueue.concat(popped.get('parents'));
+ }
+
+ return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation);
+};
+
+GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation) {
+ // there are a reduced set of checks now, so we can't exactly use parts of the rebase function
+ // but it will look similar.
+
+ // first if we are upstream of the target
+ if (this.isUpstreamOf(currentLocation, targetSource)) {
+ throw new GitError({
+ msg: 'Nothing to do... (git throws a "noop" status here); ' +
+ 'Your source is upstream of your rebase target'
+ });
+ }
+
+ // now get the stop set
+ var stopSet = this.getUpstreamSet(targetSource);
+
+ var toRebaseRough = [];
+ // standard BFS
+ var pQueue = [this.getCommitFromRef(currentLocation)];
+
+ while (pQueue.length) {
+ var popped = pQueue.pop();
+
+ if (stopSet[popped.get('id')]) {
+ continue;
+ }
+
+ toRebaseRough.push(popped);
+ pQueue = pQueue.concat(popped.get('parents'));
+ pQueue.sort(this.idSortFunc);
+ }
+
+ // throw our merge's real fast and see if we have anything to do
+ var toRebase = [];
+ _.each(toRebaseRough, function(commit) {
+ if (commit.get('parents').length == 1) {
+ toRebase.push(commit);
+ }
+ });
+
+ if (!toRebase.length) {
+ throw new GitError({
+ msg: 'No commits to rebase! Everything is a merge commit'
+ });
+ }
+
+ // now do stuff :D since all our validation checks have passed, we are going to defer animation
+ // and actually launch the dialog
+ this.animationQueue.set('defer', true);
+
+ var callback = _.bind(function(userSpecifiedRebase) {
+ // first, they might have dropped everything (annoying)
+ if (!userSpecifiedRebase.length) {
+ this.command.setResult('Nothing to do...');
+ this.animationQueue.start();
+ return;
+ }
+
+ // finish the rebase crap and animate!
+ var animationData = this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation);
+ animationFactory.rebaseAnimation(this.animationQueue, animationData, this, this.gitVisuals);
+ this.animationQueue.start();
+ }, this);
+
+ new InteractiveRebaseView({
+ callback: callback,
+ toRebase: toRebase,
+ el: $('#dialogHolder')
+ });
+};
+
+GitEngine.prototype.rebaseFinish = function(toRebaseRough, stopSet, targetSource, currentLocation) {
+ // now we have the all the commits between currentLocation and the set of target to rebase.
+ var animationResponse = {};
+ animationResponse.destinationBranch = this.resolveID(targetSource);
+
+ // we need to throw out merge commits
+ var toRebase = [];
+ _.each(toRebaseRough, function(commit) {
+ if (commit.get('parents').length == 1) {
+ toRebase.push(commit);
+ }
+ });
+
+ // we ALSO need to throw out commits that will do the same changes. like
+ // if the upstream set has a commit C4 and we have C4', we dont rebase the C4' again.
+ // get this by doing ID scraping
+ var changesAlreadyMade = {};
+ _.each(stopSet, function(val, key) {
+ changesAlreadyMade[this.scrapeBaseID(key)] = val; // val == true
+ }, this);
+
+ // now get rid of the commits that will redo same changes
+ toRebaseRough = toRebase;
+ toRebase = [];
+ _.each(toRebaseRough, function(commit) {
+ var baseID = this.scrapeBaseID(commit.get('id'));
+ if (!changesAlreadyMade[baseID]) {
+ toRebase.push(commit);
+ }
+ }, this);
+
+ if (!toRebase.length) {
+ throw new GitError({
+ msg: 'No Commits to Rebase! Everything else is merge commits or changes already have been applied'
+ });
+ }
+
+ // now reverse it once more to get it in the right order
+ toRebase.reverse();
+ animationResponse.toRebaseArray = toRebase.slice(0);
+
+ // now pop all of these commits onto targetLocation
+ var base = this.getCommitFromRef(targetSource);
+
+ // do the rebase, and also maintain all our animation info during this
+ animationResponse.rebaseSteps = [];
+ var beforeSnapshot = this.gitVisuals.genSnapshot();
+ var afterSnapshot;
+ _.each(toRebase, function(old) {
+ var newId = this.rebaseAltID(old.get('id'));
+
+ var newCommit = this.makeCommit([base], newId);
+ base = newCommit;
+
+ // animation info
+ afterSnapshot = this.gitVisuals.genSnapshot();
+ animationResponse.rebaseSteps.push({
+ oldCommit: old,
+ newCommit: newCommit,
+ beforeSnapshot: beforeSnapshot,
+ afterSnapshot: afterSnapshot
+ });
+ beforeSnapshot = afterSnapshot;
+ }, this);
+
+ if (this.resolveID(currentLocation).get('type') == 'commit') {
+ // we referenced a commit like git rebase C2 C1, so we have
+ // to manually check out C1'
+
+ var steps = animationResponse.rebaseSteps;
+ var newestCommit = steps[steps.length - 1].newCommit;
+
+ this.checkout(newestCommit);
+ } else {
+ // now we just need to update the rebased branch is
+ this.setTargetLocation(currentLocation, base);
+ this.checkout(currentLocation);
+ }
+
+ // for animation
+ return animationResponse;
+};
+
+GitEngine.prototype.mergeStarter = function() {
+ this.twoArgsImpliedHead(this.generalArgs);
+
+ var newCommit = this.merge(this.generalArgs[0], this.generalArgs[1]);
+
+ if (newCommit === undefined) {
+ // its just a fast forwrard
+ animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
+ return;
+ }
+
+ animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
+};
+
+GitEngine.prototype.merge = function(targetSource, currentLocation) {
+ // first some conditions
+ if (this.isUpstreamOf(targetSource, currentLocation) ||
+ this.getCommitFromRef(targetSource) === this.getCommitFromRef(currentLocation)) {
+ throw new CommandResult({
+ msg: 'Branch already up-to-date'
+ });
+ }
+
+ if (this.isUpstreamOf(currentLocation, targetSource)) {
+ // just set the target of this current location to the source
+ this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
+ // get fresh animation to happen
+ this.command.setResult('Fast-forwarding...');
+ return;
+ }
+
+ // now the part of making a merge commit
+ var parent1 = this.getCommitFromRef(currentLocation);
+ var parent2 = this.getCommitFromRef(targetSource);
+
+ // we need a fancy commit message
+ var msg = 'Merge ' + this.resolveName(targetSource) +
+ ' into ' + this.resolveName(currentLocation);
+
+ // since we specify parent 1 as the first parent, it is the "main" parent
+ // and the node will be displayed below that branch / commit / whatever
+ var mergeCommit = this.makeCommit(
+ [parent1, parent2],
+ null,
+ {
+ commitMessage: msg
+ }
+ );
+
+ this.setTargetLocation(currentLocation, mergeCommit)
+ return mergeCommit;
+};
+
+GitEngine.prototype.checkoutStarter = function() {
+ if (this.commandOptions['-b']) {
+ // the user is really trying to just make a branch and then switch to it. so first:
+ var args = this.commandOptions['-b'];
+ this.twoArgsImpliedHead(args, '-b');
+
+ var validId = this.validateBranchName(args[0]);
+ this.branch(validId, args[1]);
+ this.checkout(validId);
+ return;
+ }
+
+ if (this.commandOptions['-']) {
+ // get the heads last location
+ var lastPlace = this.HEAD.get('lastLastTarget');
+ if (!lastPlace) {
+ throw new GitError({
+ msg: 'Need a previous location to do - switching'
+ });
+ }
+ this.HEAD.set('target', lastPlace);
+ return;
+ }
+
+ if (this.commandOptions['-B']) {
+ var args = this.commandOptions['-B'];
+ this.twoArgsImpliedHead(args, '-B');
+
+ this.forceBranch(args[0], args[1]);
+ this.checkout(args[0]);
+ return;
+ }
+
+ this.validateArgBounds(this.generalArgs, 1, 1);
+
+ this.checkout(this.unescapeQuotes(this.generalArgs[0]));
+};
+
+GitEngine.prototype.checkout = function(idOrTarget) {
+ var target = this.resolveID(idOrTarget);
+ if (target.get('id') === 'HEAD') {
+ // git checkout HEAD is a
+ // meaningless command but i used to do this back in the day
+ return;
+ }
+
+ var type = target.get('type');
+ if (type !== 'branch' && type !== 'commit') {
+ throw new GitError({
+ msg: 'can only checkout branches and commits!'
+ });
+ }
+
+ this.HEAD.set('target', target);
+};
+
+GitEngine.prototype.branchStarter = function() {
+ // 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']) {
+ var args = this.commandOptions['--contains'];
+ this.validateArgBounds(args, 1, 1, '--contains');
+ this.printBranchesWithout(args[0]);
+ return;
+ }
+
+ if (this.commandOptions['-f']) {
+ var 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) {
+ this.printBranches(this.getBranches());
+ return;
+ }
+
+ this.twoArgsImpliedHead(this.generalArgs);
+ this.branch(this.generalArgs[0], this.generalArgs[1]);
+};
+
+GitEngine.prototype.forceBranch = function(branchName, where) {
+ // if branchname doesn't exist...
+ if (!this.refs[branchName]) {
+ this.branch(branchName, where);
+ }
+
+ var branch = this.resolveID(branchName);
+ if (branch.get('type') !== 'branch') {
+ throw new GitError({
+ msg: "Can't force move anything but a branch!!"
+ });
+ }
+
+ var whereCommit = this.getCommitFromRef(where);
+
+ this.setTargetLocation(branch, whereCommit);
+};
+
+GitEngine.prototype.branch = function(name, ref) {
+ var target = this.getCommitFromRef(ref);
+ this.makeBranch(name, target);
+};
+
+GitEngine.prototype.deleteBranch = function(name) {
+ // trying to delete, lets check our refs
+ var target = this.resolveID(name);
+ if (target.get('type') !== 'branch') {
+ throw new GitError({
+ msg: "You can't delete things that arent branches with branch command"
+ });
+ }
+ if (target.get('id') == 'master') {
+ throw new GitError({
+ msg: "You can't delete the master branch!"
+ });
+ }
+ if (this.HEAD.get('target') === target) {
+ throw new GitError({
+ msg: "Cannot delete the branch you are currently on"
+ });
+ }
+
+ // now we know it's a branch
+ var branch = target;
+
+ this.branchCollection.remove(branch);
+ this.refs[branch.get('id')] = undefined;
+ delete this.refs[branch.get('id')];
+
+ if (branch.get('visBranch')) {
+ branch.get('visBranch').remove();
+ }
+};
+
+GitEngine.prototype.unescapeQuotes = function(str) {
+ return str.replace(/'/g, "'");
+}
+
+GitEngine.prototype.dispatch = function(command, callback) {
+ // 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() {
+ command.set('status', 'finished');
+ callback();
+ }, this);
+ this.animationQueue = new AnimationQueue({
+ callback: whenDone
+ });
+
+ command.set('status', 'processing');
+ try {
+ var methodName = command.get('method').replace(/-/g, '') + 'Starter';
+ this[methodName]();
+ } catch (err) {
+ if (err instanceof GitError ||
+ err instanceof CommandResult) {
+ // short circuit animation by just setting error and returning
+ command.set('error', err);
+ callback();
+ return;
+ } else {
+ throw err;
+ }
+ }
+
+ // only add the refresh if we didn't do manual animations
+ if (!this.animationQueue.get('animations').length && !this.animationQueue.get('defer')) {
+ animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
+ }
+
+ // animation queue will call the callback when its done
+ if (!this.animationQueue.get('defer')) {
+ this.animationQueue.start();
+ }
+};
+
+GitEngine.prototype.showStarter = function() {
+ this.oneArgImpliedHead(this.generalArgs);
+
+ this.show(this.generalArgs[0]);
+};
+
+GitEngine.prototype.show = function(ref) {
+ var commit = this.getCommitFromRef(ref);
+
+ throw new CommandResult({
+ msg: commit.getShowEntry()
+ });
+};
+
+GitEngine.prototype.statusStarter = function() {
+ var lines = [];
+ if (this.getDetachedHead()) {
+ lines.push('Detached Head!');
+ } else {
+ var branchName = this.HEAD.get('target').get('id');
+ lines.push('On branch ' + branchName);
+ }
+ lines.push('Changes to be committed:');
+ lines.push('');
+ lines.push(' modified: cal/OskiCostume.stl');
+ lines.push('');
+ lines.push('Ready to commit! (as always in this demo)');
+
+ var msg = '';
+ _.each(lines, function(line) {
+ msg += '# ' + line + '\n';
+ });
+
+ throw new CommandResult({
+ msg: msg
+ });
+};
+
+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: 'I need a not branch (^branchName) when getting two arguments!'
+ });
+ }
+ }
+
+ this.oneArgImpliedHead(this.generalArgs);
+ this.log(this.generalArgs[0]);
+};
+
+GitEngine.prototype.logWithout = function(ref, omitBranch) {
+ // slice off the ^branch
+ omitBranch = omitBranch.slice(1);
+ this.log(ref, this.getUpstreamSet(omitBranch));
+};
+
+GitEngine.prototype.log = function(ref, omitSet) {
+ // omit set is for doing stuff like git log branchA ^branchB
+ omitSet = omitSet || {};
+ // first get the commit we referenced
+ var commit = this.getCommitFromRef(ref);
+
+ // then get as many far back as we can from here, order by commit date
+ var toDump = [];
+ var pQueue = [commit];
+
+ var seen = {};
+
+ while (pQueue.length) {
+ var popped = pQueue.shift(0);
+ if (seen[popped.get('id')] || omitSet[popped.get('id')]) {
+ continue;
+ }
+ seen[popped.get('id')] = true;
+
+ toDump.push(popped);
+
+ if (popped.get('parents') && popped.get('parents').length) {
+ pQueue = pQueue.concat(popped.get('parents'));
+ }
+ }
+
+ // now go through and collect logs
+ var bigLogStr = '';
+ _.each(toDump, function(c) {
+ bigLogStr += c.getLogEntry();
+ }, this);
+
+ throw new CommandResult({
+ msg: bigLogStr
+ });
+};
+
+GitEngine.prototype.addStarter = function() {
+ throw new CommandResult({
+ msg: "This demo is meant to demonstrate git branching, so don't worry about " +
+ "adding / staging files. Just go ahead and commit away!"
+ });
+};
+
+GitEngine.prototype.getCommonAncestor = function(ancestor, cousin) {
+ if (this.isUpstreamOf(cousin, ancestor)) {
+ throw new Error('Dont use common ancestor if we are upstream!');
+ }
+
+ var upstreamSet = this.getUpstreamSet(ancestor);
+ // now BFS off of cousin until you find something
+
+ var queue = [this.getCommitFromRef(cousin)];
+ while (queue.length) {
+ var here = queue.pop();
+ if (upstreamSet[here.get('id')]) {
+ return here;
+ }
+ queue = queue.concat(here.get('parents'));
+ }
+ throw new Error('something has gone very wrong... two nodes arent connected!');
+};
+
+GitEngine.prototype.isUpstreamOf = function(child, ancestor) {
+ child = this.getCommitFromRef(child);
+
+ // basically just do a completely BFS search on ancestor to the root, then
+ // check for membership of child in that set of explored nodes
+ var upstream = this.getUpstreamSet(ancestor);
+ return upstream[child.get('id')] !== undefined;
+};
+
+GitEngine.prototype.getUpstreamSet = function(ancestor) {
+ var commit = this.getCommitFromRef(ancestor);
+ var ancestorID = commit.get('id');
+ var queue = [commit];
+
+ var exploredSet = {};
+ exploredSet[ancestorID] = true;
+ while (queue.length) {
+ var here = queue.pop();
+ var rents = here.get('parents');
+
+ _.each(rents, function(rent) {
+ exploredSet[rent.get('id')] = true;
+ queue.push(rent);
+ });
+ }
+ return exploredSet;
+};
+
+
+var Ref = Backbone.Model.extend({
+ initialize: function() {
+ if (!this.get('target')) {
+ throw new Error('must be initialized with target');
+ }
+ if (!this.get('id')) {
+ throw new Error('must be given an id');
+ }
+ this.set('type', 'general ref');
+
+ if (this.get('id') == 'HEAD') {
+ this.set('lastLastTarget', null);
+ this.set('lastTarget', this.get('target'));
+ // have HEAD remember where it is for checkout -
+ this.on('change:target', this.targetChanged, this);
+ }
+ },
+
+ targetChanged: function(model, targetValue, ev) {
+ // push our little 3 stack back. we need to do this because
+ // backbone doesn't give you what the value WAS, only what it was changed
+ // TO
+ this.set('lastLastTarget', this.get('lastTarget'));
+ this.set('lastTarget', targetValue);
+ },
+
+ toString: function() {
+ return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target'));
+ }
+});
+
+var Branch = Ref.extend({
+ defaults: {
+ visBranch: null,
+ },
+
+ initialize: function() {
+ Ref.prototype.initialize.call(this);
+ this.set('type', 'branch');
+ }
+});
+
+var Commit = Backbone.Model.extend({
+ defaults: {
+ type: 'commit',
+ children: null,
+ parents: null,
+ author: 'Peter Cottle',
+ createTime: null,
+ commitMessage: null,
+ visNode: null,
+ gitVisuals: null
+ },
+
+ constants: {
+ circularFields: ['gitVisuals', 'visNode', 'children']
+ },
+
+ getLogEntry: function() {
+ // for now we are just joining all these things with newlines which
+ // will get placed by paragraph tags. Not really a fan of this, but
+ // it's better than making an entire template and all that jazz
+ return [
+ 'Author: ' + this.get('author'),
+ 'Date: ' + this.get('createTime'),
+ '
',
+ this.get('commitMessage'),
+ '
',
+ 'Commit: ' + this.get('id')
+ ].join('\n' ) + '\n';
+ },
+
+ getShowEntry: function() {
+ // same deal as above, show log entry and some fake changes
+ return [
+ this.getLogEntry(),
+ 'diff --git a/bigGameResults.html b/bigGameResults.html',
+ '--- bigGameResults.html',
+ '+++ bigGameResults.html',
+ '@@ 13,27 @@ Winner, Score',
+ '- Stanfurd, 14-7',
+ '+ Cal, 21-14',
+ ].join('\n') + '\n';
+ },
+
+ validateAtInit: function() {
+ if (!this.get('id')) {
+ throw new Error('Need ID!!');
+ }
+
+ if (!this.get('createTime')) {
+ this.set('createTime', new Date().toString());
+ }
+ if (!this.get('commitMessage')) {
+ this.set('commitMessage', 'Quick Commit. Go Bears!');
+ }
+
+ this.set('children', []);
+
+ // root commits have no parents
+ if (!this.get('rootCommit')) {
+ if (!this.get('parents') || !this.get('parents').length) {
+ throw new Error('needs parents');
+ }
+ }
+ },
+
+ addNodeToVisuals: function() {
+ var visNode = this.get('gitVisuals').addNode(this.get('id'), this);
+ this.set('visNode', visNode);
+ },
+
+ addEdgeToVisuals: function(parent) {
+ this.get('gitVisuals').addEdge(this.get('id'), parent.get('id'));
+ },
+
+ isMainParent: function(parent) {
+ var index = this.get('parents').indexOf(parent);
+ return index === 0;
+ },
+
+ initialize: function(options) {
+ this.validateAtInit();
+ this.addNodeToVisuals();
+
+ _.each(this.get('parents'), function(parent) {
+ parent.get('children').push(this);
+ this.addEdgeToVisuals(parent);
+ }, this);
+ }
+});
+
+exports.GitEngine = GitEngine;
+exports.Commit = Commit;
+exports.Branch = Branch;
+exports.Ref = Ref;
+
+
+});
+require("/git.js");
+
+require.define("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit;
+var Branch = require('./git').Branch;
+
+var CommitCollection = Backbone.Collection.extend({
+ model: Commit
+});
+
+var CommandCollection = Backbone.Collection.extend({
+ model: Command,
+});
+
+var BranchCollection = Backbone.Collection.extend({
+ model: Branch
+});
+
+var CommandEntryCollection = Backbone.Collection.extend({
+ model: CommandEntry,
+ localStorage: new Backbone.LocalStorage('CommandEntries')
+});
+
+var CommandBuffer = Backbone.Model.extend({
+ defaults: {
+ collection: null,
+ },
+
+ initialize: function(options) {
+ events.on('gitCommandReady', _.bind(
+ this.addCommand, this
+ ));
+
+ options.collection.bind('add', this.addCommand, this);
+
+ this.buffer = [];
+ this.timeout = null;
+ },
+
+ addCommand: function(command) {
+ this.buffer.push(command);
+ this.touchBuffer();
+ },
+
+ touchBuffer: function() {
+ // touch buffer just essentially means we just check if our buffer is being
+ // processed. if it's not, we immediately process the first item
+ // and then set the timeout.
+ if (this.timeout) {
+ // timeout existence implies its being processed
+ return;
+ }
+ this.setTimeout();
+ },
+
+
+ setTimeout: function() {
+ this.timeout = setTimeout(_.bind(function() {
+ this.sipFromBuffer();
+ }, this), TIME.betweenCommandsDelay);
+ },
+
+ popAndProcess: function() {
+ var popped = this.buffer.shift(0);
+ var callback = _.bind(function() {
+ this.setTimeout();
+ }, this);
+
+ // find a command with no error
+ while (popped.get('error') && this.buffer.length) {
+ popped = this.buffer.pop();
+ }
+ if (!popped.get('error')) {
+ // pass in a callback, so when this command is "done" we will process the next.
+ events.trigger('processCommand', popped, callback);
+ } else {
+ this.clear();
+ }
+ },
+
+ clear: function() {
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ },
+
+ sipFromBuffer: function() {
+ if (!this.buffer.length) {
+ this.clear();
+ return;
+ }
+
+ this.popAndProcess();
+ },
+});
+
+exports.CommitCollection = CommitCollection;
+exports.CommandCollection = CommandCollection;
+exports.BranchCollection = BranchCollection;
+exports.CommandEntryCollection = CommandEntryCollection;
+exports.CommandBuffer = CommandBuffer;
+
+
+});
+require("/collections.js");
+
+require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection;
+
+var CommandPromptView = Backbone.View.extend({
+ initialize: function(options) {
+ this.collection = options.collection;
+
+ // uses local storage
+ this.commands = new CommandEntryCollection();
+ this.commands.fetch({
+ success: _.bind(function() {
+ // reverse the commands. this is ugly but needs to be done...
+ var commands = [];
+ this.commands.each(function(c) {
+ commands.push(c);
+ });
+
+ commands.reverse();
+ this.commands.reset();
+
+ _.each(commands, function(c) {
+ this.commands.add(c);
+ }, this);
+ }, this)
+ });
+
+ this.index = -1;
+
+ this.commandSpan = this.$('#prompt span.command')[0];
+ this.commandCursor = this.$('#prompt span.cursor')[0];
+
+ // this is evil, but we will refer to HTML outside the document
+ // and attach a click event listener so we can focus / unfocus
+ $(document).delegate('#commandLineHistory', 'click', _.bind(function() {
+ this.focus();
+ }, this));
+
+
+ $(document).delegate('#commandTextField', 'blur', _.bind(function() {
+ this.blur();
+ }, this));
+
+ events.on('processCommandFromEvent', this.addToCollection, this);
+ events.on('submitCommandValueFromEvent', this.submitValue, this);
+ events.on('rollupCommands', this.rollupCommands, this);
+
+ // hacky timeout focus
+ setTimeout(_.bind(function() {
+ this.focus();
+ }, this), 100);
+ },
+
+ events: {
+ 'keydown #commandTextField': 'onKey',
+ 'keyup #commandTextField': 'onKeyUp',
+ 'blur #commandTextField': 'hideCursor',
+ 'focus #commandTextField': 'showCursor'
+ },
+
+ blur: function() {
+ $(this.commandCursor).toggleClass('shown', false);
+ },
+
+ focus: function() {
+ this.$('#commandTextField').focus();
+ this.showCursor();
+ },
+
+ hideCursor: function() {
+ this.toggleCursor(false);
+ },
+
+ showCursor: function() {
+ this.toggleCursor(true);
+ },
+
+ toggleCursor: function(state) {
+ $(this.commandCursor).toggleClass('shown', state);
+ },
+
+ onKey: function(e) {
+ var el = e.srcElement;
+ this.updatePrompt(el)
+ },
+
+ onKeyUp: function(e) {
+ this.onKey(e);
+
+ // we need to capture some of these events.
+ // WARNING: this key map is not internationalized :(
+ var keyMap = {
+ // enter
+ 13: _.bind(function() {
+ this.submit();
+ }, this),
+ // up
+ 38: _.bind(function() {
+ this.commandSelectChange(1);
+ }, this),
+ // down
+ 40: _.bind(function() {
+ this.commandSelectChange(-1);
+ }, this)
+ };
+
+ if (keyMap[e.which] !== undefined) {
+ e.preventDefault();
+ keyMap[e.which]();
+ this.onKey(e);
+ }
+ },
+
+ badHtmlEncode: function(text) {
+ return text.replace(/&/g,'&')
+ .replace(/= this.commands.length || this.index < 0) {
+ this.clear();
+ this.index = -1;
+ return;
+ }
+
+ // yay! we actually can display something
+ var commandEntry = this.commands.toArray()[this.index].get('text');
+ this.setTextField(commandEntry);
+ },
+
+ clearLocalStorage: function() {
+ this.commands.each(function(c) {
+ Backbone.sync('delete', c, function() { });
+ }, this);
+ localStorage.setItem('CommandEntries', '');
+ },
+
+ setTextField: function(value) {
+ this.$('#commandTextField').val(value);
+ },
+
+ clear: function() {
+ this.setTextField('');
+ },
+
+ submit: function() {
+ var value = this.$('#commandTextField').val().replace('\n', '');
+ this.clear();
+ this.submitValue(value);
+ },
+
+ rollupCommands: function(numBack) {
+ var which = this.commands.toArray().slice(1, Number(numBack) + 1);
+ which.reverse();
+
+ var str = '';
+ _.each(which, function(commandEntry) {
+ str += commandEntry.get('text') + ';';
+ }, this);
+
+ console.log('the str', str);
+
+ var rolled = new CommandEntry({text: str});
+ this.commands.unshift(rolled);
+ Backbone.sync('create', rolled, function() { });
+ },
+
+ submitValue: function(value) {
+ // we should add if it's not a blank line and this is a new command...
+ // or if we edited the command
+ var shouldAdd = (value.length && this.index == -1) ||
+ ((value.length && this.index !== -1 &&
+ this.commands.toArray()[this.index].get('text') !== value));
+
+ if (shouldAdd) {
+ var commandEntry = new CommandEntry({text: value});
+ this.commands.unshift(commandEntry);
+
+ // store to local storage
+ Backbone.sync('create', commandEntry, function() { });
+
+ // if our length is too egregious, reset
+ if (this.commands.length > 100) {
+ this.clearLocalStorage();
+ }
+ }
+ this.index = -1;
+
+ // split commands on semicolon
+ _.each(value.split(';'), _.bind(function(command, index) {
+ command = _.escape(command);
+
+ command = command
+ .replace(/^(\s+)/, '')
+ .replace(/(\s+)$/, '')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'");
+
+ if (index > 0 && !command.length) {
+ return;
+ }
+
+ this.addToCollection(command);
+ }, this));
+ },
+
+ addToCollection: function(value) {
+ var command = new Command({
+ rawStr: value
+ });
+ this.collection.add(command);
+ }
+});
+
+
+// This is the view for all commands -- it will represent
+// their status (inqueue, processing, finished, error),
+// their value ("git commit --amend"),
+// and the result (either errors or warnings or whatever)
+var CommandView = Backbone.View.extend({
+ tagName: 'div',
+ model: Command,
+ template: _.template($('#command-template').html()),
+
+ events: {
+ 'click': 'clicked'
+ },
+
+ clicked: function(e) {
+ },
+
+ initialize: function() {
+ this.model.bind('change', this.wasChanged, this);
+ this.model.bind('destroy', this.remove, this);
+ },
+
+ wasChanged: function(model, changeEvent) {
+ // for changes that are just comestic, we actually only want to toggle classes
+ // with jquery rather than brutally delete a html of HTML
+ var changes = changeEvent.changes;
+ var changeKeys = _.keys(changes);
+ if (_.difference(changeKeys, ['status']) == 0) {
+ this.updateStatus();
+ } else if (_.difference(changeKeys, ['error']) == 0) {
+ // the above will
+ this.render();
+ } else {
+ this.render();
+ }
+ },
+
+ updateStatus: function() {
+ var statuses = ['inqueue', 'processing', 'finished'];
+ var toggleMap = {};
+ _.each(statuses, function(status) {
+ toggleMap[status] = false;
+ });
+ toggleMap[this.model.get('status')] = true;
+
+ var query = this.$('p.commandLine');
+
+ _.each(toggleMap, function(value, key) {
+ query.toggleClass(key, value);
+ });
+ },
+
+ render: function() {
+ var json = _.extend(
+ {
+ resultType: '',
+ result: '',
+ formattedWarnings: this.model.getFormattedWarnings()
+ },
+ this.model.toJSON()
+ );
+
+ this.$el.html(this.template(json));
+ return this;
+ },
+
+ remove: function() {
+ $(this.el).hide();
+ }
+});
+
+
+var CommandLineHistoryView = Backbone.View.extend({
+ initialize: function(options) {
+ this.collection = options.collection;
+
+ this.collection.on('add', this.addOne, this);
+ this.collection.on('reset', this.addAll, this);
+ this.collection.on('all', this.render, this);
+
+ this.collection.on('change', this.scrollDown, this);
+
+ events.on('issueWarning', this.addWarning, this);
+ events.on('commandScrollDown', this.scrollDown, this);
+ },
+
+ addWarning: function(msg) {
+ var err = new Warning({
+ msg: msg
+ });
+
+ var command = new Command({
+ error: err,
+ rawStr: 'Warning:'
+ });
+
+ this.collection.add(command);
+ },
+
+ scrollDown: function() {
+ // if commandDisplay is ever bigger than #terminal, we need to
+ // add overflow-y to terminal and scroll down
+ var cD = $('#commandDisplay')[0];
+ var t = $('#terminal')[0];
+
+ if ($(t).hasClass('scrolling')) {
+ t.scrollTop = t.scrollHeight;
+ return;
+ }
+ if (cD.clientHeight > t.clientHeight) {
+ $(t).css('overflow-y', 'scroll');
+ $(t).css('overflow-x', 'hidden');
+ $(t).addClass('scrolling');
+ t.scrollTop = t.scrollHeight;
+ }
+ },
+
+ addOne: function(command) {
+ var view = new CommandView({
+ model: command
+ });
+ this.$('#commandDisplay').append(view.render().el);
+ this.scrollDown();
+ },
+
+ addAll: function() {
+ this.collection.each(this.addOne);
+ }
+});
+
+exports.CommandPromptView = CommandPromptView;
+exports.CommandLineHistoryView = CommandLineHistoryView;
+
+
+});
+require("/commandViews.js");
+
+require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var CommitCollection = require('./collections').CommitCollection;
+var BranchCollection = require('./collections').BranchCollection;
+var GitEngine = require('./git').GitEngine;
+
+var Visualization = Backbone.View.extend({
+ initialize: function(options) {
+ var _this = this;
+ Raphael(10, 10, 200, 200, function() {
+
+ // for some reason raphael calls this function with a predefined
+ // context...
+ // so switch it
+ _this.paperInitialize(this);
+ });
+ },
+
+ paperInitialize: function(paper, options) {
+ this.paper = paper;
+
+ this.commitCollection = new CommitCollection();
+ this.branchCollection = new BranchCollection();
+
+ this.gitVisuals = new GitVisuals({
+ commitCollection: this.commitCollection,
+ branchCollection: this.branchCollection,
+ paper: this.paper
+ });
+
+ this.gitEngine = new GitEngine({
+ collection: this.commitCollection,
+ branches: this.branchCollection,
+ gitVisuals: this.gitVisuals
+ });
+ this.gitEngine.init();
+ this.gitVisuals.assignGitEngine(this.gitEngine);
+
+ this.myResize();
+ $(window).on('resize', _.bind(this.myResize, this));
+ this.gitVisuals.drawTreeFirstTime();
+ },
+
+ myResize: function() {
+ var smaller = 1;
+ var el = this.el;
+
+ var left = el.offsetLeft;
+ var top = el.offsetTop;
+ var width = el.clientWidth - smaller;
+ var height = el.clientHeight - smaller;
+
+ $(this.paper.canvas).css({
+ left: left + 'px',
+ top: top + 'px'
+ });
+ this.paper.setSize(width, height);
+ this.gitVisuals.canvasResize(width, height);
+ }
+
+});
+
+function GitVisuals(options) {
+ this.commitCollection = options.commitCollection;
+ this.branchCollection = options.branchCollection;
+ this.visNodeMap = {};
+
+ this.visEdgeCollection = new VisEdgeCollection();
+ this.visBranchCollection = new VisBranchCollection();
+ this.commitMap = {};
+
+ this.rootCommit = null;
+ this.branchStackMap = null;
+ this.upstreamBranchSet = null;
+ this.upstreamHeadSet = null;
+
+ this.paper = options.paper;
+ this.gitReady = false;
+
+ this.branchCollection.on('add', this.addBranchFromEvent, this);
+ this.branchCollection.on('remove', this.removeBranch, this);
+ this.deferred = [];
+
+ events.on('refreshTree', _.bind(
+ this.refreshTree, this
+ ));
+}
+
+GitVisuals.prototype.defer = function(action) {
+ this.deferred.push(action);
+};
+
+GitVisuals.prototype.deferFlush = function() {
+ _.each(this.deferred, function(action) {
+ action();
+ }, this);
+ this.deferred = [];
+};
+
+GitVisuals.prototype.resetAll = function() {
+ // make sure to copy these collections because we remove
+ // items in place and underscore is too dumb to detect length change
+ var edges = this.visEdgeCollection.toArray();
+ _.each(edges, function(visEdge) {
+ visEdge.remove();
+ }, this);
+
+ var branches = this.visBranchCollection.toArray();
+ _.each(branches, function(visBranch) {
+ visBranch.remove();
+ }, this);
+
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.remove();
+ }, this);
+
+ this.visEdgeCollection.reset();
+ this.visBranchCollection.reset();
+
+ this.visNodeMap = {};
+ this.rootCommit = null;
+ this.commitMap = {};
+};
+
+GitVisuals.prototype.assignGitEngine = function(gitEngine) {
+ this.gitEngine = gitEngine;
+ this.initHeadBranch();
+ this.deferFlush();
+};
+
+GitVisuals.prototype.initHeadBranch = function() {
+ // it's unfortaunte we have to do this, but the head branch
+ // is an edge case because it's not part of a collection so
+ // we can't use events to load or unload it. thus we have to call
+ // this ugly method which will be deleted one day
+
+ // seed this with the HEAD pseudo-branch
+ this.addBranchFromEvent(this.gitEngine.HEAD);
+};
+
+GitVisuals.prototype.getScreenBounds = function() {
+ // for now we return the node radius subtracted from the walls
+ return {
+ widthPadding: GRAPHICS.nodeRadius * 1.5,
+ heightPadding: GRAPHICS.nodeRadius * 1.5
+ };
+};
+
+GitVisuals.prototype.toScreenCoords = function(pos) {
+ if (!this.paper.width) {
+ throw new Error('being called too early for screen coords');
+ }
+ var bounds = this.getScreenBounds();
+
+ var shrink = function(frac, total, padding) {
+ return padding + frac * (total - padding * 2);
+ };
+
+ return {
+ x: shrink(pos.x, this.paper.width, bounds.widthPadding),
+ y: shrink(pos.y, this.paper.height, bounds.heightPadding)
+ };
+};
+
+GitVisuals.prototype.animateAllFromAttrToAttr = function(fromSnapshot, toSnapshot, idsToOmit) {
+ var animate = function(obj) {
+ var id = obj.getID();
+ if (_.include(idsToOmit, id)) {
+ return;
+ }
+
+ if (!fromSnapshot[id] || !toSnapshot[id]) {
+ // its actually ok it doesnt exist yet
+ return;
+ }
+ obj.animateFromAttrToAttr(fromSnapshot[id], toSnapshot[id]);
+ };
+
+ this.visBranchCollection.each(function(visBranch) {
+ animate(visBranch);
+ });
+ this.visEdgeCollection.each(function(visEdge) {
+ animate(visEdge);
+ });
+ _.each(this.visNodeMap, function(visNode) {
+ animate(visNode);
+ });
+};
+
+/***************************************
+ == BEGIN Tree Calculation Parts ==
+ _ __ __ _
+ \\/ / \ \//_
+ \ \ / __| __
+ \ \___/ /_____/ /
+ | _______ \
+ \ ( ) / \_\
+ \ /
+ | |
+ | |
+ ____+-_=+-^ ^+-=_=__________
+
+^^ I drew that :D
+
+ **************************************/
+
+GitVisuals.prototype.genSnapshot = function() {
+ this.fullCalc();
+
+ var snapshot = {};
+ _.each(this.visNodeMap, function(visNode) {
+ snapshot[visNode.get('id')] = visNode.getAttributes();
+ }, this);
+
+ this.visBranchCollection.each(function(visBranch) {
+ snapshot[visBranch.getID()] = visBranch.getAttributes();
+ }, this);
+
+ this.visEdgeCollection.each(function(visEdge) {
+ snapshot[visEdge.getID()] = visEdge.getAttributes();
+ }, this);
+
+ return snapshot;
+};
+
+GitVisuals.prototype.refreshTree = function(speed) {
+ if (!this.gitReady) {
+ return;
+ }
+
+ // this method can only be called after graphics are rendered
+ this.fullCalc();
+
+ this.animateAll(speed);
+};
+
+GitVisuals.prototype.refreshTreeHarsh = function() {
+ this.fullCalc();
+
+ this.animateAll(0);
+};
+
+GitVisuals.prototype.animateAll = function(speed) {
+ this.zIndexReflow();
+
+ this.animateEdges(speed);
+ this.animateNodePositions(speed);
+ this.animateRefs(speed);
+};
+
+GitVisuals.prototype.fullCalc = function() {
+ this.calcTreeCoords();
+ this.calcGraphicsCoords();
+};
+
+GitVisuals.prototype.calcTreeCoords = function() {
+ // this method can only contain things that dont rely on graphics
+ if (!this.rootCommit) {
+ throw new Error('grr, no root commit!');
+ }
+
+ this.calcUpstreamSets();
+ this.calcBranchStacks();
+
+ this.calcDepth();
+ this.calcWidth();
+};
+
+GitVisuals.prototype.calcGraphicsCoords = function() {
+ this.visBranchCollection.each(function(visBranch) {
+ visBranch.updateName();
+ });
+};
+
+GitVisuals.prototype.calcUpstreamSets = function() {
+ this.upstreamBranchSet = this.gitEngine.getUpstreamBranchSet();
+ this.upstreamHeadSet = this.gitEngine.getUpstreamHeadSet();
+};
+
+GitVisuals.prototype.getCommitUpstreamBranches = function(commit) {
+ return this.branchStackMap[commit.get('id')];
+};
+
+GitVisuals.prototype.getBlendedHuesForCommit = function(commit) {
+ var branches = this.upstreamBranchSet[commit.get('id')];
+ if (!branches) {
+ throw new Error('that commit doesnt have upstream branches!');
+ }
+
+ return this.blendHuesFromBranchStack(branches);
+};
+
+GitVisuals.prototype.blendHuesFromBranchStack = function(branchStackArray) {
+ var hueStrings = [];
+ _.each(branchStackArray, function(branchWrapper) {
+ var fill = branchWrapper.obj.get('visBranch').get('fill');
+
+ if (fill.slice(0,3) !== 'hsb') {
+ // crap! convert
+ var color = Raphael.color(fill);
+ fill = 'hsb(' + String(color.h) + ',' + String(color.l);
+ fill = fill + ',' + String(color.s) + ')';
+ }
+
+ hueStrings.push(fill);
+ });
+
+ return blendHueStrings(hueStrings);
+};
+
+GitVisuals.prototype.getCommitUpstreamStatus = function(commit) {
+ if (!this.upstreamBranchSet) {
+ throw new Error("Can't calculate this yet!");
+ }
+
+ var id = commit.get('id');
+ var branch = this.upstreamBranchSet;
+ var head = this.upstreamHeadSet;
+
+ if (branch[id]) {
+ return 'branch';
+ } else if (head[id]) {
+ return 'head';
+ } else {
+ return 'none';
+ }
+};
+
+GitVisuals.prototype.calcBranchStacks = function() {
+ var branches = this.gitEngine.getBranches();
+ var map = {};
+ _.each(branches, function(branch) {
+ var thisId = branch.target.get('id');
+
+ map[thisId] = map[thisId] || [];
+ map[thisId].push(branch);
+ map[thisId].sort(function(a, b) {
+ var aId = a.obj.get('id');
+ var bId = b.obj.get('id');
+ if (aId == 'master' || bId == 'master') {
+ return aId == 'master' ? -1 : 1;
+ }
+ return aId.localeCompare(bId);
+ });
+ });
+ this.branchStackMap = map;
+};
+
+GitVisuals.prototype.calcWidth = function() {
+ this.maxWidthRecursive(this.rootCommit);
+
+ this.assignBoundsRecursive(this.rootCommit, 0, 1);
+};
+
+GitVisuals.prototype.maxWidthRecursive = function(commit) {
+ var childrenTotalWidth = 0;
+ _.each(commit.get('children'), function(child) {
+ // only include this if we are the "main" parent of
+ // this child
+ if (child.isMainParent(commit)) {
+ var childWidth = this.maxWidthRecursive(child);
+ childrenTotalWidth += childWidth;
+ }
+ }, this);
+
+ var maxWidth = Math.max(1, childrenTotalWidth);
+ commit.get('visNode').set('maxWidth', maxWidth);
+ return maxWidth;
+};
+
+GitVisuals.prototype.assignBoundsRecursive = function(commit, min, max) {
+ // I always center myself within my bounds
+ var myWidthPos = (min + max) / 2.0;
+ commit.get('visNode').get('pos').x = myWidthPos;
+
+ if (commit.get('children').length == 0) {
+ return;
+ }
+
+ // i have a certain length to divide up
+ var myLength = max - min;
+ // I will divide up that length based on my children's max width in a
+ // basic box-flex model
+ var totalFlex = 0;
+ var children = commit.get('children');
+ _.each(children, function(child) {
+ if (child.isMainParent(commit)) {
+ totalFlex += child.get('visNode').getMaxWidthScaled();
+ }
+ }, this);
+
+ var prevBound = min;
+
+ // now go through and do everything
+ // TODO: order so the max width children are in the middle!!
+ _.each(children, function(child) {
+ if (!child.isMainParent(commit)) {
+ return;
+ }
+
+ var flex = child.get('visNode').getMaxWidthScaled();
+ var portion = (flex / totalFlex) * myLength;
+ var childMin = prevBound;
+ var childMax = childMin + portion;
+ this.assignBoundsRecursive(child, childMin, childMax);
+ prevBound = childMax;
+ }, this);
+};
+
+GitVisuals.prototype.calcDepth = function() {
+ var maxDepth = this.calcDepthRecursive(this.rootCommit, 0);
+ if (maxDepth > 15) {
+ // issue warning
+ console.warn('graphics are degrading from too many layers');
+ }
+
+ var depthIncrement = this.getDepthIncrement(maxDepth);
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.setDepthBasedOn(depthIncrement);
+ }, this);
+};
+
+/***************************************
+ == END Tree Calculation ==
+ _ __ __ _
+ \\/ / \ \//_
+ \ \ / __| __
+ \ \___/ /_____/ /
+ | _______ \
+ \ ( ) / \_\
+ \ /
+ | |
+ | |
+ ____+-_=+-^ ^+-=_=__________
+
+^^ I drew that :D
+
+ **************************************/
+
+GitVisuals.prototype.animateNodePositions = function(speed) {
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.animateUpdatedPosition(speed);
+ }, this);
+};
+
+GitVisuals.prototype.turnOnPaper = function() {
+ this.gitReady = false;
+};
+
+// does making an accessor method make it any less hacky? that is the true question
+GitVisuals.prototype.turnOffPaper = function() {
+ this.gitReady = true;
+};
+
+GitVisuals.prototype.addBranchFromEvent = function(branch, collection, index) {
+ var action = _.bind(function() {
+ this.addBranch(branch);
+ }, this);
+
+ if (!this.gitEngine || !this.gitReady) {
+ this.defer(action);
+ } else {
+ action();
+ }
+};
+
+GitVisuals.prototype.addBranch = function(branch) {
+ var visBranch = new VisBranch({
+ branch: branch,
+ gitVisuals: this,
+ gitEngine: this.gitEngine
+ });
+
+ this.visBranchCollection.add(visBranch);
+ if (this.gitReady) {
+ visBranch.genGraphics(this.paper);
+ }
+};
+
+GitVisuals.prototype.removeVisBranch = function(visBranch) {
+ this.visBranchCollection.remove(visBranch);
+};
+
+GitVisuals.prototype.removeVisNode = function(visNode) {
+ this.visNodeMap[visNode.getID()] = undefined;
+};
+
+GitVisuals.prototype.removeVisEdge = function(visEdge) {
+ this.visEdgeCollection.remove(visEdge);
+};
+
+GitVisuals.prototype.animateRefs = function(speed) {
+ this.visBranchCollection.each(function(visBranch) {
+ visBranch.animateUpdatedPos(speed);
+ }, this);
+};
+
+GitVisuals.prototype.animateEdges = function(speed) {
+ this.visEdgeCollection.each(function(edge) {
+ edge.animateUpdatedPath(speed);
+ }, this);
+};
+
+GitVisuals.prototype.getDepthIncrement = function(maxDepth) {
+ // assume there are at least 7 layers until later
+ maxDepth = Math.max(maxDepth, 7);
+ var increment = 1.0 / maxDepth;
+ return increment;
+};
+
+GitVisuals.prototype.calcDepthRecursive = function(commit, depth) {
+ commit.get('visNode').setDepth(depth);
+
+ var children = commit.get('children');
+ var maxDepth = depth;
+ _.each(children, function(child) {
+ var d = this.calcDepthRecursive(child, depth + 1);
+ maxDepth = Math.max(d, maxDepth);
+ }, this);
+
+ return maxDepth;
+};
+
+GitVisuals.prototype.canvasResize = function(width, height) {
+ // refresh when we are ready
+ if (GLOBAL.isAnimating) {
+ events.trigger('processCommandFromEvent', 'refresh');
+ } else {
+ this.refreshTree();
+ }
+};
+
+GitVisuals.prototype.addCommit = function(commit) {
+ // TODO
+};
+
+GitVisuals.prototype.addNode = function(id, commit) {
+ this.commitMap[id] = commit;
+ if (commit.get('rootCommit')) {
+ this.rootCommit = commit;
+ }
+
+ var visNode = new VisNode({
+ id: id,
+ commit: commit,
+ gitVisuals: this,
+ gitEngine: this.gitEngine
+ });
+ this.visNodeMap[id] = visNode;
+
+ if (this.gitReady) {
+ visNode.genGraphics(this.paper);
+ }
+ return visNode;
+};
+
+GitVisuals.prototype.addEdge = function(idTail, idHead) {
+ var visNodeTail = this.visNodeMap[idTail];
+ var visNodeHead = this.visNodeMap[idHead];
+
+ if (!visNodeTail || !visNodeHead) {
+ throw new Error('one of the ids in (' + idTail +
+ ', ' + idHead + ') does not exist');
+ }
+
+ var edge = new VisEdge({
+ tail: visNodeTail,
+ head: visNodeHead,
+ gitVisuals: this,
+ gitEngine: this.gitEngine
+ });
+ this.visEdgeCollection.add(edge);
+
+ if (this.gitReady) {
+ edge.genGraphics(this.paper);
+ }
+};
+
+GitVisuals.prototype.collectionChanged = function() {
+ // TODO ?
+};
+
+GitVisuals.prototype.zIndexReflow = function() {
+ this.visNodesFront();
+ this.visBranchesFront();
+};
+
+GitVisuals.prototype.visNodesFront = function() {
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.toFront();
+ });
+};
+
+GitVisuals.prototype.visBranchesFront = function() {
+ this.visBranchCollection.each(function(vBranch) {
+ vBranch.nonTextToFront();
+ });
+
+ this.visBranchCollection.each(function(vBranch) {
+ vBranch.textToFront();
+ });
+};
+
+GitVisuals.prototype.drawTreeFromReload = function() {
+ this.gitReady = true;
+ // gen all the graphics we need
+ this.deferFlush();
+
+ this.calcTreeCoords();
+};
+
+GitVisuals.prototype.drawTreeFirstTime = function() {
+ this.gitReady = true;
+ this.calcTreeCoords();
+
+ _.each(this.visNodeMap, function(visNode) {
+ visNode.genGraphics(this.paper);
+ }, this);
+
+ this.visEdgeCollection.each(function(edge) {
+ edge.genGraphics(this.paper);
+ }, this);
+
+ this.visBranchCollection.each(function(visBranch) {
+ visBranch.genGraphics(this.paper);
+ }, this);
+
+ this.zIndexReflow();
+};
+
+
+/************************
+ * Random util functions, some from liquidGraph
+ ***********************/
+function blendHueStrings(hueStrings) {
+ // assumes a sat of 0.7 and brightness of 1
+
+ var x = 0;
+ var y = 0;
+ var totalSat = 0;
+ var totalBright = 0;
+ var length = hueStrings.length;
+
+ _.each(hueStrings, function(hueString) {
+ var exploded = hueString.split('(')[1];
+ exploded = exploded.split(')')[0];
+ exploded = exploded.split(',');
+
+ totalSat += parseFloat(exploded[1]);
+ totalBright += parseFloat(exploded[2]);
+ var hue = parseFloat(exploded[0]);
+
+ var angle = hue * Math.PI * 2;
+ x += Math.cos(angle);
+ y += Math.sin(angle);
+ });
+
+ x = x / length;
+ y = y / length;
+ totalSat = totalSat / length;
+ totalBright = totalBright / length;
+
+ var hue = Math.atan2(y, x) / (Math.PI * 2); // could fail on 0's
+ if (hue < 0) {
+ hue = hue + 1;
+ }
+ return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')';
+}
+
+function randomHueString() {
+ var hue = Math.random();
+ var str = 'hsb(' + String(hue) + ',0.7,1)';
+ return str;
+};
+
+exports.Visualization = Visualization;
+
+
+});
+require("/visuals.js");
})();
diff --git a/src/animationFactory.js b/src/animationFactory.js
index bc6156c1..e9d9bcdb 100644
--- a/src/animationFactory.js
+++ b/src/animationFactory.js
@@ -9,7 +9,7 @@
*/
// essentially a static class
-function AnimationFactory() {
+var AnimationFactory = function() {
}
@@ -251,4 +251,3 @@ AnimationFactory.prototype.genFromToSnapshotAnimation = function(
exports.AnimationFactory = AnimationFactory;
-
diff --git a/src/collections.js b/src/collections.js
index 4044115c..39007a55 100644
--- a/src/collections.js
+++ b/src/collections.js
@@ -1,3 +1,6 @@
+var Commit = require('./git').Commit;
+var Branch = require('./git').Branch;
+
var CommitCollection = Backbone.Collection.extend({
model: Commit
});
@@ -87,3 +90,9 @@ var CommandBuffer = Backbone.Model.extend({
},
});
+exports.CommitCollection = CommitCollection;
+exports.CommandCollection = CommandCollection;
+exports.BranchCollection = BranchCollection;
+exports.CommandEntryCollection = CommandEntryCollection;
+exports.CommandBuffer = CommandBuffer;
+
diff --git a/src/commandViews.js b/src/commandViews.js
index d3cddd28..69bd36e7 100644
--- a/src/commandViews.js
+++ b/src/commandViews.js
@@ -1,3 +1,5 @@
+var CommandEntryCollection = require('./collections').CommandEntryCollection;
+
var CommandPromptView = Backbone.View.extend({
initialize: function(options) {
this.collection = options.collection;
@@ -379,3 +381,7 @@ var CommandLineHistoryView = Backbone.View.extend({
this.collection.each(this.addOne);
}
});
+
+exports.CommandPromptView = CommandPromptView;
+exports.CommandLineHistoryView = CommandLineHistoryView;
+
diff --git a/src/git.js b/src/git.js
index f6059def..95cd4afc 100644
--- a/src/git.js
+++ b/src/git.js
@@ -1,3 +1,6 @@
+var animationFactory = new require('./animationFactory').AnimationFactory();
+console.log('this is what animatioinf actory is', require('./animationFactory'));
+
// backbone or something uses _.uniqueId, so we make our own here
var uniqueId = (function() {
var n = 0;
@@ -1624,3 +1627,8 @@ var Commit = Backbone.Model.extend({
}
});
+exports.GitEngine = GitEngine;
+exports.Commit = Commit;
+exports.Branch = Branch;
+exports.Ref = Ref;
+
diff --git a/src/index.html b/src/index.html
index fb67e28a..08de639d 100644
--- a/src/index.html
+++ b/src/index.html
@@ -135,15 +135,15 @@
-
+
-
+
-
-
+
+
diff --git a/src/main.js b/src/main.js
index 0f22ab9f..978d4867 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,4 +1,9 @@
var AnimationFactory = require('./animationFactory').AnimationFactory;
+var CommandCollection = require('./collections').CommandCollection;
+var CommandBuffer = require('./collections').CommandBuffer;
+var CommandPromptView = require('./commandViews').CommandPromptView;
+var CommandLineHistoryView = require('./commandViews').CommandLineHistoryView;
+var Visualization = require('./visuals').Visualization;
/**
* Globals
diff --git a/src/visuals.js b/src/visuals.js
index 64a1f12b..17dc2ddd 100644
--- a/src/visuals.js
+++ b/src/visuals.js
@@ -1,3 +1,7 @@
+var CommitCollection = require('./collections').CommitCollection;
+var BranchCollection = require('./collections').BranchCollection;
+var GitEngine = require('./git').GitEngine;
+
var Visualization = Backbone.View.extend({
initialize: function(options) {
var _this = this;
@@ -667,3 +671,5 @@ function randomHueString() {
return str;
};
+exports.Visualization = Visualization;
+