From 5dc7fc0bed3c5e453a19dbfae0effe22cdd997c4 Mon Sep 17 00:00:00 2001 From: Peter Cottle Date: Thu, 13 Dec 2012 17:34:26 -0800 Subject: [PATCH] now can do with whatever --- build/bundle.js | 7792 ++++++++++++++++++++++++----------------------- src/main.js | 35 +- 2 files changed, 3934 insertions(+), 3893 deletions(-) diff --git a/build/bundle.js b/build/bundle.js index 28287510..ad3fd638 100644 --- a/build/bundle.js +++ b/build/bundle.js @@ -389,265 +389,6 @@ process.binding = function (name) { }; })(); -}); - -require.define("/animationFactory.js",function(require,module,exports,__dirname,__filename,process,global){/****************** - * This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time. - * The tricky thing is that when a new commit has to be "born," say in the middle of a rebase - * or something, it must animate out from the parent position to it's birth position. - - * These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree, - * store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots. - * and then essentially animate the entire tree too. - */ - -var Animation = require('./async').Animation; -var GRAPHICS = require('./constants').GRAPHICS; - -// essentially a static class -var AnimationFactory = function() { - -} - -AnimationFactory.prototype.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) { - if (!animationQueue) { - throw new Error("Need animation queue to add closure to!"); - } - - var time = GRAPHICS.defaultAnimationTime * 1.0; - var bounceTime = time * 2; - - // essentially refresh the entire tree, but do a special thing for the commit - var visNode = commit.get('visNode'); - - var animation = function() { - // this takes care of refs and all that jazz, and updates all the positions - gitVisuals.refreshTree(time); - - visNode.setBirth(); - visNode.parentInFront(); - gitVisuals.visBranchesFront(); - - visNode.animateUpdatedPosition(bounceTime, 'bounce'); - visNode.animateOutgoingEdges(time); - }; - - animationQueue.add(new Animation({ - closure: animation, - duration: Math.max(time, bounceTime) - })); -}; - -AnimationFactory.prototype.overrideOpacityDepth2 = function(attr, opacity) { - opacity = (opacity === undefined) ? 1 : opacity; - - var newAttr = {}; - - _.each(attr, function(partObj, partName) { - newAttr[partName] = {}; - _.each(partObj, function(val, key) { - if (key == 'opacity') { - newAttr[partName][key] = opacity; - } else { - newAttr[partName][key] = val; - } - }); - }); - return newAttr; -}; - -AnimationFactory.prototype.overrideOpacityDepth3 = function(snapShot, opacity) { - var newSnap = {}; - - _.each(snapShot, function(visObj, visID) { - newSnap[visID] = this.overrideOpacityDepth2(visObj, opacity); - }, this); - return newSnap; -}; - -AnimationFactory.prototype.genCommitBirthClosureFromSnapshot = function(step, gitVisuals) { - var time = GRAPHICS.defaultAnimationTime * 1.0; - var bounceTime = time * 1.5; - - var visNode = step.newCommit.get('visNode'); - var afterAttrWithOpacity = this.overrideOpacityDepth2(step.afterSnapshot[visNode.getID()]); - var afterSnapWithOpacity = this.overrideOpacityDepth3(step.afterSnapshot); - - var animation = function() { - visNode.setBirthFromSnapshot(step.beforeSnapshot); - visNode.parentInFront(); - gitVisuals.visBranchesFront(); - - visNode.animateToAttr(afterAttrWithOpacity, bounceTime, 'bounce'); - visNode.animateOutgoingEdgesToAttr(afterSnapWithOpacity, bounceTime); - }; - - return animation; -}; - -AnimationFactory.prototype.refreshTree = function(animationQueue, gitVisuals) { - animationQueue.add(new Animation({ - closure: function() { - gitVisuals.refreshTree(); - } - })); -}; - -AnimationFactory.prototype.rebaseAnimation = function(animationQueue, rebaseResponse, - gitEngine, gitVisuals) { - - this.rebaseHighlightPart(animationQueue, rebaseResponse, gitEngine); - this.rebaseBirthPart(animationQueue, rebaseResponse, gitEngine, gitVisuals); -}; - -AnimationFactory.prototype.rebaseHighlightPart = function(animationQueue, rebaseResponse, gitEngine) { - var fullTime = GRAPHICS.defaultAnimationTime * 0.66; - var slowTime = fullTime * 2.0; - - // we want to highlight all the old commits - var oldCommits = rebaseResponse.toRebaseArray; - // we are either highlighting to a visBranch or a visNode - var visBranch = rebaseResponse.destinationBranch.get('visBranch'); - if (!visBranch) { - // in the case where we rebase onto a commit - visBranch = rebaseResponse.destinationBranch.get('visNode'); - } - - _.each(oldCommits, function(oldCommit) { - var visNode = oldCommit.get('visNode'); - animationQueue.add(new Animation({ - closure: function() { - visNode.highlightTo(visBranch, slowTime, 'easeInOut'); - }, - duration: fullTime * 1.5 - })); - - }, this); - - this.delay(animationQueue, fullTime * 2); -}; - -AnimationFactory.prototype.rebaseBirthPart = function(animationQueue, rebaseResponse, - gitEngine, gitVisuals) { - var rebaseSteps = rebaseResponse.rebaseSteps; - - var newVisNodes = []; - _.each(rebaseSteps, function(step) { - var visNode = step.newCommit.get('visNode'); - - newVisNodes.push(visNode); - visNode.setOpacity(0); - visNode.setOutgoingEdgesOpacity(0); - }, this); - - var previousVisNodes = []; - _.each(rebaseSteps, function(rebaseStep, index) { - var toOmit = newVisNodes.slice(index + 1); - - var snapshotPart = this.genFromToSnapshotAnimation( - rebaseStep.beforeSnapshot, - rebaseStep.afterSnapshot, - toOmit, - previousVisNodes, - gitVisuals - ); - var birthPart = this.genCommitBirthClosureFromSnapshot(rebaseStep, gitVisuals); - - var animation = function() { - snapshotPart(); - birthPart(); - }; - - animationQueue.add(new Animation({ - closure: animation, - duration: GRAPHICS.defaultAnimationTime * 1.5 - })); - - previousVisNodes.push(rebaseStep.newCommit.get('visNode')); - }, this); - - // need to delay to let bouncing finish - this.delay(animationQueue); - - this.refreshTree(animationQueue, gitVisuals); -}; - -AnimationFactory.prototype.delay = function(animationQueue, time) { - time = time || GRAPHICS.defaultAnimationTime; - animationQueue.add(new Animation({ - closure: function() { }, - duration: time - })); -}; - -AnimationFactory.prototype.genSetAllCommitOpacities = function(visNodes, opacity) { - // need to slice for closure - var nodesToAnimate = visNodes.slice(0); - - return function() { - _.each(nodesToAnimate, function(visNode) { - visNode.setOpacity(opacity); - visNode.setOutgoingEdgesOpacity(opacity); - }); - }; -}; - -AnimationFactory.prototype.stripObjectsFromSnapshot = function(snapShot, toOmit) { - var ids = []; - _.each(toOmit, function(obj) { - ids.push(obj.getID()); - }); - - var newSnapshot = {}; - _.each(snapShot, function(val, key) { - if (_.include(ids, key)) { - // omit - return; - } - newSnapshot[key] = val; - }, this); - return newSnapshot; -}; - -AnimationFactory.prototype.genFromToSnapshotAnimation = function( - beforeSnapshot, - afterSnapshot, - commitsToOmit, - commitsToFixOpacity, - gitVisuals) { - - // we want to omit the commit outgoing edges - var toOmit = []; - _.each(commitsToOmit, function(visNode) { - toOmit.push(visNode); - toOmit = toOmit.concat(visNode.get('outgoingEdges')); - }); - - var fixOpacity = function(obj) { - if (!obj) { return; } - _.each(obj, function(attr, partName) { - obj[partName].opacity = 1; - }); - }; - - // HORRIBLE loop to fix opacities all throughout the snapshot - _.each([beforeSnapshot, afterSnapshot], function(snapShot) { - _.each(commitsToFixOpacity, function(visNode) { - fixOpacity(snapShot[visNode.getID()]); - _.each(visNode.get('outgoingEdges'), function(visEdge) { - fixOpacity(snapShot[visEdge.getID()]); - }); - }); - }); - - return function() { - gitVisuals.animateAllFromAttrToAttr(beforeSnapshot, afterSnapshot, toOmit); - }; -}; - -exports.AnimationFactory = AnimationFactory; - - }); require.define("/async.js",function(require,module,exports,__dirname,__filename,process,global){var GLOBAL = require('./constants').GLOBAL; @@ -785,175 +526,6 @@ exports.TIME = TIME; exports.GRAPHICS = GRAPHICS; -}); - -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 - */ -var events = _.clone(Backbone.Events); -var ui = null; -var animationFactory = null; - -/** - * Static Classes - */ -animationFactory = new AnimationFactory(); - -/////////////////////////////////////////////////////////////////////// - -$(document).ready(function(){ - ui = new UI(); - mainVis = new Visualization({ - el: $('#canvasWrapper')[0] - }); - - if (/\?demo/.test(window.location.href)) { - setTimeout(function() { - events.trigger('submitCommandValueFromEvent', "gc; git checkout HEAD~1; git commit; git checkout -b bugFix; gc; gc; git rebase master; git checkout master; gc; gc; git merge bugFix"); - }, 500); - } -}); - -function UI() { - // static classes - this.commandCollection = new CommandCollection(); - - this.commandBuffer = new CommandBuffer({ - collection: this.commandCollection - }); - - this.commandPromptView = new CommandPromptView({ - el: $('#commandLineBar'), - collection: this.commandCollection - }); - this.commandLineHistoryView = new CommandLineHistoryView({ - el: $('#commandLineHistory'), - collection: this.commandCollection - }); - - $('#commandTextField').focus(); -} - -exports.getEvents = function() { - return events; -}; -exports.ui = ui; -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 Main = require('./main'); -var Command = require('./commandModel').Command; -var CommandEntry = require('./commandModel').CommandEntry; -var TIME = require('./constants').TIME; - -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) { - require('./main').getEvents().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. - Main.getEvents().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 AnimationFactoryModule = require('./animationFactory'); @@ -2600,202 +2172,1105 @@ exports.Ref = Ref; }); -require.define("/miscViews.js",function(require,module,exports,__dirname,__filename,process,global){var InteractiveRebaseView = Backbone.View.extend({ - tagName: 'div', - template: _.template($('#interactive-rebase-template').html()), +require.define("/animationFactory.js",function(require,module,exports,__dirname,__filename,process,global){/****************** + * This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time. + * The tricky thing is that when a new commit has to be "born," say in the middle of a rebase + * or something, it must animate out from the parent position to it's birth position. - events: { - 'click #confirmButton': 'confirmed' - }, + * These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree, + * store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots. + * and then essentially animate the entire tree too. + */ - initialize: function(options) { - this.hasClicked = false; - this.rebaseCallback = options.callback; +var Animation = require('./async').Animation; +var GRAPHICS = require('./constants').GRAPHICS; - this.rebaseArray = options.toRebase; +// essentially a static class +var AnimationFactory = function() { - this.rebaseEntries = new RebaseEntryCollection(); - this.rebaseMap = {}; - this.entryObjMap = {}; +} - this.rebaseArray.reverse(); - // make basic models for each commit - _.each(this.rebaseArray, function(commit) { - var id = commit.get('id'); - this.rebaseMap[id] = commit; - this.entryObjMap[id] = new RebaseEntry({ - id: id - }); - this.rebaseEntries.add(this.entryObjMap[id]); - }, this); +AnimationFactory.prototype.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) { + if (!animationQueue) { + throw new Error("Need animation queue to add closure to!"); + } - this.render(); + var time = GRAPHICS.defaultAnimationTime * 1.0; + var bounceTime = time * 2; - // show the dialog holder - this.show(); - }, + // essentially refresh the entire tree, but do a special thing for the commit + var visNode = commit.get('visNode'); - show: function() { - this.toggleVisibility(true); - }, + var animation = function() { + // this takes care of refs and all that jazz, and updates all the positions + gitVisuals.refreshTree(time); - hide: function() { - this.toggleVisibility(false); - }, + visNode.setBirth(); + visNode.parentInFront(); + gitVisuals.visBranchesFront(); - toggleVisibility: function(toggle) { - console.log('toggling'); - $('#dialogHolder').toggleClass('shown', toggle); - }, + visNode.animateUpdatedPosition(bounceTime, 'bounce'); + visNode.animateOutgoingEdges(time); + }; - confirmed: function() { - // we hide the dialog anyways, but they might be fast clickers - if (this.hasClicked) { + animationQueue.add(new Animation({ + closure: animation, + duration: Math.max(time, bounceTime) + })); +}; + +AnimationFactory.prototype.overrideOpacityDepth2 = function(attr, opacity) { + opacity = (opacity === undefined) ? 1 : opacity; + + var newAttr = {}; + + _.each(attr, function(partObj, partName) { + newAttr[partName] = {}; + _.each(partObj, function(val, key) { + if (key == 'opacity') { + newAttr[partName][key] = opacity; + } else { + newAttr[partName][key] = val; + } + }); + }); + return newAttr; +}; + +AnimationFactory.prototype.overrideOpacityDepth3 = function(snapShot, opacity) { + var newSnap = {}; + + _.each(snapShot, function(visObj, visID) { + newSnap[visID] = this.overrideOpacityDepth2(visObj, opacity); + }, this); + return newSnap; +}; + +AnimationFactory.prototype.genCommitBirthClosureFromSnapshot = function(step, gitVisuals) { + var time = GRAPHICS.defaultAnimationTime * 1.0; + var bounceTime = time * 1.5; + + var visNode = step.newCommit.get('visNode'); + var afterAttrWithOpacity = this.overrideOpacityDepth2(step.afterSnapshot[visNode.getID()]); + var afterSnapWithOpacity = this.overrideOpacityDepth3(step.afterSnapshot); + + var animation = function() { + visNode.setBirthFromSnapshot(step.beforeSnapshot); + visNode.parentInFront(); + gitVisuals.visBranchesFront(); + + visNode.animateToAttr(afterAttrWithOpacity, bounceTime, 'bounce'); + visNode.animateOutgoingEdgesToAttr(afterSnapWithOpacity, bounceTime); + }; + + return animation; +}; + +AnimationFactory.prototype.refreshTree = function(animationQueue, gitVisuals) { + animationQueue.add(new Animation({ + closure: function() { + gitVisuals.refreshTree(); + } + })); +}; + +AnimationFactory.prototype.rebaseAnimation = function(animationQueue, rebaseResponse, + gitEngine, gitVisuals) { + + this.rebaseHighlightPart(animationQueue, rebaseResponse, gitEngine); + this.rebaseBirthPart(animationQueue, rebaseResponse, gitEngine, gitVisuals); +}; + +AnimationFactory.prototype.rebaseHighlightPart = function(animationQueue, rebaseResponse, gitEngine) { + var fullTime = GRAPHICS.defaultAnimationTime * 0.66; + var slowTime = fullTime * 2.0; + + // we want to highlight all the old commits + var oldCommits = rebaseResponse.toRebaseArray; + // we are either highlighting to a visBranch or a visNode + var visBranch = rebaseResponse.destinationBranch.get('visBranch'); + if (!visBranch) { + // in the case where we rebase onto a commit + visBranch = rebaseResponse.destinationBranch.get('visNode'); + } + + _.each(oldCommits, function(oldCommit) { + var visNode = oldCommit.get('visNode'); + animationQueue.add(new Animation({ + closure: function() { + visNode.highlightTo(visBranch, slowTime, 'easeInOut'); + }, + duration: fullTime * 1.5 + })); + + }, this); + + this.delay(animationQueue, fullTime * 2); +}; + +AnimationFactory.prototype.rebaseBirthPart = function(animationQueue, rebaseResponse, + gitEngine, gitVisuals) { + var rebaseSteps = rebaseResponse.rebaseSteps; + + var newVisNodes = []; + _.each(rebaseSteps, function(step) { + var visNode = step.newCommit.get('visNode'); + + newVisNodes.push(visNode); + visNode.setOpacity(0); + visNode.setOutgoingEdgesOpacity(0); + }, this); + + var previousVisNodes = []; + _.each(rebaseSteps, function(rebaseStep, index) { + var toOmit = newVisNodes.slice(index + 1); + + var snapshotPart = this.genFromToSnapshotAnimation( + rebaseStep.beforeSnapshot, + rebaseStep.afterSnapshot, + toOmit, + previousVisNodes, + gitVisuals + ); + var birthPart = this.genCommitBirthClosureFromSnapshot(rebaseStep, gitVisuals); + + var animation = function() { + snapshotPart(); + birthPart(); + }; + + animationQueue.add(new Animation({ + closure: animation, + duration: GRAPHICS.defaultAnimationTime * 1.5 + })); + + previousVisNodes.push(rebaseStep.newCommit.get('visNode')); + }, this); + + // need to delay to let bouncing finish + this.delay(animationQueue); + + this.refreshTree(animationQueue, gitVisuals); +}; + +AnimationFactory.prototype.delay = function(animationQueue, time) { + time = time || GRAPHICS.defaultAnimationTime; + animationQueue.add(new Animation({ + closure: function() { }, + duration: time + })); +}; + +AnimationFactory.prototype.genSetAllCommitOpacities = function(visNodes, opacity) { + // need to slice for closure + var nodesToAnimate = visNodes.slice(0); + + return function() { + _.each(nodesToAnimate, function(visNode) { + visNode.setOpacity(opacity); + visNode.setOutgoingEdgesOpacity(opacity); + }); + }; +}; + +AnimationFactory.prototype.stripObjectsFromSnapshot = function(snapShot, toOmit) { + var ids = []; + _.each(toOmit, function(obj) { + ids.push(obj.getID()); + }); + + var newSnapshot = {}; + _.each(snapShot, function(val, key) { + if (_.include(ids, key)) { + // omit return; } - this.hasClicked = true; + newSnapshot[key] = val; + }, this); + return newSnapshot; +}; - // first of all hide - this.$el.css('display', 'none'); +AnimationFactory.prototype.genFromToSnapshotAnimation = function( + beforeSnapshot, + afterSnapshot, + commitsToOmit, + commitsToFixOpacity, + gitVisuals) { - // get our ordering - var uiOrder = []; - this.$('ul#rebaseEntries li').each(function(i, obj) { - uiOrder.push(obj.id); + // we want to omit the commit outgoing edges + var toOmit = []; + _.each(commitsToOmit, function(visNode) { + toOmit.push(visNode); + toOmit = toOmit.concat(visNode.get('outgoingEdges')); + }); + + var fixOpacity = function(obj) { + if (!obj) { return; } + _.each(obj, function(attr, partName) { + obj[partName].opacity = 1; }); + }; - // now get the real array - var toRebase = []; - _.each(uiOrder, function(id) { - // the model - if (this.entryObjMap[id].get('pick')) { - toRebase.unshift(this.rebaseMap[id]); - } - }, this); - - this.rebaseCallback(toRebase); - - this.$el.html(''); - // kill ourselves? - delete this; - }, - - render: function() { - var json = { - num: this.rebaseArray.length - }; - - this.$el.html(this.template(json)); - - // also render each entry - var listHolder = this.$('ul#rebaseEntries'); - this.rebaseEntries.each(function(entry) { - new RebaseEntryView({ - el: listHolder, - model: entry + // HORRIBLE loop to fix opacities all throughout the snapshot + _.each([beforeSnapshot, afterSnapshot], function(snapShot) { + _.each(commitsToFixOpacity, function(visNode) { + fixOpacity(snapShot[visNode.getID()]); + _.each(visNode.get('outgoingEdges'), function(visEdge) { + fixOpacity(snapShot[visEdge.getID()]); }); - }, this); - - // then make it reorderable.. - listHolder.sortable({ - distance: 5, - placeholder: 'ui-state-highlight' }); - }, + }); + + return function() { + gitVisuals.animateAllFromAttrToAttr(beforeSnapshot, afterSnapshot, toOmit); + }; +}; + +exports.AnimationFactory = AnimationFactory; + }); -var RebaseEntry = Backbone.Model.extend({ - defaults: { - pick: true - }, +require.define("/main.js",function(require,module,exports,__dirname,__filename,process,global){/** + * Globals + */ +var events = _.clone(Backbone.Events); +var ui = null; +var mainVis = null; - toggle: function() { - this.set('pick', !this.get('pick')); +/////////////////////////////////////////////////////////////////////// + +$(document).ready(function(){ + var Visuals = require('./visuals'); + + ui = new UI(); + mainVis = new Visuals.Visualization({ + el: $('#canvasWrapper')[0] + }); + + if (/\?demo/.test(window.location.href)) { + setTimeout(function() { + events.trigger('submitCommandValueFromEvent', "gc; git checkout HEAD~1; git commit; git checkout -b bugFix; gc; gc; git rebase master; git checkout master; gc; gc; git merge bugFix"); + }, 500); } }); -var RebaseEntryCollection = Backbone.Collection.extend({ - model: RebaseEntry +function UI() { + var Collections = require('./collections'); + var CommandViews = require('./commandViews'); + + this.commandCollection = new Collections.CommandCollection(); + + this.commandBuffer = new Collections.CommandBuffer({ + collection: this.commandCollection + }); + + this.commandPromptView = new CommandViews.CommandPromptView({ + el: $('#commandLineBar'), + collection: this.commandCollection + }); + this.commandLineHistoryView = new CommandViews.CommandLineHistoryView({ + el: $('#commandLineHistory'), + collection: this.commandCollection + }); + + $('#commandTextField').focus(); +} + +exports.getEvents = function() { + return events; +}; +exports.getUI = function() { + return ui; +}; + + }); -var RebaseEntryView = Backbone.View.extend({ - tagName: 'li', - template: _.template($('#interactive-rebase-entry-template').html()), +require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); +var GRAPHICS = require('./constants').GRAPHICS; +var GLOBAL = require('./constants').GLOBAL; - toggle: function() { - this.model.toggle(); - - // toggle a class also - this.listEntry.toggleClass('notPicked', !this.model.get('pick')); +var Collections = require('./collections'); +var CommitCollection = Collections.CommitCollection; +var BranchCollection = Collections.BranchCollection; + +var Tree = require('./tree'); +var VisEdgeCollection = Tree.VisEdgeCollection; +var VisBranchCollection = Tree.VisBranchCollection; +var VisNode = Tree.VisNode; +var VisBranch = Tree.VisBranch; +var VisEdge = Tree.VisEdge; + +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 + }); + + var GitEngine = require('./git').GitEngine; + 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 = []; + + Main.getEvents().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; +}; + +// we debounce here so we aren't firing a resize call on every resize event +// but only after they stop +GitVisuals.prototype.canvasResize = _.debounce(function(width, height) { + // refresh when we are ready + if (GLOBAL.isAnimating) { + Main.getEvents().trigger('processCommandFromEvent', 'refresh'); + } else { + this.refreshTree(); + } +}, 200); + +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) + ')'; +} + +exports.Visualization = Visualization; + + +}); + +require.define("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit; +var Branch = require('./git').Branch; + +var Main = require('./main'); +var Command = require('./commandModel').Command; +var CommandEntry = require('./commandModel').CommandEntry; +var TIME = require('./constants').TIME; + +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) { - this.render(); + require('./main').getEvents().on('gitCommandReady', _.bind( + this.addCommand, this + )); + + options.collection.bind('add', this.addCommand, this); + + this.buffer = []; + this.timeout = null; }, - render: function() { - var json = this.model.toJSON(); - this.$el.append(this.template(this.model.toJSON())); - - // hacky :( who would have known jquery barfs on ids with %'s and quotes - this.listEntry = this.$el.children(':last'); - - this.listEntry.delegate('#toggleButton', 'click', _.bind(function() { - this.toggle(); - }, this)); - } -}); - -exports.InteractiveRebaseView = InteractiveRebaseView; - - -}); - -require.define("/errors.js",function(require,module,exports,__dirname,__filename,process,global){var MyError = Backbone.Model.extend({ - defaults: { - type: 'MyError', - msg: 'Unknown Error' - }, - toString: function() { - return this.get('type') + ': ' + this.get('msg'); + addCommand: function(command) { + this.buffer.push(command); + this.touchBuffer(); }, - getMsg: function() { - return this.get('msg') || 'Unknown Error'; - }, - - toResult: function() { - if (!this.get('msg').length) { - return ''; + 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; } - return '

' + this.get('msg').replace(/\n/g, '

') + '

'; - } + 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. + Main.getEvents().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(); + }, }); -var CommandProcessError = exports.CommandProcessError = MyError.extend({ - defaults: { - type: 'Command Process Error' - } -}); - -var CommandResult = exports.CommandResult = MyError.extend({ - defaults: { - type: 'Command Result' - } -}); - -var Warning = exports.Warning = MyError.extend({ - defaults: { - type: 'Warning' - } -}); - -var GitError = exports.GitError = MyError.extend({ - defaults: { - type: 'Git Error' - } -}); +exports.CommitCollection = CommitCollection; +exports.CommandCollection = CommandCollection; +exports.BranchCollection = BranchCollection; +exports.CommandEntryCollection = CommandEntryCollection; +exports.CommandBuffer = CommandBuffer; }); @@ -3171,490 +3646,50 @@ exports.Command = Command; }); -require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection; -var Main = require('./main'); -var Command = require('./commandModel').Command; -var CommandEntry = require('./commandModel').CommandEntry; - -var Errors = require('./errors'); -var Warning = Errors.Warning; - -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)); - - Main.getEvents().on('processCommandFromEvent', this.addToCollection, this); - Main.getEvents().on('submitCommandValueFromEvent', this.submitValue, this); - Main.getEvents().on('rollupCommands', this.rollupCommands, this); - - // hacky timeout focus - setTimeout(_.bind(function() { - this.focus(); - }, this), 100); +require.define("/errors.js",function(require,module,exports,__dirname,__filename,process,global){var MyError = Backbone.Model.extend({ + defaults: { + type: 'MyError', + msg: 'Unknown Error' + }, + toString: function() { + return this.get('type') + ': ' + this.get('msg'); }, - events: { - 'keydown #commandTextField': 'onKey', - 'keyup #commandTextField': 'onKeyUp', - 'blur #commandTextField': 'hideCursor', - 'focus #commandTextField': 'showCursor' + getMsg: function() { + return this.get('msg') || 'Unknown Error'; }, - 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); + toResult: function() { + if (!this.get('msg').length) { + return ''; } - }, - - 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); + return '

' + this.get('msg').replace(/\n/g, '

') + '

'; } }); - -// 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 CommandProcessError = exports.CommandProcessError = MyError.extend({ + defaults: { + type: 'Command Process Error' } }); - -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); - - Main.getEvents().on('issueWarning', this.addWarning, this); - Main.getEvents().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); +var CommandResult = exports.CommandResult = MyError.extend({ + defaults: { + type: 'Command Result' } }); -exports.CommandPromptView = CommandPromptView; -exports.CommandLineHistoryView = CommandLineHistoryView; - - +var Warning = exports.Warning = MyError.extend({ + defaults: { + type: 'Warning' + } }); -require.define("/levels.js",function(require,module,exports,__dirname,__filename,process,global){// static class... -function LevelEngine() { - -} - -LevelEngine.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) { - var result = true; - _.each(branches, function(branchName) { - result = result && this.compareBranchWithinTrees(treeA, treeB, branchName); - }, this); - - return result; -}; - -LevelEngine.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) { - treeA = this.convertTreeSafe(treeA); - treeB = this.convertTreeSafe(treeB); - - this.stripTreeFields([treeA, treeB]); - - // we need a recursive comparison function to bubble up the branch - var recurseCompare = function(commitA, commitB) { - // this is the short-circuit base case - var result = _.isEqual(commitA, commitB); - if (!result) { - return false; - } - - // we loop through each parent ID. we sort the parent ID's beforehand - // so the index lookup is valid - _.each(commitA.parents, function(pAid, index) { - var pBid = commitB.parents[index]; - - var childA = treeA.commits[pAid]; - var childB = treeB.commits[pBid]; - - result = result && recurseCompare(childA, childB); - }, this); - // if each of our children recursively are equal, we are good - return result; - }; - - var branchA = treeA.branches[branchName]; - var branchB = treeB.branches[branchName]; - - return _.isEqual(branchA, branchB) && - recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); -}; - -LevelEngine.prototype.convertTreeSafe = function(tree) { - if (typeof tree == 'string') { - return JSON.parse(unescape(tree)); +var GitError = exports.GitError = MyError.extend({ + defaults: { + type: 'Git Error' } - return tree; -}; - -LevelEngine.prototype.stripTreeFields = function(trees) { - var stripFields = ['createTime', 'author', 'commitMessage']; - var sortFields = ['children', 'parents']; - - _.each(trees, function(tree) { - _.each(tree.commits, function(commit) { - _.each(stripFields, function(field) { - commit[field] = undefined; - }); - _.each(sortFields, function(field) { - if (commit[field]) { - commit[field] = commit[field].sort(); - } - }); - }); - }); -}; - -LevelEngine.prototype.compareTrees = function(treeA, treeB) { - treeA = this.convertTreeSafe(treeA); - treeB = this.convertTreeSafe(treeB); - - // now we need to strip out the fields we don't care about, aka things - // like createTime, message, author - this.stripTreeFields([treeA, treeB]); - - return _.isEqual(treeA, treeB); -}; - -var levelEngine = new LevelEngine(); - -exports.LevelEngine = LevelEngine; +}); }); @@ -4629,688 +4664,1993 @@ exports.VisBranch = VisBranch; }); -require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); -var GRAPHICS = require('./constants').GRAPHICS; -var GLOBAL = require('./constants').GLOBAL; +require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection; +var Main = require('./main'); +var Command = require('./commandModel').Command; +var CommandEntry = require('./commandModel').CommandEntry; -var Collections = require('./collections'); -var CommitCollection = Collections.CommitCollection; -var BranchCollection = Collections.BranchCollection; +var Errors = require('./errors'); +var Warning = Errors.Warning; -var Tree = require('./tree'); -var VisEdgeCollection = Tree.VisEdgeCollection; -var VisBranchCollection = Tree.VisBranchCollection; -var VisNode = Tree.VisNode; -var VisBranch = Tree.VisBranch; -var VisEdge = Tree.VisEdge; - -var Visualization = Backbone.View.extend({ +var CommandPromptView = Backbone.View.extend({ initialize: function(options) { - var _this = this; - Raphael(10, 10, 200, 200, function() { + this.collection = options.collection; - // for some reason raphael calls this function with a predefined - // context... - // so switch it - _this.paperInitialize(this); + // 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)); + + Main.getEvents().on('processCommandFromEvent', this.addToCollection, this); + Main.getEvents().on('submitCommandValueFromEvent', this.submitValue, this); + Main.getEvents().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; + } - this.commitCollection = new CommitCollection(); - this.branchCollection = new BranchCollection(); - - this.gitVisuals = new GitVisuals({ - commitCollection: this.commitCollection, - branchCollection: this.branchCollection, - paper: this.paper - }); - - var GitEngine = require('./git').GitEngine; - 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(); + // yay! we actually can display something + var commandEntry = this.commands.toArray()[this.index].get('text'); + this.setTextField(commandEntry); }, - myResize: function() { - var smaller = 1; - var el = this.el; + clearLocalStorage: function() { + this.commands.each(function(c) { + Backbone.sync('delete', c, function() { }); + }, this); + localStorage.setItem('CommandEntries', ''); + }, - var left = el.offsetLeft; - var top = el.offsetTop; - var width = el.clientWidth - smaller; - var height = el.clientHeight - smaller; + setTextField: function(value) { + this.$('#commandTextField').val(value); + }, - $(this.paper.canvas).css({ - left: left + 'px', - top: top + 'px' - }); - this.paper.setSize(width, height); - this.gitVisuals.canvasResize(width, height); - } + clear: function() { + this.setTextField(''); + }, -}); + submit: function() { + var value = this.$('#commandTextField').val().replace('\n', ''); + this.clear(); + this.submitValue(value); + }, -function GitVisuals(options) { - this.commitCollection = options.commitCollection; - this.branchCollection = options.branchCollection; - this.visNodeMap = {}; + rollupCommands: function(numBack) { + var which = this.commands.toArray().slice(1, Number(numBack) + 1); + which.reverse(); - this.visEdgeCollection = new VisEdgeCollection(); - this.visBranchCollection = new VisBranchCollection(); - this.commitMap = {}; + var str = ''; + _.each(which, function(commandEntry) { + str += commandEntry.get('text') + ';'; + }, this); - this.rootCommit = null; - this.branchStackMap = null; - this.upstreamBranchSet = null; - this.upstreamHeadSet = null; + console.log('the str', str); - this.paper = options.paper; - this.gitReady = false; + var rolled = new CommandEntry({text: str}); + this.commands.unshift(rolled); + Backbone.sync('create', rolled, function() { }); + }, - this.branchCollection.on('add', this.addBranchFromEvent, this); - this.branchCollection.on('remove', this.removeBranch, this); - this.deferred = []; - - Main.getEvents().on('refreshTree', _.bind( - this.refreshTree, this - )); -} + 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)); -GitVisuals.prototype.defer = function(action) { - this.deferred.push(action); -}; + if (shouldAdd) { + var commandEntry = new CommandEntry({text: value}); + this.commands.unshift(commandEntry); -GitVisuals.prototype.deferFlush = function() { - _.each(this.deferred, function(action) { - action(); - }, this); - this.deferred = []; -}; + // store to local storage + Backbone.sync('create', commandEntry, function() { }); -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; + // if our length is too egregious, reset + if (this.commands.length > 100) { + this.clearLocalStorage(); } - return aId.localeCompare(bId); + } + 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.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; + this.collection.add(command); } +}); - // 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 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(); } - }, this); + }, - var prevBound = min; + updateStatus: function() { + var statuses = ['inqueue', 'processing', 'finished']; + var toggleMap = {}; + _.each(statuses, function(status) { + toggleMap[status] = false; + }); + toggleMap[this.model.get('status')] = true; - // 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)) { + 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); + + Main.getEvents().on('issueWarning', this.addWarning, this); + Main.getEvents().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; + } + }, - 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); -}; + addOne: function(command) { + var view = new CommandView({ + model: command + }); + this.$('#commandDisplay').append(view.render().el); + this.scrollDown(); + }, -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'); + addAll: function() { + this.collection.each(this.addOne); } +}); - 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; -}; - -// we debounce here so we aren't firing a resize call on every resize event -// but only after they stop -GitVisuals.prototype.canvasResize = _.debounce(function(width, height) { - // refresh when we are ready - if (GLOBAL.isAnimating) { - Main.getEvents().trigger('processCommandFromEvent', 'refresh'); - } else { - this.refreshTree(); - } -}, 200); - -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) + ')'; -} - -exports.Visualization = Visualization; +exports.CommandPromptView = CommandPromptView; +exports.CommandLineHistoryView = CommandLineHistoryView; }); +require.define("/miscViews.js",function(require,module,exports,__dirname,__filename,process,global){var InteractiveRebaseView = Backbone.View.extend({ + tagName: 'div', + template: _.template($('#interactive-rebase-template').html()), + + events: { + 'click #confirmButton': 'confirmed' + }, + + initialize: function(options) { + this.hasClicked = false; + this.rebaseCallback = options.callback; + + this.rebaseArray = options.toRebase; + + this.rebaseEntries = new RebaseEntryCollection(); + this.rebaseMap = {}; + this.entryObjMap = {}; + + this.rebaseArray.reverse(); + // make basic models for each commit + _.each(this.rebaseArray, function(commit) { + var id = commit.get('id'); + this.rebaseMap[id] = commit; + this.entryObjMap[id] = new RebaseEntry({ + id: id + }); + this.rebaseEntries.add(this.entryObjMap[id]); + }, this); + + this.render(); + + // show the dialog holder + this.show(); + }, + + show: function() { + this.toggleVisibility(true); + }, + + hide: function() { + this.toggleVisibility(false); + }, + + toggleVisibility: function(toggle) { + console.log('toggling'); + $('#dialogHolder').toggleClass('shown', toggle); + }, + + confirmed: function() { + // we hide the dialog anyways, but they might be fast clickers + if (this.hasClicked) { + return; + } + this.hasClicked = true; + + // first of all hide + this.$el.css('display', 'none'); + + // get our ordering + var uiOrder = []; + this.$('ul#rebaseEntries li').each(function(i, obj) { + uiOrder.push(obj.id); + }); + + // now get the real array + var toRebase = []; + _.each(uiOrder, function(id) { + // the model + if (this.entryObjMap[id].get('pick')) { + toRebase.unshift(this.rebaseMap[id]); + } + }, this); + + this.rebaseCallback(toRebase); + + this.$el.html(''); + // kill ourselves? + delete this; + }, + + render: function() { + var json = { + num: this.rebaseArray.length + }; + + this.$el.html(this.template(json)); + + // also render each entry + var listHolder = this.$('ul#rebaseEntries'); + this.rebaseEntries.each(function(entry) { + new RebaseEntryView({ + el: listHolder, + model: entry + }); + }, this); + + // then make it reorderable.. + listHolder.sortable({ + distance: 5, + placeholder: 'ui-state-highlight' + }); + }, + +}); + +var RebaseEntry = Backbone.Model.extend({ + defaults: { + pick: true + }, + + toggle: function() { + this.set('pick', !this.get('pick')); + } +}); + +var RebaseEntryCollection = Backbone.Collection.extend({ + model: RebaseEntry +}); + +var RebaseEntryView = Backbone.View.extend({ + tagName: 'li', + template: _.template($('#interactive-rebase-entry-template').html()), + + toggle: function() { + this.model.toggle(); + + // toggle a class also + this.listEntry.toggleClass('notPicked', !this.model.get('pick')); + }, + + initialize: function(options) { + this.render(); + }, + + render: function() { + var json = this.model.toJSON(); + this.$el.append(this.template(this.model.toJSON())); + + // hacky :( who would have known jquery barfs on ids with %'s and quotes + this.listEntry = this.$el.children(':last'); + + this.listEntry.delegate('#toggleButton', 'click', _.bind(function() { + this.toggle(); + }, this)); + } +}); + +exports.InteractiveRebaseView = InteractiveRebaseView; + + +}); + +require.define("/levels.js",function(require,module,exports,__dirname,__filename,process,global){// static class... +function LevelEngine() { + +} + +LevelEngine.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) { + var result = true; + _.each(branches, function(branchName) { + result = result && this.compareBranchWithinTrees(treeA, treeB, branchName); + }, this); + + return result; +}; + +LevelEngine.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) { + treeA = this.convertTreeSafe(treeA); + treeB = this.convertTreeSafe(treeB); + + this.stripTreeFields([treeA, treeB]); + + // we need a recursive comparison function to bubble up the branch + var recurseCompare = function(commitA, commitB) { + // this is the short-circuit base case + var result = _.isEqual(commitA, commitB); + if (!result) { + return false; + } + + // we loop through each parent ID. we sort the parent ID's beforehand + // so the index lookup is valid + _.each(commitA.parents, function(pAid, index) { + var pBid = commitB.parents[index]; + + var childA = treeA.commits[pAid]; + var childB = treeB.commits[pBid]; + + result = result && recurseCompare(childA, childB); + }, this); + // if each of our children recursively are equal, we are good + return result; + }; + + var branchA = treeA.branches[branchName]; + var branchB = treeB.branches[branchName]; + + return _.isEqual(branchA, branchB) && + recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); +}; + +LevelEngine.prototype.convertTreeSafe = function(tree) { + if (typeof tree == 'string') { + return JSON.parse(unescape(tree)); + } + return tree; +}; + +LevelEngine.prototype.stripTreeFields = function(trees) { + var stripFields = ['createTime', 'author', 'commitMessage']; + var sortFields = ['children', 'parents']; + + _.each(trees, function(tree) { + _.each(tree.commits, function(commit) { + _.each(stripFields, function(field) { + commit[field] = undefined; + }); + _.each(sortFields, function(field) { + if (commit[field]) { + commit[field] = commit[field].sort(); + } + }); + }); + }); +}; + +LevelEngine.prototype.compareTrees = function(treeA, treeB) { + treeA = this.convertTreeSafe(treeA); + treeB = this.convertTreeSafe(treeB); + + // now we need to strip out the fields we don't care about, aka things + // like createTime, message, author + this.stripTreeFields([treeA, treeB]); + + return _.isEqual(treeA, treeB); +}; + +var levelEngine = new LevelEngine(); + +exports.LevelEngine = LevelEngine; + + +}); + +require.define("/animationFactory.js",function(require,module,exports,__dirname,__filename,process,global){/****************** + * This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time. + * The tricky thing is that when a new commit has to be "born," say in the middle of a rebase + * or something, it must animate out from the parent position to it's birth position. + + * These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree, + * store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots. + * and then essentially animate the entire tree too. + */ + +var Animation = require('./async').Animation; +var GRAPHICS = require('./constants').GRAPHICS; + +// essentially a static class +var AnimationFactory = function() { + +} + +AnimationFactory.prototype.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) { + if (!animationQueue) { + throw new Error("Need animation queue to add closure to!"); + } + + var time = GRAPHICS.defaultAnimationTime * 1.0; + var bounceTime = time * 2; + + // essentially refresh the entire tree, but do a special thing for the commit + var visNode = commit.get('visNode'); + + var animation = function() { + // this takes care of refs and all that jazz, and updates all the positions + gitVisuals.refreshTree(time); + + visNode.setBirth(); + visNode.parentInFront(); + gitVisuals.visBranchesFront(); + + visNode.animateUpdatedPosition(bounceTime, 'bounce'); + visNode.animateOutgoingEdges(time); + }; + + animationQueue.add(new Animation({ + closure: animation, + duration: Math.max(time, bounceTime) + })); +}; + +AnimationFactory.prototype.overrideOpacityDepth2 = function(attr, opacity) { + opacity = (opacity === undefined) ? 1 : opacity; + + var newAttr = {}; + + _.each(attr, function(partObj, partName) { + newAttr[partName] = {}; + _.each(partObj, function(val, key) { + if (key == 'opacity') { + newAttr[partName][key] = opacity; + } else { + newAttr[partName][key] = val; + } + }); + }); + return newAttr; +}; + +AnimationFactory.prototype.overrideOpacityDepth3 = function(snapShot, opacity) { + var newSnap = {}; + + _.each(snapShot, function(visObj, visID) { + newSnap[visID] = this.overrideOpacityDepth2(visObj, opacity); + }, this); + return newSnap; +}; + +AnimationFactory.prototype.genCommitBirthClosureFromSnapshot = function(step, gitVisuals) { + var time = GRAPHICS.defaultAnimationTime * 1.0; + var bounceTime = time * 1.5; + + var visNode = step.newCommit.get('visNode'); + var afterAttrWithOpacity = this.overrideOpacityDepth2(step.afterSnapshot[visNode.getID()]); + var afterSnapWithOpacity = this.overrideOpacityDepth3(step.afterSnapshot); + + var animation = function() { + visNode.setBirthFromSnapshot(step.beforeSnapshot); + visNode.parentInFront(); + gitVisuals.visBranchesFront(); + + visNode.animateToAttr(afterAttrWithOpacity, bounceTime, 'bounce'); + visNode.animateOutgoingEdgesToAttr(afterSnapWithOpacity, bounceTime); + }; + + return animation; +}; + +AnimationFactory.prototype.refreshTree = function(animationQueue, gitVisuals) { + animationQueue.add(new Animation({ + closure: function() { + gitVisuals.refreshTree(); + } + })); +}; + +AnimationFactory.prototype.rebaseAnimation = function(animationQueue, rebaseResponse, + gitEngine, gitVisuals) { + + this.rebaseHighlightPart(animationQueue, rebaseResponse, gitEngine); + this.rebaseBirthPart(animationQueue, rebaseResponse, gitEngine, gitVisuals); +}; + +AnimationFactory.prototype.rebaseHighlightPart = function(animationQueue, rebaseResponse, gitEngine) { + var fullTime = GRAPHICS.defaultAnimationTime * 0.66; + var slowTime = fullTime * 2.0; + + // we want to highlight all the old commits + var oldCommits = rebaseResponse.toRebaseArray; + // we are either highlighting to a visBranch or a visNode + var visBranch = rebaseResponse.destinationBranch.get('visBranch'); + if (!visBranch) { + // in the case where we rebase onto a commit + visBranch = rebaseResponse.destinationBranch.get('visNode'); + } + + _.each(oldCommits, function(oldCommit) { + var visNode = oldCommit.get('visNode'); + animationQueue.add(new Animation({ + closure: function() { + visNode.highlightTo(visBranch, slowTime, 'easeInOut'); + }, + duration: fullTime * 1.5 + })); + + }, this); + + this.delay(animationQueue, fullTime * 2); +}; + +AnimationFactory.prototype.rebaseBirthPart = function(animationQueue, rebaseResponse, + gitEngine, gitVisuals) { + var rebaseSteps = rebaseResponse.rebaseSteps; + + var newVisNodes = []; + _.each(rebaseSteps, function(step) { + var visNode = step.newCommit.get('visNode'); + + newVisNodes.push(visNode); + visNode.setOpacity(0); + visNode.setOutgoingEdgesOpacity(0); + }, this); + + var previousVisNodes = []; + _.each(rebaseSteps, function(rebaseStep, index) { + var toOmit = newVisNodes.slice(index + 1); + + var snapshotPart = this.genFromToSnapshotAnimation( + rebaseStep.beforeSnapshot, + rebaseStep.afterSnapshot, + toOmit, + previousVisNodes, + gitVisuals + ); + var birthPart = this.genCommitBirthClosureFromSnapshot(rebaseStep, gitVisuals); + + var animation = function() { + snapshotPart(); + birthPart(); + }; + + animationQueue.add(new Animation({ + closure: animation, + duration: GRAPHICS.defaultAnimationTime * 1.5 + })); + + previousVisNodes.push(rebaseStep.newCommit.get('visNode')); + }, this); + + // need to delay to let bouncing finish + this.delay(animationQueue); + + this.refreshTree(animationQueue, gitVisuals); +}; + +AnimationFactory.prototype.delay = function(animationQueue, time) { + time = time || GRAPHICS.defaultAnimationTime; + animationQueue.add(new Animation({ + closure: function() { }, + duration: time + })); +}; + +AnimationFactory.prototype.genSetAllCommitOpacities = function(visNodes, opacity) { + // need to slice for closure + var nodesToAnimate = visNodes.slice(0); + + return function() { + _.each(nodesToAnimate, function(visNode) { + visNode.setOpacity(opacity); + visNode.setOutgoingEdgesOpacity(opacity); + }); + }; +}; + +AnimationFactory.prototype.stripObjectsFromSnapshot = function(snapShot, toOmit) { + var ids = []; + _.each(toOmit, function(obj) { + ids.push(obj.getID()); + }); + + var newSnapshot = {}; + _.each(snapShot, function(val, key) { + if (_.include(ids, key)) { + // omit + return; + } + newSnapshot[key] = val; + }, this); + return newSnapshot; +}; + +AnimationFactory.prototype.genFromToSnapshotAnimation = function( + beforeSnapshot, + afterSnapshot, + commitsToOmit, + commitsToFixOpacity, + gitVisuals) { + + // we want to omit the commit outgoing edges + var toOmit = []; + _.each(commitsToOmit, function(visNode) { + toOmit.push(visNode); + toOmit = toOmit.concat(visNode.get('outgoingEdges')); + }); + + var fixOpacity = function(obj) { + if (!obj) { return; } + _.each(obj, function(attr, partName) { + obj[partName].opacity = 1; + }); + }; + + // HORRIBLE loop to fix opacities all throughout the snapshot + _.each([beforeSnapshot, afterSnapshot], function(snapShot) { + _.each(commitsToFixOpacity, function(visNode) { + fixOpacity(snapShot[visNode.getID()]); + _.each(visNode.get('outgoingEdges'), function(visEdge) { + fixOpacity(snapShot[visEdge.getID()]); + }); + }); + }); + + return function() { + gitVisuals.animateAllFromAttrToAttr(beforeSnapshot, afterSnapshot, toOmit); + }; +}; + +exports.AnimationFactory = AnimationFactory; + + +}); +require("/animationFactory.js"); + +require.define("/async.js",function(require,module,exports,__dirname,__filename,process,global){var GLOBAL = require('./constants').GLOBAL; + +var Animation = Backbone.Model.extend({ + defaults: { + duration: 300, + closure: null + }, + + validateAtInit: function() { + if (!this.get('closure')) { + throw new Error('give me a closure!'); + } + }, + + initialize: function(options) { + this.validateAtInit(); + }, + + run: function() { + this.get('closure')(); + } +}); + +var AnimationQueue = Backbone.Model.extend({ + defaults: { + animations: null, + index: 0, + callback: null, + defer: false + }, + + initialize: function(options) { + this.set('animations', []); + if (!options.callback) { + console.warn('no callback'); + } + }, + + add: function(animation) { + if (!animation instanceof Animation) { + throw new Error("Need animation not something else"); + } + + this.get('animations').push(animation); + }, + + start: function() { + this.set('index', 0); + + // set the global lock that we are animating + GLOBAL.isAnimating = true; + this.next(); + }, + + finish: function() { + // release lock here + GLOBAL.isAnimating = false; + this.get('callback')(); + }, + + next: function() { + // ok so call the first animation, and then set a timeout to call the next + // TODO: animations with callbacks!! + var animations = this.get('animations'); + var index = this.get('index'); + if (index >= animations.length) { + this.finish(); + return; + } + + var next = animations[index]; + var duration = next.get('duration'); + + next.run(); + + this.set('index', index + 1); + setTimeout(_.bind(function() { + this.next(); + }, this), duration); + }, +}); + +exports.Animation = Animation; +exports.AnimationQueue = AnimationQueue; + + +}); +require("/async.js"); + +require.define("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit; +var Branch = require('./git').Branch; + +var Main = require('./main'); +var Command = require('./commandModel').Command; +var CommandEntry = require('./commandModel').CommandEntry; +var TIME = require('./constants').TIME; + +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) { + require('./main').getEvents().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. + Main.getEvents().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("/commandModel.js",function(require,module,exports,__dirname,__filename,process,global){var Errors = require('./errors'); + +var CommandProcessError = Errors.CommandProcessError; +var GitError = Errors.GitError; +var Warning = Errors.Warning; +var CommandResult = Errors.CommandResult; + +var Command = Backbone.Model.extend({ + defaults: { + status: 'inqueue', + rawStr: null, + result: '', + + error: null, + warnings: null, + + generalArgs: null, + supportedMap: null, + options: null, + method: null, + + createTime: null + }, + + validateAtInit: function() { + // weird things happen with defaults if you dont + // make new objects + this.set('generalArgs', []); + this.set('supportedMap', {}); + this.set('warnings', []); + + if (this.get('rawStr') === null) { + throw new Error('Give me a string!'); + } + if (!this.get('createTime')) { + this.set('createTime', new Date().toString()); + } + + + this.on('change:error', this.errorChanged, this); + // catch errors on init + if (this.get('error')) { + this.errorChanged(); + } + }, + + setResult: function(msg) { + this.set('result', msg); + }, + + addWarning: function(msg) { + this.get('warnings').push(msg); + // change numWarnings so the change event fires. This is bizarre -- Backbone can't + // detect if an array changes, so adding an element does nothing + this.set('numWarnings', this.get('numWarnings') ? this.get('numWarnings') + 1 : 1); + }, + + getFormattedWarnings: function() { + if (!this.get('warnings').length) { + return ''; + } + var i = ''; + return '

' + i + this.get('warnings').join('

' + i) + '

'; + }, + + initialize: function() { + this.validateAtInit(); + this.parseOrCatch(); + }, + + parseOrCatch: function() { + try { + this.parse(); + } catch (err) { + if (err instanceof CommandProcessError || + err instanceof GitError || + err instanceof CommandResult || + err instanceof Warning) { + // errorChanged() will handle status and all of that + this.set('error', err); + } else { + throw err; + } + } + }, + + errorChanged: function() { + var err = this.get('error'); + if (err instanceof CommandProcessError || + err instanceof GitError) { + this.set('status', 'error'); + } else if (err instanceof CommandResult) { + this.set('status', 'finished'); + } else if (err instanceof Warning) { + this.set('status', 'warning'); + } + this.formatError(); + }, + + formatError: function() { + this.set('result', this.get('error').toResult()); + }, + + getShortcutMap: function() { + return { + 'git commit': /^gc($|\s)/, + 'git add': /^ga($|\s)/, + 'git checkout': /^gchk($|\s)/, + 'git rebase': /^gr($|\s)/, + 'git branch': /^gb($|\s)/ + }; + }, + + getRegexMap: function() { + return { + // ($|\s) means that we either have to end the string + // after the command or there needs to be a space for options + commit: /^commit($|\s)/, + add: /^add($|\s)/, + checkout: /^checkout($|\s)/, + rebase: /^rebase($|\s)/, + reset: /^reset($|\s)/, + branch: /^branch($|\s)/, + revert: /^revert($|\s)/, + log: /^log($|\s)/, + merge: /^merge($|\s)/, + show: /^show($|\s)/, + status: /^status($|\s)/, + 'cherry-pick': /^cherry-pick($|\s)/ + }; + }, + + getSandboxCommands: function() { + return [ + [/^ls/, function() { + throw new CommandResult({ + msg: "DontWorryAboutFilesInThisDemo.txt" + }); + }], + [/^cd/, function() { + throw new CommandResult({ + msg: "Directory Changed to '/directories/dont/matter/in/this/demo'" + }); + }], + [/^git help($|\s)/, function() { + // sym link this to the blank git command + var allCommands = Command.prototype.getSandboxCommands(); + // wow this is hacky :( + var equivalent = 'git'; + _.each(allCommands, function(bits) { + var regex = bits[0]; + if (regex.test(equivalent)) { + bits[1](); + } + }); + }], + [/^git$/, function() { + var lines = [ + 'Git Version PCOTTLE.1.0', + '
', + 'Usage:', + _.escape('\t git []'), + '
', + 'Supported commands:', + '
', + ]; + var commands = OptionParser.prototype.getMasterOptionMap(); + + // build up a nice display of what we support + _.each(commands, function(commandOptions, command) { + lines.push('git ' + command); + _.each(commandOptions, function(vals, optionName) { + lines.push('\t ' + optionName); + }, this); + }, this); + + // format and throw + var msg = lines.join('\n'); + msg = msg.replace(/\t/g, '   '); + throw new CommandResult({ + msg: msg + }); + }], + [/^refresh$/, function() { + events.trigger('refreshTree'); + throw new CommandResult({ + msg: "Refreshing tree..." + }); + }], + [/^rollup (\d+)$/, function(bits) { + // go roll up these commands by joining them with semicolons + events.trigger('rollupCommands', bits[1]); + throw new CommandResult({ + msg: 'Commands combined!' + }); + }] + ]; + }, + + parse: function() { + var str = this.get('rawStr'); + // first if the string is empty, they just want a blank line + if (!str.length) { + throw new CommandResult({msg: ""}); + } + + // then check if it's one of our sandbox commands + _.each(this.getSandboxCommands(), function(tuple) { + var regex = tuple[0]; + var results = regex.exec(str); + if (results) { + tuple[1](results); + } + }); + + // then check if shortcut exists, and replace, but + // preserve options if so + _.each(this.getShortcutMap(), function(regex, method) { + var results = regex.exec(str); + if (results) { + str = method + ' ' + str.slice(results[0].length); + } + }); + + // see if begins with git + if (str.slice(0,3) !== 'git') { + throw new CommandProcessError({ + msg: 'That command is not supported, sorry!' + }); + } + + // ok, we have a (probably) valid command. actually parse it + this.gitParse(str); + }, + + gitParse: function(str) { + // now slice off command part + var fullCommand = str.slice('git '.length); + + // see if we support this particular command + _.each(this.getRegexMap(), function(regex, method) { + if (regex.exec(fullCommand)) { + this.set('options', fullCommand.slice(method.length + 1)); + this.set('method', method); + // we should stop iterating, but the regex will only match + // one command in practice. we could stop iterating if we used + // jqeurys for each but im using underscore (for no real reason other + // than style) + } + }, this); + + if (!this.get('method')) { + throw new CommandProcessError({ + msg: "Sorry, this demo does not support that git command: " + fullCommand + }); + } + + // parse off the options and assemble the map / general args + var optionParser = new OptionParser(this.get('method'), this.get('options')); + + // steal these away so we can be completely JSON + this.set('generalArgs', optionParser.generalArgs); + this.set('supportedMap', optionParser.supportedMap); + }, +}); + +/** + * OptionParser + */ +function OptionParser(method, options) { + this.method = method; + this.rawOptions = options; + + this.supportedMap = this.getMasterOptionMap()[method]; + if (this.supportedMap === undefined) { + throw new Error('No option map for ' + method); + } + + this.generalArgs = []; + this.explodeAndSet(); +} + +OptionParser.prototype.getMasterOptionMap = function() { + // here a value of false means that we support it, even if its just a + // pass-through option. If the value is not here (aka will be undefined + // when accessed), we do not support it. + return { + commit: { + '--amend': false, + '-a': false, // warning + '-am': false, // warning + '-m': false + }, + status: {}, + log: {}, + add: {}, + 'cherry-pick': {}, + branch: { + '-d': false, + '-D': false, + '-f': false, + '--contains': false + }, + checkout: { + '-b': false, + '-B': false, + '-': false + }, + reset: { + '--hard': false, + '--soft': false, // this will raise an error but we catch it in gitEngine + }, + merge: {}, + rebase: { + '-i': false // the mother of all options + }, + revert: {}, + show: {} + }; +}; + +OptionParser.prototype.explodeAndSet = function() { + // split on spaces, except when inside quotes + + var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/g) || []; + + for (var i = 0; i < exploded.length; i++) { + var part = exploded[i]; + if (part.slice(0,1) == '-') { + // it's an option, check supportedMap + if (this.supportedMap[part] === undefined) { + throw new CommandProcessError({ + msg: 'The option "' + part + '" is not supported' + }); + } + + // go through and include all the next args until we hit another option or the end + var optionArgs = []; + var next = i + 1; + while (next < exploded.length && exploded[next].slice(0,1) != '-') { + optionArgs.push(exploded[next]); + next += 1; + } + i = next - 1; + + // **phew** we are done grabbing those. theseArgs is truthy even with an empty array + this.supportedMap[part] = optionArgs; + } else { + // must be a general arg + this.generalArgs.push(part); + } + } + + // done! +}; + +// command entry is for the commandview +var CommandEntry = Backbone.Model.extend({ + defaults: { + text: '' + }, + localStorage: new Backbone.LocalStorage('CommandEntries') +}); + + +exports.CommandEntry = CommandEntry; +exports.Command = Command; + + +}); +require("/commandModel.js"); + +require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection; +var Main = require('./main'); +var Command = require('./commandModel').Command; +var CommandEntry = require('./commandModel').CommandEntry; + +var Errors = require('./errors'); +var Warning = Errors.Warning; + +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)); + + Main.getEvents().on('processCommandFromEvent', this.addToCollection, this); + Main.getEvents().on('submitCommandValueFromEvent', this.submitValue, this); + Main.getEvents().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); + + Main.getEvents().on('issueWarning', this.addWarning, this); + Main.getEvents().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("/constants.js",function(require,module,exports,__dirname,__filename,process,global){/** + * Constants....!!! + */ +var TIME = { + betweenCommandsDelay: 400, +}; + +// useful for locks, etc +var GLOBAL = { + isAnimating: false +}; + +var GRAPHICS = { + arrowHeadSize: 8, + + nodeRadius: 17, + curveControlPointOffset: 50, + defaultEasing: 'easeInOut', + defaultAnimationTime: 400, + + //rectFill: '#FF3A3A', + rectFill: 'hsb(0.8816909813322127,0.7,1)', + headRectFill: '#2831FF', + rectStroke: '#FFF', + rectStrokeWidth: '3', + + multiBranchY: 20, + upstreamHeadOpacity: 0.5, + upstreamNoneOpacity: 0.2, + edgeUpstreamHeadOpacity: 0.4, + edgeUpstreamNoneOpacity: 0.15, + + visBranchStrokeWidth: 2, + visBranchStrokeColorNone: '#333', + + defaultNodeFill: 'hsba(0.5,0.8,0.7,1)', + defaultNodeStrokeWidth: 2, + defaultNodeStroke: '#FFF', + + orphanNodeFill: 'hsb(0.5,0.8,0.7)', +}; + +exports.GLOBAL = GLOBAL; +exports.TIME = TIME; +exports.GRAPHICS = GRAPHICS; + + +}); +require("/constants.js"); + +require.define("/debug.js",function(require,module,exports,__dirname,__filename,process,global){var toGlobalize = { + Tree: require('./tree'), + Visuals: require('./visuals'), + Git: require('./git'), + CommandModel: require('./commandModel'), + Levels: require('./levels'), + Constants: require('./constants'), + Collections: require('./collections'), + Async: require('./async'), + AnimationFactory: require('./animationFactory'), + Main: require('./main') +}; + +_.each(toGlobalize, function(module) { + _.extend(window, module); +}); + +window.events = toGlobalize.Main.getEvents(); + + +}); +require("/debug.js"); + +require.define("/errors.js",function(require,module,exports,__dirname,__filename,process,global){var MyError = Backbone.Model.extend({ + defaults: { + type: 'MyError', + msg: 'Unknown Error' + }, + toString: function() { + return this.get('type') + ': ' + this.get('msg'); + }, + + getMsg: function() { + return this.get('msg') || 'Unknown Error'; + }, + + toResult: function() { + if (!this.get('msg').length) { + return ''; + } + return '

' + this.get('msg').replace(/\n/g, '

') + '

'; + } +}); + +var CommandProcessError = exports.CommandProcessError = MyError.extend({ + defaults: { + type: 'Command Process Error' + } +}); + +var CommandResult = exports.CommandResult = MyError.extend({ + defaults: { + type: 'Command Result' + } +}); + +var Warning = exports.Warning = MyError.extend({ + defaults: { + type: 'Warning' + } +}); + +var GitError = exports.GitError = MyError.extend({ + defaults: { + type: 'Git Error' + } +}); + + +}); +require("/errors.js"); + require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactoryModule = require('./animationFactory'); var animationFactory = new AnimationFactoryModule.AnimationFactory(); var Main = require('./main'); @@ -6956,1566 +8296,308 @@ exports.Ref = Ref; }); require("/git.js"); -require.define("/commandModel.js",function(require,module,exports,__dirname,__filename,process,global){var Errors = require('./errors'); +require.define("/levels.js",function(require,module,exports,__dirname,__filename,process,global){// static class... +function LevelEngine() { -var CommandProcessError = Errors.CommandProcessError; -var GitError = Errors.GitError; -var Warning = Errors.Warning; -var CommandResult = Errors.CommandResult; - -var Command = Backbone.Model.extend({ - defaults: { - status: 'inqueue', - rawStr: null, - result: '', - - error: null, - warnings: null, - - generalArgs: null, - supportedMap: null, - options: null, - method: null, - - createTime: null - }, - - validateAtInit: function() { - // weird things happen with defaults if you dont - // make new objects - this.set('generalArgs', []); - this.set('supportedMap', {}); - this.set('warnings', []); - - if (this.get('rawStr') === null) { - throw new Error('Give me a string!'); - } - if (!this.get('createTime')) { - this.set('createTime', new Date().toString()); - } - - - this.on('change:error', this.errorChanged, this); - // catch errors on init - if (this.get('error')) { - this.errorChanged(); - } - }, - - setResult: function(msg) { - this.set('result', msg); - }, - - addWarning: function(msg) { - this.get('warnings').push(msg); - // change numWarnings so the change event fires. This is bizarre -- Backbone can't - // detect if an array changes, so adding an element does nothing - this.set('numWarnings', this.get('numWarnings') ? this.get('numWarnings') + 1 : 1); - }, - - getFormattedWarnings: function() { - if (!this.get('warnings').length) { - return ''; - } - var i = ''; - return '

' + i + this.get('warnings').join('

' + i) + '

'; - }, - - initialize: function() { - this.validateAtInit(); - this.parseOrCatch(); - }, - - parseOrCatch: function() { - try { - this.parse(); - } catch (err) { - if (err instanceof CommandProcessError || - err instanceof GitError || - err instanceof CommandResult || - err instanceof Warning) { - // errorChanged() will handle status and all of that - this.set('error', err); - } else { - throw err; - } - } - }, - - errorChanged: function() { - var err = this.get('error'); - if (err instanceof CommandProcessError || - err instanceof GitError) { - this.set('status', 'error'); - } else if (err instanceof CommandResult) { - this.set('status', 'finished'); - } else if (err instanceof Warning) { - this.set('status', 'warning'); - } - this.formatError(); - }, - - formatError: function() { - this.set('result', this.get('error').toResult()); - }, - - getShortcutMap: function() { - return { - 'git commit': /^gc($|\s)/, - 'git add': /^ga($|\s)/, - 'git checkout': /^gchk($|\s)/, - 'git rebase': /^gr($|\s)/, - 'git branch': /^gb($|\s)/ - }; - }, - - getRegexMap: function() { - return { - // ($|\s) means that we either have to end the string - // after the command or there needs to be a space for options - commit: /^commit($|\s)/, - add: /^add($|\s)/, - checkout: /^checkout($|\s)/, - rebase: /^rebase($|\s)/, - reset: /^reset($|\s)/, - branch: /^branch($|\s)/, - revert: /^revert($|\s)/, - log: /^log($|\s)/, - merge: /^merge($|\s)/, - show: /^show($|\s)/, - status: /^status($|\s)/, - 'cherry-pick': /^cherry-pick($|\s)/ - }; - }, - - getSandboxCommands: function() { - return [ - [/^ls/, function() { - throw new CommandResult({ - msg: "DontWorryAboutFilesInThisDemo.txt" - }); - }], - [/^cd/, function() { - throw new CommandResult({ - msg: "Directory Changed to '/directories/dont/matter/in/this/demo'" - }); - }], - [/^git help($|\s)/, function() { - // sym link this to the blank git command - var allCommands = Command.prototype.getSandboxCommands(); - // wow this is hacky :( - var equivalent = 'git'; - _.each(allCommands, function(bits) { - var regex = bits[0]; - if (regex.test(equivalent)) { - bits[1](); - } - }); - }], - [/^git$/, function() { - var lines = [ - 'Git Version PCOTTLE.1.0', - '
', - 'Usage:', - _.escape('\t git []'), - '
', - 'Supported commands:', - '
', - ]; - var commands = OptionParser.prototype.getMasterOptionMap(); - - // build up a nice display of what we support - _.each(commands, function(commandOptions, command) { - lines.push('git ' + command); - _.each(commandOptions, function(vals, optionName) { - lines.push('\t ' + optionName); - }, this); - }, this); - - // format and throw - var msg = lines.join('\n'); - msg = msg.replace(/\t/g, '   '); - throw new CommandResult({ - msg: msg - }); - }], - [/^refresh$/, function() { - events.trigger('refreshTree'); - throw new CommandResult({ - msg: "Refreshing tree..." - }); - }], - [/^rollup (\d+)$/, function(bits) { - // go roll up these commands by joining them with semicolons - events.trigger('rollupCommands', bits[1]); - throw new CommandResult({ - msg: 'Commands combined!' - }); - }] - ]; - }, - - parse: function() { - var str = this.get('rawStr'); - // first if the string is empty, they just want a blank line - if (!str.length) { - throw new CommandResult({msg: ""}); - } - - // then check if it's one of our sandbox commands - _.each(this.getSandboxCommands(), function(tuple) { - var regex = tuple[0]; - var results = regex.exec(str); - if (results) { - tuple[1](results); - } - }); - - // then check if shortcut exists, and replace, but - // preserve options if so - _.each(this.getShortcutMap(), function(regex, method) { - var results = regex.exec(str); - if (results) { - str = method + ' ' + str.slice(results[0].length); - } - }); - - // see if begins with git - if (str.slice(0,3) !== 'git') { - throw new CommandProcessError({ - msg: 'That command is not supported, sorry!' - }); - } - - // ok, we have a (probably) valid command. actually parse it - this.gitParse(str); - }, - - gitParse: function(str) { - // now slice off command part - var fullCommand = str.slice('git '.length); - - // see if we support this particular command - _.each(this.getRegexMap(), function(regex, method) { - if (regex.exec(fullCommand)) { - this.set('options', fullCommand.slice(method.length + 1)); - this.set('method', method); - // we should stop iterating, but the regex will only match - // one command in practice. we could stop iterating if we used - // jqeurys for each but im using underscore (for no real reason other - // than style) - } - }, this); - - if (!this.get('method')) { - throw new CommandProcessError({ - msg: "Sorry, this demo does not support that git command: " + fullCommand - }); - } - - // parse off the options and assemble the map / general args - var optionParser = new OptionParser(this.get('method'), this.get('options')); - - // steal these away so we can be completely JSON - this.set('generalArgs', optionParser.generalArgs); - this.set('supportedMap', optionParser.supportedMap); - }, -}); - -/** - * OptionParser - */ -function OptionParser(method, options) { - this.method = method; - this.rawOptions = options; - - this.supportedMap = this.getMasterOptionMap()[method]; - if (this.supportedMap === undefined) { - throw new Error('No option map for ' + method); - } - - this.generalArgs = []; - this.explodeAndSet(); } -OptionParser.prototype.getMasterOptionMap = function() { - // here a value of false means that we support it, even if its just a - // pass-through option. If the value is not here (aka will be undefined - // when accessed), we do not support it. - return { - commit: { - '--amend': false, - '-a': false, // warning - '-am': false, // warning - '-m': false - }, - status: {}, - log: {}, - add: {}, - 'cherry-pick': {}, - branch: { - '-d': false, - '-D': false, - '-f': false, - '--contains': false - }, - checkout: { - '-b': false, - '-B': false, - '-': false - }, - reset: { - '--hard': false, - '--soft': false, // this will raise an error but we catch it in gitEngine - }, - merge: {}, - rebase: { - '-i': false // the mother of all options - }, - revert: {}, - show: {} - }; +LevelEngine.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) { + var result = true; + _.each(branches, function(branchName) { + result = result && this.compareBranchWithinTrees(treeA, treeB, branchName); + }, this); + + return result; }; -OptionParser.prototype.explodeAndSet = function() { - // split on spaces, except when inside quotes +LevelEngine.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) { + treeA = this.convertTreeSafe(treeA); + treeB = this.convertTreeSafe(treeB); - var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/g) || []; + this.stripTreeFields([treeA, treeB]); - for (var i = 0; i < exploded.length; i++) { - var part = exploded[i]; - if (part.slice(0,1) == '-') { - // it's an option, check supportedMap - if (this.supportedMap[part] === undefined) { - throw new CommandProcessError({ - msg: 'The option "' + part + '" is not supported' - }); - } - - // go through and include all the next args until we hit another option or the end - var optionArgs = []; - var next = i + 1; - while (next < exploded.length && exploded[next].slice(0,1) != '-') { - optionArgs.push(exploded[next]); - next += 1; - } - i = next - 1; - - // **phew** we are done grabbing those. theseArgs is truthy even with an empty array - this.supportedMap[part] = optionArgs; - } else { - // must be a general arg - this.generalArgs.push(part); + // we need a recursive comparison function to bubble up the branch + var recurseCompare = function(commitA, commitB) { + // this is the short-circuit base case + var result = _.isEqual(commitA, commitB); + if (!result) { + return false; } - } - // done! + // we loop through each parent ID. we sort the parent ID's beforehand + // so the index lookup is valid + _.each(commitA.parents, function(pAid, index) { + var pBid = commitB.parents[index]; + + var childA = treeA.commits[pAid]; + var childB = treeB.commits[pBid]; + + result = result && recurseCompare(childA, childB); + }, this); + // if each of our children recursively are equal, we are good + return result; + }; + + var branchA = treeA.branches[branchName]; + var branchB = treeB.branches[branchName]; + + return _.isEqual(branchA, branchB) && + recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); }; -// command entry is for the commandview -var CommandEntry = Backbone.Model.extend({ - defaults: { - text: '' - }, - localStorage: new Backbone.LocalStorage('CommandEntries') -}); +LevelEngine.prototype.convertTreeSafe = function(tree) { + if (typeof tree == 'string') { + return JSON.parse(unescape(tree)); + } + return tree; +}; +LevelEngine.prototype.stripTreeFields = function(trees) { + var stripFields = ['createTime', 'author', 'commitMessage']; + var sortFields = ['children', 'parents']; -exports.CommandEntry = CommandEntry; -exports.Command = Command; - - -}); -require("/commandModel.js"); - -require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection; -var Main = require('./main'); -var Command = require('./commandModel').Command; -var CommandEntry = require('./commandModel').CommandEntry; - -var Errors = require('./errors'); -var Warning = Errors.Warning; - -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) + _.each(trees, function(tree) { + _.each(tree.commits, function(commit) { + _.each(stripFields, function(field) { + commit[field] = undefined; + }); + _.each(sortFields, function(field) { + if (commit[field]) { + commit[field] = commit[field].sort(); + } + }); }); + }); +}; - this.index = -1; +LevelEngine.prototype.compareTrees = function(treeA, treeB) { + treeA = this.convertTreeSafe(treeA); + treeB = this.convertTreeSafe(treeB); - this.commandSpan = this.$('#prompt span.command')[0]; - this.commandCursor = this.$('#prompt span.cursor')[0]; + // now we need to strip out the fields we don't care about, aka things + // like createTime, message, author + this.stripTreeFields([treeA, treeB]); - // 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)); + return _.isEqual(treeA, treeB); +}; + +var levelEngine = new LevelEngine(); + +exports.LevelEngine = LevelEngine; - $(document).delegate('#commandTextField', 'blur', _.bind(function() { - this.blur(); - }, this)); +}); +require("/levels.js"); - Main.getEvents().on('processCommandFromEvent', this.addToCollection, this); - Main.getEvents().on('submitCommandValueFromEvent', this.submitValue, this); - Main.getEvents().on('rollupCommands', this.rollupCommands, this); +require.define("/main.js",function(require,module,exports,__dirname,__filename,process,global){/** + * Globals + */ +var events = _.clone(Backbone.Events); +var ui = null; +var mainVis = null; - // hacky timeout focus - setTimeout(_.bind(function() { - this.focus(); - }, this), 100); - }, +/////////////////////////////////////////////////////////////////////// + +$(document).ready(function(){ + var Visuals = require('./visuals'); + + ui = new UI(); + mainVis = new Visuals.Visualization({ + el: $('#canvasWrapper')[0] + }); + + if (/\?demo/.test(window.location.href)) { + setTimeout(function() { + events.trigger('submitCommandValueFromEvent', "gc; git checkout HEAD~1; git commit; git checkout -b bugFix; gc; gc; git rebase master; git checkout master; gc; gc; git merge bugFix"); + }, 500); + } +}); + +function UI() { + var Collections = require('./collections'); + var CommandViews = require('./commandViews'); + + this.commandCollection = new Collections.CommandCollection(); + + this.commandBuffer = new Collections.CommandBuffer({ + collection: this.commandCollection + }); + + this.commandPromptView = new CommandViews.CommandPromptView({ + el: $('#commandLineBar'), + collection: this.commandCollection + }); + this.commandLineHistoryView = new CommandViews.CommandLineHistoryView({ + el: $('#commandLineHistory'), + collection: this.commandCollection + }); + + $('#commandTextField').focus(); +} + +exports.getEvents = function() { + return events; +}; +exports.getUI = function() { + return ui; +}; + + +}); +require("/main.js"); + +require.define("/miscViews.js",function(require,module,exports,__dirname,__filename,process,global){var InteractiveRebaseView = Backbone.View.extend({ + tagName: 'div', + template: _.template($('#interactive-rebase-template').html()), events: { - 'keydown #commandTextField': 'onKey', - 'keyup #commandTextField': 'onKeyUp', - 'blur #commandTextField': 'hideCursor', - 'focus #commandTextField': 'showCursor' + 'click #confirmButton': 'confirmed' }, - blur: function() { - $(this.commandCursor).toggleClass('shown', false); + initialize: function(options) { + this.hasClicked = false; + this.rebaseCallback = options.callback; + + this.rebaseArray = options.toRebase; + + this.rebaseEntries = new RebaseEntryCollection(); + this.rebaseMap = {}; + this.entryObjMap = {}; + + this.rebaseArray.reverse(); + // make basic models for each commit + _.each(this.rebaseArray, function(commit) { + var id = commit.get('id'); + this.rebaseMap[id] = commit; + this.entryObjMap[id] = new RebaseEntry({ + id: id + }); + this.rebaseEntries.add(this.entryObjMap[id]); + }, this); + + this.render(); + + // show the dialog holder + this.show(); }, - focus: function() { - this.$('#commandTextField').focus(); - this.showCursor(); + show: function() { + this.toggleVisibility(true); }, - hideCursor: function() { - this.toggleCursor(false); + hide: function() { + this.toggleVisibility(false); }, - showCursor: function() { - this.toggleCursor(true); + toggleVisibility: function(toggle) { + console.log('toggling'); + $('#dialogHolder').toggleClass('shown', toggle); }, - 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; + confirmed: function() { + // we hide the dialog anyways, but they might be fast clickers + if (this.hasClicked) { return; } + this.hasClicked = true; - // yay! we actually can display something - var commandEntry = this.commands.toArray()[this.index].get('text'); - this.setTextField(commandEntry); - }, + // first of all hide + this.$el.css('display', 'none'); - clearLocalStorage: function() { - this.commands.each(function(c) { - Backbone.sync('delete', c, function() { }); - }, this); - localStorage.setItem('CommandEntries', ''); - }, + // get our ordering + var uiOrder = []; + this.$('ul#rebaseEntries li').each(function(i, obj) { + uiOrder.push(obj.id); + }); - 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') + ';'; + // now get the real array + var toRebase = []; + _.each(uiOrder, function(id) { + // the model + if (this.entryObjMap[id].get('pick')) { + toRebase.unshift(this.rebaseMap[id]); + } }, this); - console.log('the str', str); + this.rebaseCallback(toRebase); - 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); - }); + this.$el.html(''); + // kill ourselves? + delete this; }, render: function() { - var json = _.extend( - { - resultType: '', - result: '', - formattedWarnings: this.model.getFormattedWarnings() - }, - this.model.toJSON() - ); + var json = { + num: this.rebaseArray.length + }; 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); - - Main.getEvents().on('issueWarning', this.addWarning, this); - Main.getEvents().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("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit; -var Branch = require('./git').Branch; - -var Main = require('./main'); -var Command = require('./commandModel').Command; -var CommandEntry = require('./commandModel').CommandEntry; -var TIME = require('./constants').TIME; - -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) { - require('./main').getEvents().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(); + // also render each entry + var listHolder = this.$('ul#rebaseEntries'); + this.rebaseEntries.each(function(entry) { + new RebaseEntryView({ + el: listHolder, + model: entry + }); }, 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. - Main.getEvents().trigger('processCommand', popped, callback); - } else { - this.clear(); - } + // then make it reorderable.. + listHolder.sortable({ + distance: 5, + placeholder: 'ui-state-highlight' + }); }, - 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; - +var RebaseEntry = Backbone.Model.extend({ + defaults: { + pick: true + }, + toggle: function() { + this.set('pick', !this.get('pick')); + } }); -require("/collections.js"); -require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); -var GRAPHICS = require('./constants').GRAPHICS; -var GLOBAL = require('./constants').GLOBAL; +var RebaseEntryCollection = Backbone.Collection.extend({ + model: RebaseEntry +}); -var Collections = require('./collections'); -var CommitCollection = Collections.CommitCollection; -var BranchCollection = Collections.BranchCollection; +var RebaseEntryView = Backbone.View.extend({ + tagName: 'li', + template: _.template($('#interactive-rebase-entry-template').html()), -var Tree = require('./tree'); -var VisEdgeCollection = Tree.VisEdgeCollection; -var VisBranchCollection = Tree.VisBranchCollection; -var VisNode = Tree.VisNode; -var VisBranch = Tree.VisBranch; -var VisEdge = Tree.VisEdge; + toggle: function() { + this.model.toggle(); + + // toggle a class also + this.listEntry.toggleClass('notPicked', !this.model.get('pick')); + }, -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); - }); + this.render(); }, - paperInitialize: function(paper, options) { - this.paper = paper; + render: function() { + var json = this.model.toJSON(); + this.$el.append(this.template(this.model.toJSON())); - this.commitCollection = new CommitCollection(); - this.branchCollection = new BranchCollection(); + // hacky :( who would have known jquery barfs on ids with %'s and quotes + this.listEntry = this.$el.children(':last'); - this.gitVisuals = new GitVisuals({ - commitCollection: this.commitCollection, - branchCollection: this.branchCollection, - paper: this.paper - }); - - var GitEngine = require('./git').GitEngine; - 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); + this.listEntry.delegate('#toggleButton', 'click', _.bind(function() { + this.toggle(); + }, this)); } - }); -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 = []; - - Main.getEvents().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) { - Main.getEvents().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) + ')'; -} - -exports.Visualization = Visualization; +exports.InteractiveRebaseView = InteractiveRebaseView; }); -require("/visuals.js"); +require("/miscViews.js"); require.define("/tree.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); var GRAPHICS = require('./constants').GRAPHICS; @@ -9488,720 +9570,686 @@ exports.VisBranch = VisBranch; }); require("/tree.js"); -require.define("/animationFactory.js",function(require,module,exports,__dirname,__filename,process,global){/****************** - * This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time. - * The tricky thing is that when a new commit has to be "born," say in the middle of a rebase - * or something, it must animate out from the parent position to it's birth position. - - * These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree, - * store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots. - * and then essentially animate the entire tree too. - */ - -var Animation = require('./async').Animation; +require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); var GRAPHICS = require('./constants').GRAPHICS; +var GLOBAL = require('./constants').GLOBAL; -// essentially a static class -var AnimationFactory = function() { +var Collections = require('./collections'); +var CommitCollection = Collections.CommitCollection; +var BranchCollection = Collections.BranchCollection; +var Tree = require('./tree'); +var VisEdgeCollection = Tree.VisEdgeCollection; +var VisBranchCollection = Tree.VisBranchCollection; +var VisNode = Tree.VisNode; +var VisBranch = Tree.VisBranch; +var VisEdge = Tree.VisEdge; + +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 + }); + + var GitEngine = require('./git').GitEngine; + 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 = []; + + Main.getEvents().on('refreshTree', _.bind( + this.refreshTree, this + )); } -AnimationFactory.prototype.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) { - if (!animationQueue) { - throw new Error("Need animation queue to add closure to!"); +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 time = GRAPHICS.defaultAnimationTime * 1.0; - var bounceTime = time * 2; - - // essentially refresh the entire tree, but do a special thing for the commit - var visNode = commit.get('visNode'); - - var animation = function() { - // this takes care of refs and all that jazz, and updates all the positions - gitVisuals.refreshTree(time); - - visNode.setBirth(); - visNode.parentInFront(); - gitVisuals.visBranchesFront(); - - visNode.animateUpdatedPosition(bounceTime, 'bounce'); - visNode.animateOutgoingEdges(time); + var shrink = function(frac, total, padding) { + return padding + frac * (total - padding * 2); }; - animationQueue.add(new Animation({ - closure: animation, - duration: Math.max(time, bounceTime) - })); -}; - -AnimationFactory.prototype.overrideOpacityDepth2 = function(attr, opacity) { - opacity = (opacity === undefined) ? 1 : opacity; - - var newAttr = {}; - - _.each(attr, function(partObj, partName) { - newAttr[partName] = {}; - _.each(partObj, function(val, key) { - if (key == 'opacity') { - newAttr[partName][key] = opacity; - } else { - newAttr[partName][key] = val; - } - }); - }); - return newAttr; -}; - -AnimationFactory.prototype.overrideOpacityDepth3 = function(snapShot, opacity) { - var newSnap = {}; - - _.each(snapShot, function(visObj, visID) { - newSnap[visID] = this.overrideOpacityDepth2(visObj, opacity); - }, this); - return newSnap; -}; - -AnimationFactory.prototype.genCommitBirthClosureFromSnapshot = function(step, gitVisuals) { - var time = GRAPHICS.defaultAnimationTime * 1.0; - var bounceTime = time * 1.5; - - var visNode = step.newCommit.get('visNode'); - var afterAttrWithOpacity = this.overrideOpacityDepth2(step.afterSnapshot[visNode.getID()]); - var afterSnapWithOpacity = this.overrideOpacityDepth3(step.afterSnapshot); - - var animation = function() { - visNode.setBirthFromSnapshot(step.beforeSnapshot); - visNode.parentInFront(); - gitVisuals.visBranchesFront(); - - visNode.animateToAttr(afterAttrWithOpacity, bounceTime, 'bounce'); - visNode.animateOutgoingEdgesToAttr(afterSnapWithOpacity, bounceTime); - }; - - return animation; -}; - -AnimationFactory.prototype.refreshTree = function(animationQueue, gitVisuals) { - animationQueue.add(new Animation({ - closure: function() { - gitVisuals.refreshTree(); - } - })); -}; - -AnimationFactory.prototype.rebaseAnimation = function(animationQueue, rebaseResponse, - gitEngine, gitVisuals) { - - this.rebaseHighlightPart(animationQueue, rebaseResponse, gitEngine); - this.rebaseBirthPart(animationQueue, rebaseResponse, gitEngine, gitVisuals); -}; - -AnimationFactory.prototype.rebaseHighlightPart = function(animationQueue, rebaseResponse, gitEngine) { - var fullTime = GRAPHICS.defaultAnimationTime * 0.66; - var slowTime = fullTime * 2.0; - - // we want to highlight all the old commits - var oldCommits = rebaseResponse.toRebaseArray; - // we are either highlighting to a visBranch or a visNode - var visBranch = rebaseResponse.destinationBranch.get('visBranch'); - if (!visBranch) { - // in the case where we rebase onto a commit - visBranch = rebaseResponse.destinationBranch.get('visNode'); - } - - _.each(oldCommits, function(oldCommit) { - var visNode = oldCommit.get('visNode'); - animationQueue.add(new Animation({ - closure: function() { - visNode.highlightTo(visBranch, slowTime, 'easeInOut'); - }, - duration: fullTime * 1.5 - })); - - }, this); - - this.delay(animationQueue, fullTime * 2); -}; - -AnimationFactory.prototype.rebaseBirthPart = function(animationQueue, rebaseResponse, - gitEngine, gitVisuals) { - var rebaseSteps = rebaseResponse.rebaseSteps; - - var newVisNodes = []; - _.each(rebaseSteps, function(step) { - var visNode = step.newCommit.get('visNode'); - - newVisNodes.push(visNode); - visNode.setOpacity(0); - visNode.setOutgoingEdgesOpacity(0); - }, this); - - var previousVisNodes = []; - _.each(rebaseSteps, function(rebaseStep, index) { - var toOmit = newVisNodes.slice(index + 1); - - var snapshotPart = this.genFromToSnapshotAnimation( - rebaseStep.beforeSnapshot, - rebaseStep.afterSnapshot, - toOmit, - previousVisNodes, - gitVisuals - ); - var birthPart = this.genCommitBirthClosureFromSnapshot(rebaseStep, gitVisuals); - - var animation = function() { - snapshotPart(); - birthPart(); - }; - - animationQueue.add(new Animation({ - closure: animation, - duration: GRAPHICS.defaultAnimationTime * 1.5 - })); - - previousVisNodes.push(rebaseStep.newCommit.get('visNode')); - }, this); - - // need to delay to let bouncing finish - this.delay(animationQueue); - - this.refreshTree(animationQueue, gitVisuals); -}; - -AnimationFactory.prototype.delay = function(animationQueue, time) { - time = time || GRAPHICS.defaultAnimationTime; - animationQueue.add(new Animation({ - closure: function() { }, - duration: time - })); -}; - -AnimationFactory.prototype.genSetAllCommitOpacities = function(visNodes, opacity) { - // need to slice for closure - var nodesToAnimate = visNodes.slice(0); - - return function() { - _.each(nodesToAnimate, function(visNode) { - visNode.setOpacity(opacity); - visNode.setOutgoingEdgesOpacity(opacity); - }); + return { + x: shrink(pos.x, this.paper.width, bounds.widthPadding), + y: shrink(pos.y, this.paper.height, bounds.heightPadding) }; }; -AnimationFactory.prototype.stripObjectsFromSnapshot = function(snapShot, toOmit) { - var ids = []; - _.each(toOmit, function(obj) { - ids.push(obj.getID()); - }); - - var newSnapshot = {}; - _.each(snapShot, function(val, key) { - if (_.include(ids, key)) { - // omit +GitVisuals.prototype.animateAllFromAttrToAttr = function(fromSnapshot, toSnapshot, idsToOmit) { + var animate = function(obj) { + var id = obj.getID(); + if (_.include(idsToOmit, id)) { return; } - newSnapshot[key] = val; + + 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); - return newSnapshot; + + this.visBranchCollection.each(function(visBranch) { + snapshot[visBranch.getID()] = visBranch.getAttributes(); + }, this); + + this.visEdgeCollection.each(function(visEdge) { + snapshot[visEdge.getID()] = visEdge.getAttributes(); + }, this); + + return snapshot; }; -AnimationFactory.prototype.genFromToSnapshotAnimation = function( - beforeSnapshot, - afterSnapshot, - commitsToOmit, - commitsToFixOpacity, - gitVisuals) { +GitVisuals.prototype.refreshTree = function(speed) { + if (!this.gitReady) { + return; + } - // we want to omit the commit outgoing edges - var toOmit = []; - _.each(commitsToOmit, function(visNode) { - toOmit.push(visNode); - toOmit = toOmit.concat(visNode.get('outgoingEdges')); - }); + // this method can only be called after graphics are rendered + this.fullCalc(); - var fixOpacity = function(obj) { - if (!obj) { return; } - _.each(obj, function(attr, partName) { - obj[partName].opacity = 1; - }); - }; - - // HORRIBLE loop to fix opacities all throughout the snapshot - _.each([beforeSnapshot, afterSnapshot], function(snapShot) { - _.each(commitsToFixOpacity, function(visNode) { - fixOpacity(snapShot[visNode.getID()]); - _.each(visNode.get('outgoingEdges'), function(visEdge) { - fixOpacity(snapShot[visEdge.getID()]); - }); - }); - }); - - return function() { - gitVisuals.animateAllFromAttrToAttr(beforeSnapshot, afterSnapshot, toOmit); - }; + this.animateAll(speed); }; -exports.AnimationFactory = AnimationFactory; +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; +}; + +// we debounce here so we aren't firing a resize call on every resize event +// but only after they stop +GitVisuals.prototype.canvasResize = _.debounce(function(width, height) { + // refresh when we are ready + if (GLOBAL.isAnimating) { + Main.getEvents().trigger('processCommandFromEvent', 'refresh'); + } else { + this.refreshTree(); + } +}, 200); + +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(); +}; -}); -require("/animationFactory.js"); +/************************ + * Random util functions, some from liquidGraph + ***********************/ +function blendHueStrings(hueStrings) { + // assumes a sat of 0.7 and brightness of 1 -require.define("/levels.js",function(require,module,exports,__dirname,__filename,process,global){// static class... -function LevelEngine() { + 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) + ')'; } -LevelEngine.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) { - var result = true; - _.each(branches, function(branchName) { - result = result && this.compareBranchWithinTrees(treeA, treeB, branchName); - }, this); - - return result; -}; - -LevelEngine.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) { - treeA = this.convertTreeSafe(treeA); - treeB = this.convertTreeSafe(treeB); - - this.stripTreeFields([treeA, treeB]); - - // we need a recursive comparison function to bubble up the branch - var recurseCompare = function(commitA, commitB) { - // this is the short-circuit base case - var result = _.isEqual(commitA, commitB); - if (!result) { - return false; - } - - // we loop through each parent ID. we sort the parent ID's beforehand - // so the index lookup is valid - _.each(commitA.parents, function(pAid, index) { - var pBid = commitB.parents[index]; - - var childA = treeA.commits[pAid]; - var childB = treeB.commits[pBid]; - - result = result && recurseCompare(childA, childB); - }, this); - // if each of our children recursively are equal, we are good - return result; - }; - - var branchA = treeA.branches[branchName]; - var branchB = treeB.branches[branchName]; - - return _.isEqual(branchA, branchB) && - recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); -}; - -LevelEngine.prototype.convertTreeSafe = function(tree) { - if (typeof tree == 'string') { - return JSON.parse(unescape(tree)); - } - return tree; -}; - -LevelEngine.prototype.stripTreeFields = function(trees) { - var stripFields = ['createTime', 'author', 'commitMessage']; - var sortFields = ['children', 'parents']; - - _.each(trees, function(tree) { - _.each(tree.commits, function(commit) { - _.each(stripFields, function(field) { - commit[field] = undefined; - }); - _.each(sortFields, function(field) { - if (commit[field]) { - commit[field] = commit[field].sort(); - } - }); - }); - }); -}; - -LevelEngine.prototype.compareTrees = function(treeA, treeB) { - treeA = this.convertTreeSafe(treeA); - treeB = this.convertTreeSafe(treeB); - - // now we need to strip out the fields we don't care about, aka things - // like createTime, message, author - this.stripTreeFields([treeA, treeB]); - - return _.isEqual(treeA, treeB); -}; - -var levelEngine = new LevelEngine(); - -exports.LevelEngine = LevelEngine; +exports.Visualization = Visualization; }); -require("/levels.js"); - -require.define("/async.js",function(require,module,exports,__dirname,__filename,process,global){var GLOBAL = require('./constants').GLOBAL; - -var Animation = Backbone.Model.extend({ - defaults: { - duration: 300, - closure: null - }, - - validateAtInit: function() { - if (!this.get('closure')) { - throw new Error('give me a closure!'); - } - }, - - initialize: function(options) { - this.validateAtInit(); - }, - - run: function() { - this.get('closure')(); - } -}); - -var AnimationQueue = Backbone.Model.extend({ - defaults: { - animations: null, - index: 0, - callback: null, - defer: false - }, - - initialize: function(options) { - this.set('animations', []); - if (!options.callback) { - console.warn('no callback'); - } - }, - - add: function(animation) { - if (!animation instanceof Animation) { - throw new Error("Need animation not something else"); - } - - this.get('animations').push(animation); - }, - - start: function() { - this.set('index', 0); - - // set the global lock that we are animating - GLOBAL.isAnimating = true; - this.next(); - }, - - finish: function() { - // release lock here - GLOBAL.isAnimating = false; - this.get('callback')(); - }, - - next: function() { - // ok so call the first animation, and then set a timeout to call the next - // TODO: animations with callbacks!! - var animations = this.get('animations'); - var index = this.get('index'); - if (index >= animations.length) { - this.finish(); - return; - } - - var next = animations[index]; - var duration = next.get('duration'); - - next.run(); - - this.set('index', index + 1); - setTimeout(_.bind(function() { - this.next(); - }, this), duration); - }, -}); - -exports.Animation = Animation; -exports.AnimationQueue = AnimationQueue; - - -}); -require("/async.js"); - -require.define("/constants.js",function(require,module,exports,__dirname,__filename,process,global){/** - * Constants....!!! - */ -var TIME = { - betweenCommandsDelay: 400, -}; - -// useful for locks, etc -var GLOBAL = { - isAnimating: false -}; - -var GRAPHICS = { - arrowHeadSize: 8, - - nodeRadius: 17, - curveControlPointOffset: 50, - defaultEasing: 'easeInOut', - defaultAnimationTime: 400, - - //rectFill: '#FF3A3A', - rectFill: 'hsb(0.8816909813322127,0.7,1)', - headRectFill: '#2831FF', - rectStroke: '#FFF', - rectStrokeWidth: '3', - - multiBranchY: 20, - upstreamHeadOpacity: 0.5, - upstreamNoneOpacity: 0.2, - edgeUpstreamHeadOpacity: 0.4, - edgeUpstreamNoneOpacity: 0.15, - - visBranchStrokeWidth: 2, - visBranchStrokeColorNone: '#333', - - defaultNodeFill: 'hsba(0.5,0.8,0.7,1)', - defaultNodeStrokeWidth: 2, - defaultNodeStroke: '#FFF', - - orphanNodeFill: 'hsb(0.5,0.8,0.7)', -}; - -exports.GLOBAL = GLOBAL; -exports.TIME = TIME; -exports.GRAPHICS = GRAPHICS; - - -}); -require("/constants.js"); - -require.define("/miscViews.js",function(require,module,exports,__dirname,__filename,process,global){var InteractiveRebaseView = Backbone.View.extend({ - tagName: 'div', - template: _.template($('#interactive-rebase-template').html()), - - events: { - 'click #confirmButton': 'confirmed' - }, - - initialize: function(options) { - this.hasClicked = false; - this.rebaseCallback = options.callback; - - this.rebaseArray = options.toRebase; - - this.rebaseEntries = new RebaseEntryCollection(); - this.rebaseMap = {}; - this.entryObjMap = {}; - - this.rebaseArray.reverse(); - // make basic models for each commit - _.each(this.rebaseArray, function(commit) { - var id = commit.get('id'); - this.rebaseMap[id] = commit; - this.entryObjMap[id] = new RebaseEntry({ - id: id - }); - this.rebaseEntries.add(this.entryObjMap[id]); - }, this); - - this.render(); - - // show the dialog holder - this.show(); - }, - - show: function() { - this.toggleVisibility(true); - }, - - hide: function() { - this.toggleVisibility(false); - }, - - toggleVisibility: function(toggle) { - console.log('toggling'); - $('#dialogHolder').toggleClass('shown', toggle); - }, - - confirmed: function() { - // we hide the dialog anyways, but they might be fast clickers - if (this.hasClicked) { - return; - } - this.hasClicked = true; - - // first of all hide - this.$el.css('display', 'none'); - - // get our ordering - var uiOrder = []; - this.$('ul#rebaseEntries li').each(function(i, obj) { - uiOrder.push(obj.id); - }); - - // now get the real array - var toRebase = []; - _.each(uiOrder, function(id) { - // the model - if (this.entryObjMap[id].get('pick')) { - toRebase.unshift(this.rebaseMap[id]); - } - }, this); - - this.rebaseCallback(toRebase); - - this.$el.html(''); - // kill ourselves? - delete this; - }, - - render: function() { - var json = { - num: this.rebaseArray.length - }; - - this.$el.html(this.template(json)); - - // also render each entry - var listHolder = this.$('ul#rebaseEntries'); - this.rebaseEntries.each(function(entry) { - new RebaseEntryView({ - el: listHolder, - model: entry - }); - }, this); - - // then make it reorderable.. - listHolder.sortable({ - distance: 5, - placeholder: 'ui-state-highlight' - }); - }, - -}); - -var RebaseEntry = Backbone.Model.extend({ - defaults: { - pick: true - }, - - toggle: function() { - this.set('pick', !this.get('pick')); - } -}); - -var RebaseEntryCollection = Backbone.Collection.extend({ - model: RebaseEntry -}); - -var RebaseEntryView = Backbone.View.extend({ - tagName: 'li', - template: _.template($('#interactive-rebase-entry-template').html()), - - toggle: function() { - this.model.toggle(); - - // toggle a class also - this.listEntry.toggleClass('notPicked', !this.model.get('pick')); - }, - - initialize: function(options) { - this.render(); - }, - - render: function() { - var json = this.model.toJSON(); - this.$el.append(this.template(this.model.toJSON())); - - // hacky :( who would have known jquery barfs on ids with %'s and quotes - this.listEntry = this.$el.children(':last'); - - this.listEntry.delegate('#toggleButton', 'click', _.bind(function() { - this.toggle(); - }, this)); - } -}); - -exports.InteractiveRebaseView = InteractiveRebaseView; - - -}); -require("/miscViews.js"); - -require.define("/errors.js",function(require,module,exports,__dirname,__filename,process,global){var MyError = Backbone.Model.extend({ - defaults: { - type: 'MyError', - msg: 'Unknown Error' - }, - toString: function() { - return this.get('type') + ': ' + this.get('msg'); - }, - - getMsg: function() { - return this.get('msg') || 'Unknown Error'; - }, - - toResult: function() { - if (!this.get('msg').length) { - return ''; - } - return '

' + this.get('msg').replace(/\n/g, '

') + '

'; - } -}); - -var CommandProcessError = exports.CommandProcessError = MyError.extend({ - defaults: { - type: 'Command Process Error' - } -}); - -var CommandResult = exports.CommandResult = MyError.extend({ - defaults: { - type: 'Command Result' - } -}); - -var Warning = exports.Warning = MyError.extend({ - defaults: { - type: 'Warning' - } -}); - -var GitError = exports.GitError = MyError.extend({ - defaults: { - type: 'Git Error' - } -}); - - -}); -require("/errors.js"); - -require.define("/debug.js",function(require,module,exports,__dirname,__filename,process,global){var toGlobalize = { - Tree: require('./tree'), - Visuals: require('./visuals'), - Git: require('./git'), - CommandModel: require('./commandModel'), - Levels: require('./levels'), - Constants: require('./constants'), - Collections: require('./collections'), - Async: require('./async'), - AnimationFactory: require('./animationFactory'), - Main: require('./main') -}; - -_.each(toGlobalize, function(module) { - _.extend(window, module); -}); - -window.events = toGlobalize.Main.getEvents(); - - -}); -require("/debug.js"); +require("/visuals.js"); })(); diff --git a/src/main.js b/src/main.js index be750230..f6caba85 100644 --- a/src/main.js +++ b/src/main.js @@ -1,27 +1,17 @@ -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 */ var events = _.clone(Backbone.Events); var ui = null; -var animationFactory = null; - -/** - * Static Classes - */ -animationFactory = new AnimationFactory(); +var mainVis = null; /////////////////////////////////////////////////////////////////////// $(document).ready(function(){ + var Visuals = require('./visuals'); + ui = new UI(); - mainVis = new Visualization({ + mainVis = new Visuals.Visualization({ el: $('#canvasWrapper')[0] }); @@ -33,18 +23,20 @@ $(document).ready(function(){ }); function UI() { - // static classes - this.commandCollection = new CommandCollection(); + var Collections = require('./collections'); + var CommandViews = require('./commandViews'); - this.commandBuffer = new CommandBuffer({ + this.commandCollection = new Collections.CommandCollection(); + + this.commandBuffer = new Collections.CommandBuffer({ collection: this.commandCollection }); - this.commandPromptView = new CommandPromptView({ + this.commandPromptView = new CommandViews.CommandPromptView({ el: $('#commandLineBar'), collection: this.commandCollection }); - this.commandLineHistoryView = new CommandLineHistoryView({ + this.commandLineHistoryView = new CommandViews.CommandLineHistoryView({ el: $('#commandLineHistory'), collection: this.commandCollection }); @@ -55,6 +47,7 @@ function UI() { exports.getEvents = function() { return events; }; -exports.ui = ui; -exports.animationFactory = animationFactory; +exports.getUI = function() { + return ui; +};