diff --git a/index.html b/index.html index c6061da..ef1e87f 100644 --- a/index.html +++ b/index.html @@ -262,15 +262,46 @@ function open() { explainGit.reset(); - explainGit.open({ + var savedState = null + if (window.localStorage) { + savedState = window.localStorage.getItem('git-viz-snapshot') + } + + var initial = null + if (savedState) { + savedState = JSON.parse(savedState) + var serialized = savedState.stack[savedState.pointer].hv + initial = { name: 'Zen', height: '100%', + initialMessage: 'Have fun.', commitData: [ {id: 'e137e9b', tags: ['master'], message: 'first commit'} ], - initialMessage: - 'Have fun.' - }); + undoHistory: savedState, + savedState: serialized + } + } else { + initial = { + name: 'Zen', + height: '100%', + commitData: [ + {id: 'e137e9b', tags: ['master'], message: 'first commit'} + ], + initialMessage: + 'Have fun.' + } + } + explainGit.open(initial); + } + + window.resetVis = function () { + if (confirm('This will reset your visualization. Are you sure?')) { + if (window.localStorage) { + window.localStorage.removeItem('git-viz-snapshot') + } + open() + } } }); diff --git a/js/controlbox.js b/js/controlbox.js index 7fe5e80..6eaa708 100644 --- a/js/controlbox.js +++ b/js/controlbox.js @@ -25,9 +25,47 @@ function(_yargs) { this._currentCommand = -1; this._tempCommand = ''; this.rebaseConfig = {}; // to configure branches for rebase + + this.undoHistory = config.undoHistory || { + pointer: 0, + stack: [ + { hv: this.historyView.serialize() } + ] + } + + this.historyView.on('lock', this.lock.bind(this)) + this.historyView.on('unlock', this.unlock.bind(this)) } ControlBox.prototype = { + lock: function () { + this.locked = true + }, + + unlock: function () { + this.locked = false + this.createUndoSnapshot(true) + }, + + createUndoSnapshot: function (replace) { + var state = this.historyView.serialize() + if (!replace) { + this.undoHistory.pointer++ + this.undoHistory.stack.length = this.undoHistory.pointer + this.undoHistory.stack.push({ hv: state }) + } else { + this.undoHistory.stack[this.undoHistory.pointer] = { hv: state } + } + + this.persist() + }, + + persist: function () { + if (window.localStorage) { + window.localStorage.setItem('git-viz-snapshot', JSON.stringify(this.undoHistory)) + } + }, + render: function(container) { var cBox = this, cBoxContainer, log, input; @@ -48,8 +86,8 @@ function(_yargs) { switch (e.keyCode) { case 13: - if (this.value.trim() === '') { - break; + if (this.value.trim() === '' || cBox.locked) { + return; } cBox._commandHistory.unshift(this.value); @@ -117,6 +155,43 @@ function(_yargs) { return; } + if (entry.trim().toLowerCase() === 'undo') { + var lastId = this.undoHistory.pointer - 1 + var lastState = this.undoHistory.stack[lastId] + if (lastState) { + this.historyView.deserialize(lastState.hv) + this.undoHistory.pointer = lastId + } else { + this.error("Nothing to undo") + } + this.persist() + this.terminalOutput.append('div') + .classed('command-entry', true) + .html(entry); + return + } + + if (entry.trim().toLowerCase() === 'redo') { + var lastId = this.undoHistory.pointer + 1 + var lastState = this.undoHistory.stack[lastId] + if (lastState) { + this.historyView.deserialize(lastState.hv) + this.undoHistory.pointer = lastId + } else { + this.error("Nothing to redo") + } + this.persist() + this.terminalOutput.append('div') + .classed('command-entry', true) + .html(entry); + return + } + + if (entry.trim().toLowerCase() === 'clear') { + window.resetVis() + return + } + var split = entry.split(' '); this.terminalOutput.append('div') @@ -138,6 +213,7 @@ function(_yargs) { try { if (typeof this[method] === 'function') { this[method](args, options, argsStr); + this.createUndoSnapshot() } else { this.error(); } @@ -538,6 +614,7 @@ function(_yargs) { throw new Error('Current branch is not set up for pulling.'); } + this.lock() setTimeout(function() { try { if (args[0] === '--rebase' || control.rebaseConfig[currentBranch] === 'true') { @@ -547,6 +624,8 @@ function(_yargs) { } } catch (error) { control.error(error.message); + } finally { + this.unlock() } if (isFastForward) { diff --git a/js/explaingit.js b/js/explaingit.js index f48fbd1..4d70937 100644 --- a/js/explaingit.js +++ b/js/explaingit.js @@ -37,7 +37,8 @@ define(['historyview', 'controlbox', 'd3'], function(HistoryView, ControlBox, d3 controlBox = new ControlBox({ historyView: historyView, originView: originView, - initialMessage: args.initialMessage + initialMessage: args.initialMessage, + undoHistory: args.undoHistory }); window.cb = controlBox; diff --git a/js/historyview.js b/js/historyview.js index 3089e93..55bdb76 100644 --- a/js/historyview.js +++ b/js/historyview.js @@ -270,6 +270,15 @@ define(['d3'], function() { cx: -(this.commitRadius * 2), cy: this.baseLine }; + + this.locks = 0 + this._eventCallbacks = {} + + if (config.savedState) { + setTimeout(function() { + this.deserialize(config.savedState) + }.bind(this)) + } } HistoryView.generateId = function() { @@ -277,6 +286,71 @@ define(['d3'], function() { }; HistoryView.prototype = { + serialize: function () { + var data = { + commitData: this.commitData, + branches: this.branches, + logs: this.logs, + currentBranch: this.currentBranch, + } + + return JSON.stringify(data) + }, + + deserialize: function (data) { + data = JSON.parse(data) + this.commitData = data.commitData + this.branches = data.branches + this.logs = data.logs + this.currentBranch = data.currentBranch + this.renderCommits() + this.renderTags() + }, + + emit: function (event) { + var callbacks = this._eventCallbacks[event] || [] + callbacks.forEach(function(callback) { + try { + callback(event) + } finally { + // nothing + } + }) + }, + + on: function (event, callback) { + var callbacks = this._eventCallbacks[event] || [] + callbacks.push(callback) + this._eventCallbacks[event] = callbacks + + return function () { + var cbs = this._eventCallbacks[event] || [] + var idx = cbs.indexOf(callback) + if (idx > -1) { + cbs.splice(idx, 1) + this._eventCallbacks[event] = cbs + } + }.bind(this) + }, + + lock: function () { + this.locks++ + if (this.locks === 1) { + this.emit('lock') + } + }, + + unlock: function () { + if (this.locks <= 0) { + throw new Error('cannot unlock! not locked') + } + + this.locks-- + if (this.locks === 0) { + this.emit('unlock') + } + }, + /** * @method getCommit * @param ref {String} the id or a tag name that refers to the commit @@ -581,7 +655,10 @@ define(['d3'], function() { .attr('r', 1) .transition("inflate") .duration(500) - .attr('r', this.commitRadius); + .attr('r', this.commitRadius) + + existingCircles.exit() + .remove() }, @@ -620,6 +697,9 @@ define(['d3'], function() { .transition() .duration(500) .call(fixPointerEndPosition, view); + + existingPointers.exit() + .remove() }, _renderMergePointers: function() { @@ -674,6 +754,9 @@ define(['d3'], function() { points[1] = x2 + ',' + y2; return points.join(' '); }); + + existingPointers.exit() + .remove() }, _renderIdLabels: function() { @@ -703,6 +786,9 @@ define(['d3'], function() { .classed(className, true) .text(getText) .call(fixIdPosition, view, delta); + + existingTexts.exit() + .remove() }, _parseTagData: function() { @@ -857,6 +943,9 @@ define(['d3'], function() { return commit.cx; }); + existingTags.exit() + .remove() + this._markBranchlessCommits(); }, @@ -941,7 +1030,10 @@ define(['d3'], function() { delete ancestors.initial ancestors[refspec] = -1 var commitIds = Object.keys(ancestors) - this.flashProperty(commitIds, 'logging') + this.lock() + this.flashProperty(commitIds, 'logging', function () { + this.unlock() + }) return commitIds.map(function(commitId) { return {commit: this.getCommit(commitId), order: ancestors[commitId]} }, this).sort(function(a,b) { @@ -1001,6 +1093,7 @@ define(['d3'], function() { refs.forEach(function(ref) { var commit = this.getCommit(ref) var message = commit.message || "" + this.lock() this.flashProperty([commit.id], 'cherryPicked', function() { this.commit({cherryPickSource: [commit.id]}, message) var reflogMessage = "cherry-pick: " + message @@ -1012,6 +1105,7 @@ define(['d3'], function() { this.currentBranch, this.getCommit('HEAD').id, reflogMessage ) } + this.unlock() }) }, this) } else { @@ -1020,6 +1114,7 @@ define(['d3'], function() { var message = commit.message || "" var cherryPickSource = this.getNonMainlineCommits(commit.id, mainline) + this.lock() this.flashProperty(cherryPickSource, 'cherryPicked', function() { this.commit({cherryPickSource: cherryPickSource}, message) var reflogMessage = "cherry-pick: " + message @@ -1031,6 +1126,7 @@ define(['d3'], function() { this.currentBranch, this.getCommit('HEAD').id, reflogMessage ) } + this.unlock() }) }, this) } @@ -1232,6 +1328,7 @@ define(['d3'], function() { refs.forEach(function(ref) { var commit = this.getCommit(ref) var message = commit.message || "" + this.lock() this.flashProperty([commit.id], 'reverted', function() { this.commit({revertSource: [commit.id]}, "Revert " + commit.id) var reflogMessage = "revert: " + message @@ -1243,6 +1340,7 @@ define(['d3'], function() { this.currentBranch, this.getCommit('HEAD').id, reflogMessage ) } + this.unlock() }) }, this) } else { @@ -1251,6 +1349,7 @@ define(['d3'], function() { var message = commit.message || "" var revertSource = this.getNonMainlineCommits(commit.id, mainline) + this.lock() this.flashProperty(revertSource, 'reverted', function() { this.commit({revertSource: revertSource}, "Revert " + commit.id) var reflogMessage = "revert: " + message @@ -1262,6 +1361,7 @@ define(['d3'], function() { this.currentBranch, this.getCommit('HEAD').id, reflogMessage ) } + this.unlock() }) }, this) } @@ -1352,6 +1452,7 @@ define(['d3'], function() { return uniqueAncestors[key2] - uniqueAncestors[key1] }) + this.lock() setTimeout(function() { this.flashProperty(commitsToCopy, 'rebased', function() { commitsToCopy.forEach(function(ref) { @@ -1363,6 +1464,7 @@ define(['d3'], function() { if (origBranch) { this.moveTag(origBranch, newHeadCommit.id) this.reset(origBranch) + this._setCurrentBranch(origBranch) this.addReflogEntry( 'HEAD', targetCommit.id, 'rebase finished: returning to resf/heads/' + origBranch ) @@ -1372,6 +1474,7 @@ define(['d3'], function() { ) } this.unsetProperty(commitsToCopy, 'rebased') + this.unlock() }.bind(this), 1000) }) }.bind(this), 1000)