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)