diff --git a/src/commandline.js b/src/commandline.js index fde0325a..4f246246 100644 --- a/src/commandline.js +++ b/src/commandline.js @@ -12,11 +12,11 @@ function Command(str) { Command.prototype.getShortcutMap = function() { return { - 'git commit': /^gc/, - 'git add': /^ga/, - 'git checkout': /^gchk/, - 'git rebase': /^gr/, - 'git branch': /^gb/ + 'git commit': /^gc($|\s)/, + 'git add': /^ga($|\s)/, + 'git checkout': /^gchk($|\s)/, + 'git rebase': /^gr($|\s)/, + 'git branch': /^gb($|\s)/ }; }; @@ -30,7 +30,8 @@ Command.prototype.getRegexMap = function() { rebase: /^rebase($|\s)/, reset: /^reset($|\s)/, branch: /^branch($|\s)/, - revert: /^revert($|\s)/ + revert: /^revert($|\s)/, + merge: /^merge($|\s)/ }; }; @@ -78,7 +79,7 @@ Command.prototype.parse = function(str) { _.each(this.getShortcutMap(), function(regex, method) { var results = regex.exec(str); if (results) { - str = method + str.slice(results[0].length); + str = method + ' ' + str.slice(results[0].length); } }); @@ -155,6 +156,7 @@ OptionParser.prototype.getMasterOptionMap = function() { reset: { '--hard': false, }, + merge: {}, rebase: {}, revert: {} }; diff --git a/src/constants.js b/src/constants.js index ce56f409..d3f554de 100644 --- a/src/constants.js +++ b/src/constants.js @@ -29,6 +29,7 @@ var graphics = { // ref names refFont: '14pt Courier New', refFontFill: '#FFF', + refSelectedFontFill: 'rgb(255, 30, 10)', // ref arrows arrowFill: '#FFF', diff --git a/src/git.js b/src/git.js index ab802dbd..31e023d1 100644 --- a/src/git.js +++ b/src/git.js @@ -116,9 +116,15 @@ GitEngine.prototype.logBranches = function() { }); }; -GitEngine.prototype.makeCommit = function(parent) { +GitEngine.prototype.makeCommit = function() { + // manually copy arguments + var parents = []; + _.each(arguments, function(arg) { + parents.push(arg); + }); + var commit = new Commit({ - parents: [parent] + parents: parents }); this.refs[commit.get('id')] = commit; this.collection.add(commit); @@ -154,6 +160,7 @@ GitEngine.prototype.commit = function() { var newCommit = this.makeCommit(targetCommit); if (this.getDetachedHead()) { events.trigger('commandProcessWarn', 'Warning!! Detached HEAD state'); + this.HEAD.set('target', newCommit); } else { var targetBranch = this.HEAD.get('target'); targetBranch.set('target', newCommit); @@ -218,6 +225,14 @@ GitEngine.prototype.getCommitFromRef = function(ref) { return start; }; +GitEngine.prototype.getOneBeforeCommit = function(ref) { + var start = this.resolveId(ref); + if (start === this.HEAD) { + start = start.get('target'); + } + return start; +}; + GitEngine.prototype.numBackFrom = function(commit, numBack) { // going back '3' from a given ref is not trivial, for you might have // a bunch of merge commits and such. like this situation: @@ -250,6 +265,7 @@ GitEngine.prototype.numBackFrom = function(commit, numBack) { while (pQueue.length && numBack !== 0) { var popped = pQueue.shift(0); + console.log(popped); pQueue = pQueue.concat(popped.get('parents')); pQueue.sort(sortFunc); numBack--; @@ -263,20 +279,92 @@ GitEngine.prototype.numBackFrom = function(commit, numBack) { return pQueue.shift(0); }; +GitEngine.prototype.mergeStarter = function() { + if (this.generalArgs.length > 2) { + throw new GitError({ + msg: 'merge with more than 2 arguments doesnt make sense!' + }); + } + if (!this.generalArgs.length) { + throw new GitError({ + msg: 'Give me a branch to merge into!' + }); + } + if (this.generalArgs.length == 1) { + this.generalArgs.push('HEAD'); + } + + if (_.include(this.generalArgs, 'HEAD') && this.getDetachedHead()) { + throw new GitError({ + msg: 'Cant merge things referencing HEAD when you are in detached head!' + }); + } + + this.merge(this.generalArgs[0], this.generalArgs[1]); +}; + +GitEngine.prototype.merge = function(targetSource, currentLocation) { + // first some conditions + if (this.isUpstreamOf(targetSource, currentLocation)) { + throw new CommandResult({ + msg: 'Branch already up-to-date' + }); + } + + if (this.isUpstreamOf(currentLocation, targetSource)) { + // just set the target of this current location to the source + var currLoc = this.getOneBeforeCommit(currentLocation); + currLoc.set('target', this.getCommitFromRef(targetSource)); + throw new CommandResult({ + msg: 'Fast-forwarding...' + }); + } + + // now the part of making a merge commit + var parent1 = this.getCommitFromRef(currentLocation); + var parent2 = this.getCommitFromRef(targetSource); + + var commit = this.makeCommit(parent1, parent2); + var currLoc = this.getOneBeforeCommit(currentLocation); + currLoc.set('target', commit); +}; + GitEngine.prototype.checkoutStarter = function() { + if (this.commandOptions['-b']) { + // the user is really trying to just make a branch and then switch to it. so first: + var args = this.commandOptions['-b']; + if (!args.length) { + throw new GitError({ + msg: 'I expect a branch name with "checkout -b"!!' + }); + } + if (args.length > 2) { + throw new GitError({ + msg: 'Only two args max with checkout -b please (the new name and target branch)' + }); + } + + // we are good! + if (args.length == 1) { + args.push('HEAD'); + } + this.branch(args[0], args[1]); + this.checkout(args[0]); + return; + } + if (this.generalArgs.length != 1) { throw new GitError({ msg: 'I expect one argument along with git checkout (dont reference files)' }); } - this.checkout(this.generalArgs[0]); }; GitEngine.prototype.checkout = function(idOrTarget) { console.log('the target', idOrTarget); var target = this.resolveId(idOrTarget); - console.log(target); + console.log('the result', target); if (target.get('id') === 'HEAD') { // git checkout HEAD is a // meaningless command but i used to do this back in the day @@ -363,37 +451,59 @@ GitEngine.prototype.dispatch = function(commandObj) { this[commandObj.method + 'Starter'](); }; -GitEngine.prototype.add = function() { +GitEngine.prototype.addStarter = function() { throw new CommandResult({ msg: "This demo is meant to demonstrate git branching, so don't worry about " + "adding / staging files. Just go ahead and commit away!" }); }; -GitEngine.prototype.execute = function(command, callback) { - // execute command, and when it's finished, call the callback - // we still need to figure this out +GitEngine.prototype.getCommonAncestor = function(cousin, ancestor) { + if (this.isUpstreamOf(cousin, ancestor)) { + throw new Error('Dont use common ancestor if we are upstream!'); + } - var closures = this.getClosuresForCommand(command); - // make a scheduler based on all the closures, and pass in our callback - var s = new Scheduler(closures, { - callback: callback - }); - s.start(); + var upstreamSet = this.getUpstreamSet(ancestor); + // now BFS off of cousin until you find something + + var queue = [this.getCommitFromRef(cousin)]; + while (queue.length) { + var here = queue.pop(); + if (upstreamSet[here.get('id')]) { + return here; + } + queue.concat(here.get('parents')); + } + throw new Error('something has gone very wrong... two nodes arent connected!'); }; -GitEngine.prototype.getClosuresForCommand = function(command) { - var numbers = [1,2,3,4,5,6,7,8,9,10]; - var closures = []; - _.each(numbers, function(num) { - var c = function() { - console.log(num); - }; - closures.push(c); - }); - return closures; +GitEngine.prototype.isUpstreamOf = function(child, ancestor) { + child = this.getCommitFromRef(child); + + // basically just do a completely BFS search on ancestor to the root, then + // check for membership of child in that set of explored nodes + var upstream = this.getUpstreamSet(ancestor); + return upstream[child.get('id')] !== undefined; }; +GitEngine.prototype.getUpstreamSet = function(ancestor) { + var commit = this.getCommitFromRef(ancestor); + var queue = [commit]; + + var exploredSet = {}; + while (queue.length) { + var here = queue.pop(); + var rents = here.get('parents'); + + _.each(rents, function(rent) { + exploredSet[rent.get('id')] = true; + queue.push(rent); + }); + } + return exploredSet; +}; + + var Ref = Backbone.Model.extend({ initialize: function() { if (!this.get('target')) { @@ -422,12 +532,15 @@ var Branch = Ref.extend({ }); var Commit = Backbone.Model.extend({ + defaults: { + type: 'commit', + children: [] + }, + validateAtInit: function() { if (!this.get('id')) { this.set('id', uniqueId('C')); } - this.set('type', 'commit'); - this.set('children', []); // root commits have no parents if (this.get('rootCommit')) { diff --git a/src/legacy.js b/src/legacy.js index ee4fdcf9..0add74f3 100644 --- a/src/legacy.js +++ b/src/legacy.js @@ -36,9 +36,12 @@ Renderer = function(canvas) { return; } + events.trigger('fixNodePositions', particleSystem); + ctx.clearRect(0,0, canvas.width, canvas.height); particleSystem.eachEdge(this.drawEdge); particleSystem.eachNode(this.drawNode); + events.trigger('drawGitVisuals', particleSystem, ctx, canvas); }, diff --git a/src/mine.js b/src/mine.js index 43f2e9c8..268ce6b5 100644 --- a/src/mine.js +++ b/src/mine.js @@ -8,7 +8,7 @@ var gitEngine = null; var gitVisuals = null; $(document).ready(function(){ - sys = arbor.ParticleSystem(4000, 500, 0.5, false, 55, 0.005, 'verlet'); + sys = arbor.ParticleSystem(4000, 200, 0.5, false, 55, 0.005, 'verlet'); sys.renderer = Renderer('#viewport'); new CommandLineView({ @@ -24,7 +24,8 @@ $(document).ready(function(){ var repulsionBreathe = function(r) { sys.parameters({repulsion: r}); }; - var b = new Breather(repulsionBreathe, 6050, 4000); + // TODO: decide on breather + // var b = new Breather(repulsionBreathe, 6050, 4000); graphicsEffects.edgeStrokeEffect = new GraphicsEffect('edgeStroke', {wait: 1000}); }); diff --git a/src/style/main.css b/src/style/main.css index 892f16c8..0941877d 100644 --- a/src/style/main.css +++ b/src/style/main.css @@ -1,5 +1,5 @@ -html,body { - overflow:hidden; +html, body { + overflow:hidden; background: -moz-radial-gradient(center, ellipse cover, #0066cc 0%, #000000 90%); background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,#0066cc), color-stop(90%,#000000)); @@ -54,7 +54,7 @@ span.arrows { #commandLineHistory { width: 100%; - height: 500px; + height: 200px; overflow-y: scroll; background: #000; opacity: 0.85; diff --git a/src/visuals.js b/src/visuals.js index efa7740d..7fb946d7 100644 --- a/src/visuals.js +++ b/src/visuals.js @@ -3,9 +3,20 @@ function GitVisuals() { this.collection.on('change', _.bind(this.collectionChanged, this)); events.on('drawGitVisuals', _.bind(this.drawVisuals, this)); + events.on('fixNodePositions', _.bind(this.fixNodes, this)); } GitVisuals.prototype.drawVisuals = function(sys, ctx, canvas) { + + this.drawRefs(sys, ctx, canvas); +}; + +GitVisuals.prototype.fixNodes = function(sys) { + this.fixRootCommit(sys); +}; + +GitVisuals.prototype.drawRefs = function(sys, ctx, canvas) { + var sFill = graphics.refSelectedFontFill; // we need to draw refs here var branches = gitEngine.getBranches(); var detachedHead = gitEngine.getDetachedHead(); @@ -14,21 +25,32 @@ GitVisuals.prototype.drawVisuals = function(sys, ctx, canvas) { _.forEach(branches, _.bind(function(branch) { // get the location of the arbor node and then somehow draw the ref to the side? var node = branch.target.get('arborNode'); - var nodePoint = sys.toScreen(node._p); - - // text position - // TODO: better positioning of text here - var screenPoint = _.clone(nodePoint); - screenPoint.x += 100; - - ctx.font = graphics.refFont; - ctx.fillStyle = graphics.refFontFill; - ctx.fillText(branch.id, screenPoint.x, screenPoint.y); - - // also draw an arrow - var offset = Math.round(graphics.nodeRadius * 2.5); - this.drawArrow(ctx, screenPoint, nodePoint, graphics.arrowHeadWidth, offset); + var fillStyle = branch.selected ? sFill : undefined; + this.drawLabel(ctx, sys, node, branch.id, fillStyle); }, this)); + + if (detachedHead) { + var node = HEAD.get('target').get('arborNode'); + this.drawLabel(ctx, sys, node, 'HEAD', sFill); + } +}; + +GitVisuals.prototype.drawLabel = function(ctx, sys, node, name, fillStyle) { + fillStyle = fillStyle || graphics.refFontFill; + + var nodePoint = sys.toScreen(node._p); + // text position + // TODO: better positioning of text here + var screenPoint = _.clone(nodePoint); + screenPoint.x += 100; + + ctx.font = graphics.refFont; + ctx.fillStyle = fillStyle; + ctx.fillText(name, screenPoint.x, screenPoint.y); + + // also draw an arrow + var offset = Math.round(graphics.nodeRadius * 2.5); + this.drawArrow(ctx, screenPoint, nodePoint, graphics.arrowHeadWidth, offset); }; GitVisuals.prototype.drawArrow = function(ctx, start, end, headWidth, offset) { @@ -55,3 +77,15 @@ GitVisuals.prototype.drawArrow = function(ctx, start, end, headWidth, offset) { GitVisuals.prototype.collectionChanged = function() { // redo the algorithms }; + +GitVisuals.prototype.fixRootCommit = function(sys) { + // get the viewports bottom center + var bottomPosScreen = { + x: Math.round($('#viewport').width() * 0.5), + y: $('#viewport').height() - graphics.nodeRadius * 2.5 + }; + + var bottomPos = sys.fromScreen(bottomPosScreen); + // fix the root commit to the bottom + gitEngine.rootCommit.get('arborNode').p = bottomPos; +};