diff --git a/README.md b/README.md
index af2bb5d..e216309 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Type `help` in the command box to see a list of supported operations
`pres()` = Turn on presenter mode
`undo` = Undo the last git command
`redo` = Redo the last undone git command
+`edit` = Make a file edit
`mode` = Change mode (`local` or `remote`)
`clear` = Clear the history pane and reset the visualization
@@ -45,6 +46,8 @@ git reset
git rev_parse
git revert
git tag
+git add
+git stash
```
diff --git a/css/explaingit.css b/css/explaingit.css
index 13f9b22..067f8cc 100644
--- a/css/explaingit.css
+++ b/css/explaingit.css
@@ -88,10 +88,21 @@ span.cmd {
border: 1px dotted #AAA;
position: absolute;
top: 0;
- bottom: 0;
right: 0;
left: 250px;
margin-left: 0;
+ height: 49%;
+}
+
+#ExplainGitZen-Container .ws-container {
+ display: inline-block;
+ border: 1px dotted #AAA;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ left: 250px;
+ margin-left: 5px;
+ height: 49%;
}
#ExplainGitZen-Container .svg-container.remote-container {
@@ -285,6 +296,27 @@ text.message-label {
font-size: 10px;
}
+g.stash > rect {
+ fill: #FFCC66;
+ stroke: #CC9900;
+ stroke-width: 2;
+}
+
+g.curr-ws > rect {
+ fill: #7FC9FF;
+ stroke: #0026FF;
+}
+
+g.index > rect {
+ fill: #CCC;
+ stroke: #888;
+}
+
+g.blob-space > rect {
+ fill: #8ce08c;
+ stroke: #339900;
+}
+
g.branch-tag > rect {
fill: #FFCC66;
stroke: #CC9900;
diff --git a/examples/working_tree.md b/examples/working_tree.md
new file mode 100644
index 0000000..9389b52
--- /dev/null
+++ b/examples/working_tree.md
@@ -0,0 +1,52 @@
+### Make local changes to the working tree
+
+Simulate file edits with the 'edit' command. No filename is required.
+
+```
+edit
+```
+
+### The Stash
+
+The stash a convenient place to store changes that aren't ready for committing.
+To move these files changes to the stash:
+
+```
+git stash
+```
+
+To move these changes back to the working tree:
+
+```
+git stash apply
+```
+OR
+```
+git stash pop
+```
+
+Applying the changes will move the changes back to the working tree and leave a copy in the stash.
+Popping the changes will move the changes back to the working tree but will remove the changes from
+the stash.
+
+### Add files to the index
+
+If files are ready for committing, they can "staged"/"added to the index".
+
+```
+git add -u
+```
+
+The `-u` flag adds all previously tracked files to the staging area
+
+### Remove files from the index
+
+If a file needs to be removed from the staging area, use 'git reset'
+
+```
+git reset --
+```
+
+### Committing
+
+Committing files will remove them from the index and add them to the local repository.
diff --git a/index.html b/index.html
index a87e584..d75e97f 100644
--- a/index.html
+++ b/index.html
@@ -109,9 +109,9 @@
explainGit.reset();
var savedState = null
- if (window.localStorage) {
- savedState = JSON.parse(window.localStorage.getItem('git-viz-snapshot') || 'null')
- }
+ //if (window.localStorage) {
+ // savedState = JSON.parse(window.localStorage.getItem('git-viz-snapshot') || 'null')
+ //}
var initial = Object.assign(copyDemo(lastDemo), {
name: 'Zen',
diff --git a/js/controlbox.js b/js/controlbox.js
index c9761bf..336fa6b 100644
--- a/js/controlbox.js
+++ b/js/controlbox.js
@@ -20,6 +20,7 @@ function(_yargs, d3, demos) {
function ControlBox(config) {
this.historyView = config.historyView;
this.originView = config.originView;
+ this.workspace = config.workspace;
this.initialMessage = config.initialMessage || 'Enter git commands below.';
this._commandHistory = [];
this._currentCommand = -1;
@@ -85,7 +86,6 @@ function(_yargs, d3, demos) {
},
changeMode: function (mode) {
- console.log(mode)
if (mode === 'local' && this.historyView) {
this.mode = 'local'
} else if (mode === 'remote' && this.originView) {
@@ -231,6 +231,7 @@ function(_yargs, d3, demos) {
this.info('pres() = Turn on presenter mode')
this.info('undo = Undo the last git command')
this.info('redo = Redo the last undone git command')
+ this.info('edit = Make a file edit')
this.info('mode = Change mode (`local` or `remote`)')
this.info('clear = Clear the history pane and reset the visualization')
this.info()
@@ -250,6 +251,8 @@ function(_yargs, d3, demos) {
this.info('`git rev_parse`')
this.info('`git revert`')
this.info('`git tag`')
+ this.info('`git add`')
+ this.info('`git stash`')
return
}
@@ -300,6 +303,12 @@ function(_yargs, d3, demos) {
return
}
+ if (entry.toLowerCase() === 'edit') {
+ var workspace = this.workspace;
+ workspace.addBlob(null, workspace.curr_ws);
+ return
+ }
+
if (entry.toLowerCase() === 'clear') {
window.resetVis()
return
@@ -378,6 +387,7 @@ function(_yargs, d3, demos) {
this.getRepoView().amendCommit(opts.m || this.getRepoView().getCommit('head').message)
} else {
this.getRepoView().commit(null, opts.m);
+ workspace.removeAllBlobs(workspace.index);
}
}, function(before, after) {
var reflogMsg = 'commit: ' + msg
@@ -472,25 +482,33 @@ function(_yargs, d3, demos) {
},
checkout: function(args, opts) {
- if (opts.b) {
- if (opts._[0]) {
- this.branch(null, null, opts.b + ' ' + opts._[0])
- } else {
- this.branch(null, null, opts.b)
+ if (args && args[0] === "--") {
+ args.shift();
+ while (args.length > 0) {
+ var arg = args.shift();
+ workspace.moveBlobByName(workspace.curr_ws, undefined, arg);
+ }
+ } else {
+ if (opts.b) {
+ if (opts._[0]) {
+ this.branch(null, null, opts.b + ' ' + opts._[0])
+ } else {
+ this.branch(null, null, opts.b)
+ }
}
- }
- var name = opts.b || opts._[0]
+ var name = opts.b || opts._[0]
- this.transact(function() {
- this.getRepoView().checkout(name);
- }, function(before, after) {
- this.getRepoView().addReflogEntry(
- 'HEAD', after.commit.id,
- 'checkout: moving from ' + before.ref +
- ' to ' + name
- )
- })
+ this.transact(function() {
+ this.getRepoView().checkout(name);
+ }, function(before, after) {
+ this.getRepoView().addReflogEntry(
+ 'HEAD', after.commit.id,
+ 'checkout: moving from ' + before.ref +
+ ' to ' + name
+ )
+ })
+ }
},
tag: function(args) {
@@ -534,33 +552,38 @@ function(_yargs, d3, demos) {
},
reset: function(args) {
+ var unstage = false;
while (args.length > 0) {
var arg = args.shift();
-
- switch (arg) {
- case '--soft':
- this.info(
- 'The "--soft" flag works in real git, but ' +
- 'I am unable to show you how it works in this demo. ' +
- 'So I am just going to show you what "--hard" looks like instead.'
- );
- break;
- case '--mixed':
- this.info(
- 'The "--mixed" flag works in real git, but ' +
- 'I am unable to show you how it works in this demo. ' +
- 'So I am just going to show you what "--hard" looks like instead.'
- );
- break;
- case '--hard':
- this.doReset(args.join(' '));
- args.length = 0;
- break;
- default:
- var remainingArgs = [arg].concat(args);
- args.length = 0;
- this.info('Assuming "--hard".');
- this.doReset(remainingArgs.join(' '));
+ if (unstage) {
+ workspace.moveBlobByName(workspace.index, workspace.curr_ws, arg);
+ } else {
+ switch (arg) {
+ case '--soft':
+ // no resetting of the index or working tree
+ this.doReset(args.join(' '));
+ args.length = 0;
+ break;
+ case '--mixed':
+ // reset just the index
+ this.doReset(args.join(' '));
+ workspace.removeAllBlobs(workspace.index);
+ args.length = 0;
+ break;
+ case '--hard':
+ this.doReset(args.join(' '));
+ // reset the index and the working tree
+ workspace.removeAllBlobs(workspace.curr_ws);
+ workspace.removeAllBlobs(workspace.index);
+ args.length = 0;
+ break;
+ case "HEAD":
+ // unstage the file (move from index to working tree)
+ unstage = true;
+ break;
+ default:
+ this.info("Invalid ref: " + arg);
+ }
}
}
},
@@ -915,6 +938,48 @@ function(_yargs, d3, demos) {
this.info("Real git reflog supports the '" + subcommand +
"' subcommand but this tool only supports 'show' and 'exists'")
}
+ },
+
+ add: function(args) {
+ // Create boxes to visualize working tree, index, stash
+ var workspace = this.workspace;
+ while (args.length > 0) {
+ var arg = args.shift();
+ switch (arg) {
+ case '-u':
+ case '.':
+ workspace.addBlob(workspace.curr_ws, workspace.index, true);
+ break;
+ default:
+ workspace.moveBlobByName(workspace.curr_ws, workspace.index, arg);
+ break;
+ }
+ }
+ return;
+ },
+
+ stash: function(args) {
+ // Create boxes to visualize working tree, index, stash
+ var workspace = this.workspace;
+ var stash_id = "0";
+ if (args && args[0] === "pop") {
+ workspace.addBlob(workspace.stash, workspace.curr_ws);
+ } else if (args && args[0] === "apply") {
+ if (args[1] != undefined) {
+ stash_id = args[1];
+ }
+ workspace.moveBlobByName(workspace.stash, workspace.curr_ws, stash_id, false);
+ } else if (args && args[0] === "drop") {
+ if (args[1] != undefined) {
+ stash_id = args[1];
+ }
+ workspace.removeBlob(workspace.stash, stash_id);
+ } else if (args && args[0] === "clear") {
+ workspace.removeAllBlobs(workspace.stash);
+ } else {
+ workspace.addBlob(workspace.curr_ws, workspace.stash, true);
+ }
+ return;
}
};
diff --git a/js/explaingit.js b/js/explaingit.js
index 8aa89d5..bd4e3b6 100644
--- a/js/explaingit.js
+++ b/js/explaingit.js
@@ -1,4 +1,5 @@
-define(['historyview', 'controlbox', 'd3'], function(HistoryView, ControlBox, d3) {
+define(['historyview', 'controlbox', 'workspace', 'd3'], function(HistoryView,
+ControlBox, Workspace, d3) {
var prefix = 'ExplainGit',
openSandBoxes = [],
open,
@@ -36,9 +37,20 @@ define(['historyview', 'controlbox', 'd3'], function(HistoryView, ControlBox, d3
window.ov = originView;
}
+ workspace = new Workspace({
+ historyView: historyView,
+ originView: originView,
+ undoHistory: args.undoHistory,
+ name: name + '-Workspace',
+ width: 300,
+ height: 400
+ });
+ window.ws = workspace
+
controlBox = new ControlBox({
historyView: historyView,
originView: originView,
+ workspace: workspace,
initialMessage: args.initialMessage,
undoHistory: args.undoHistory
});
@@ -46,10 +58,12 @@ define(['historyview', 'controlbox', 'd3'], function(HistoryView, ControlBox, d3
controlBox.render(playground);
historyView.render(playground);
+ workspace.render(playground);
openSandBoxes.push({
hv: historyView,
cb: controlBox,
+ ws: workspace,
container: container
});
};
@@ -59,6 +73,7 @@ define(['historyview', 'controlbox', 'd3'], function(HistoryView, ControlBox, d3
var osb = openSandBoxes[i];
osb.hv.destroy();
osb.cb.destroy();
+ osb.ws.destroy();
osb.container.style('display', 'none');
}
@@ -69,6 +84,7 @@ define(['historyview', 'controlbox', 'd3'], function(HistoryView, ControlBox, d3
explainGit = {
HistoryView: HistoryView,
ControlBox: ControlBox,
+ Workspace: Workspace,
generateId: HistoryView.generateId,
open: open,
reset: reset
diff --git a/js/workspace.js b/js/workspace.js
new file mode 100644
index 0000000..98be4e8
--- /dev/null
+++ b/js/workspace.js
@@ -0,0 +1,350 @@
+define(['historyview', 'd3'], function(HistoryView) {
+ "use strict";
+
+ var fixBlobPosition,
+ fixIdPosition, tagY, getUniqueSetItems;
+
+ fixBlobPosition = function(selection) {
+ selection
+ .attr('x', function(d) {
+ return d.x;
+ })
+ .attr('y', function(d) {
+ return d.y;
+ });
+ };
+
+ fixIdPosition = function(selection, view, delta) {
+ selection.attr('x', function(d) {
+ return d.x + view.blob_width / 2;
+ }).attr('y', function(d) {
+ return d.y + view.blob_height + delta;
+ });
+ };
+
+ tagY = function tagY(t, view) {
+ var blobs = view.getBlobs,
+ blobIndex = blobs.indexOf(t.blob);
+
+ if (blobIndex === -1) {
+ blobIndex = blobs.length;
+ }
+ return t.blob.y - 45 - (blobIndex * 25);
+ };
+
+ getUniqueSetItems = function(set1, set2) {
+ var uniqueSet1 = JSON.parse(JSON.stringify(set1))
+ var uniqueSet2 = JSON.parse(JSON.stringify(set2))
+ for (var id in set1) {
+ delete uniqueSet2[id]
+ }
+ for (var id in set2) {
+ delete uniqueSet1[id]
+ }
+ return [uniqueSet1, uniqueSet2]
+ };
+
+ /**
+ * @class Workspace
+ * @constructor
+ */
+ function Workspace(config) {
+ this.historyView = config.historyView;
+ this.originView = config.originView;
+ this.name = config.name || 'UnnamedWorkspace';
+ this.width = config.width;
+ this.height = config.height || 400;
+ //this.width = this.historyView.width;
+ //this.height = this.historyView.height || 400;
+ this.blob_height = config.blob_height || 75;
+ this.blob_width = config.blob_height || 200;
+ this.filename_counter = 0;
+ }
+
+ Workspace.prototype = {
+ /**
+ * @method render
+ * @param container {String} selector for the container to render the SVG into
+ */
+ render: function(container) {
+ var svgContainer, svg, curr_ws, stash, index;
+
+ //svgContainer = container.select('svg-container');
+ svgContainer = container.append('div')
+ .classed('ws-container', true);
+
+ svg = svgContainer.append('svg:svg');
+
+ svg.attr('id', this.name)
+ .attr('width', "100%")
+ .attr('height', "100%");
+ //.attr('width', this.width)
+ //.attr('height', this.height);
+ var labelX = 15;
+ var labelY = 25;
+
+
+ stash = svg.append('svg:g').classed('stash', true).attr('id', 'stash')
+ stash.append('svg:rect')
+ .attr('width', "31%")
+ .attr('height', "100%")
+ .attr('x', 0)
+ .attr('y', 0);
+ stash.append('svg:text')
+ .classed('workspace-label', true)
+ .text('Stash')
+ .attr('x', labelX)
+ .attr('y', labelY);
+ stash.append('svg:g').classed('blob-space', true).attr('id', 'stash.blob-space');
+
+ curr_ws = svg.append('svg:g').classed('curr-ws', true)
+ .attr('id', 'curr_ws')
+ .attr('transform', 'translate(750, 0)');
+ curr_ws.append('svg:rect')
+ .attr('width', "31%")
+ .attr('height', "100%")
+ .attr('x', 0)
+ .attr('y', 0);
+ curr_ws.append('svg:text')
+ .classed('workspace-label', true)
+ .text('Workspace/Working Tree')
+ .attr('x', labelX)
+ .attr('y', labelY);
+ curr_ws.append('svg:g').classed('blob-space', true).attr('id', 'curr_ws.blob-space');
+
+ index = svg.append('svg:g').classed('index', true)
+ .attr('id', 'index')
+ .attr('transform', 'translate(1500, 0)');
+ index.append('svg:rect')
+ .attr('width', "31%")
+ .attr('height', "100%")
+ .attr('x', 0)
+ .attr('y', 0);
+ index.append('svg:text')
+ .classed('workspace-label', true)
+ .text('Index/Stage')
+ .attr('x', labelX)
+ .attr('y', labelY);
+ index.append('svg:g').classed('blob-space', true).attr('id', 'index.blob-space');
+
+ this.svgContainer = svgContainer;
+ this.svg = svg;
+ this.curr_ws = curr_ws;
+ this.curr_ws.name = "workspace"
+ this.curr_ws.blobs = this.curr_ws.blobs || []
+ this.stash = stash
+ this.stash.name = "stash"
+ this.stash.blobs = this.stash.blobs || []
+ this.index = index
+ this.index.name = "index"
+ this.index.blobs = this.index.blobs || []
+
+ this.renderBlobs();
+ },
+
+ destroy: function() {
+ this.svg.remove();
+ this.svgContainer.remove();
+ clearInterval(this.refreshSizeTimer);
+
+ for (var prop in this) {
+ if (this.hasOwnProperty(prop)) {
+ this[prop] = null;
+ }
+ }
+ },
+
+ addNewBlob: function(ws) {
+ if (ws.blobs === undefined || !ws.blobs) {
+ ws.blobs = [];
+ }
+ var blob = {'id': HistoryView.generateId(),
+ 'x': 50,
+ 'y': 50,
+ 'filename': 'file_' + this.filename_counter}
+ this.filename_counter += 1;
+ ws.blobs.push(blob);
+ },
+
+ addBlob: function(src, dst, moveAll=false) {
+ if (src === null) {
+ // Adding a brand new blob
+ this.addNewBlob(dst);
+ } else {
+ // Moving an existing blob from 'src' to 'dst'
+ if (src.blobs !== undefined && src.blobs.length > 0) {
+ if (moveAll) {
+ if (dst.name === "stash") {
+ // stash is treated like a stack of changesets (group of blobs)
+ dst.blobs.unshift(src.blobs);
+ } else {
+ dst.blobs = dst.blobs.concat(src.blobs);
+ }
+ // empty out the src blobs
+ src.blobs = [];
+ } else {
+ if (dst.blobs === undefined) {
+ dst.blobs = [];
+ }
+ if (src.name === "stash") {
+ var top_blob = src.blobs.shift();
+ } else {
+ var top_blob = src.blobs.pop();
+ }
+ if (Array.isArray(top_blob)) {
+ dst.blobs = dst.blobs.concat(top_blob);
+ } else {
+ dst.blobs.push(top_blob);
+ }
+ }
+ }
+ }
+ this.renderBlobs();
+ },
+
+ moveBlobByName: function(src, dst, filename, remove_from_src=true) {
+ // set dst to undefined to remove the blob
+ var target_blob = src.blobs.filter(function(d) {
+ if (src.name === "stash") {
+ return d.filename.split("{")[1].split("}")[0] === filename;
+ } else {
+ return d.filename === filename;
+ }
+ });
+ if (target_blob && target_blob.length == 1) {
+ if (remove_from_src) {
+ src.blobs.splice(src.blobs.indexOf(target_blob[0]), 1);
+ }
+ // add to dst
+ if (dst) {
+ if (Array.isArray(target_blob[0])) {
+ // ensure there's no duplicates after a 'stash apply'
+ dst.blobs = Array.from(new Set(dst.blobs.concat(target_blob[0])));
+ } else {
+ dst.blobs.push(target_blob[0]);
+ }
+ }
+ }
+ this.renderBlobs();
+ },
+
+ removeBlob: function(ws, index_to_remove) {
+ ws.blobs.splice(index_to_remove, 1);
+ this.renderBlobs();
+ },
+
+ removeAllBlobs: function(ws) {
+ ws.blobs = []
+ this.renderBlobs();
+ },
+
+ _calculatePositionData: function(blobs) {
+ for (var i = 0; i < blobs.length; i++) {
+ var blob = blobs[i];
+ blob.x = 50;
+ blob.y = 50 + i * 125;
+ }
+ },
+
+ _resizeSvg: function() {
+ var ele = document.getElementById(this.svg.node().id);
+ var container = ele.parentNode;
+ var currentWidth = ele.offsetWidth;
+ var newWidth;
+
+ if (ele.getBBox().width > container.offsetWidth)
+ newWidth = Math.round(ele.getBBox().width);
+ else
+ newWidth = container.offsetWidth - 5;
+
+ if (currentWidth != newWidth) {
+ this.svg.attr('width', newWidth);
+ container.scrollLeft = container.scrollWidth;
+ }
+ },
+
+ _renderWorkspace: function(view, ws, blob_type) {
+ // Bind the data
+ var blob_rect = ws.select("g.blob-space").selectAll("rect").data(ws.blobs);
+ // Enter
+ blob_rect.enter().append("svg:rect")
+ .attr("id", function(d) { return blob_type + "-" + d.id; })
+ .classed("rendered-" + blob_type, true)
+ .attr("width", 1)
+ .attr("height", 1)
+ .transition("inflate")
+ .attr("width", view.blob_width)
+ .attr("height", view.blob_height)
+ .duration(500);
+ // Update
+ view._calculatePositionData(ws.blobs);
+ blob_rect
+ .call(fixBlobPosition);
+ // Remove
+ blob_rect.exit().remove();
+ view._renderIdLabels(ws);
+ },
+
+ renderBlobs: function() {
+ var view = this,
+ existingBlobs,
+ newBlobs,
+ curr_workspace = this.stash,
+ workspaces = [this.curr_ws, this.index],
+ changeset_workspaces = [this.stash];
+
+ workspaces.forEach(function(ws) {
+ view._renderWorkspace(view, ws, "blob");
+ });
+ changeset_workspaces.forEach(function(ws) {
+ view._renderWorkspace(view, ws, "changeset");
+ });
+ },
+
+ _renderIdLabels: function(ws) {
+ this._renderText(ws, 'id-label', function(d) {
+ if (Array.isArray(d)) {
+ var ret_str = "";
+ d.forEach( function(blob) {
+ ret_str += blob.id + ',';
+ });
+ ret_str = ret_str.substr(0, ret_str.length - 1);
+ return ret_str.substr(0, 32);
+ }
+ return d.id + '..';
+ }, 14);
+ this._renderText(ws, 'message-label', function(d) {
+ if (ws.name === "stash") {
+ var filename = "stash@{" + ws.blobs.indexOf(d) + "}";
+ // save the stash name in the changeset object
+ d.filename = filename;
+ return filename;
+ }
+ return d.filename;
+ }, 24);
+ },
+
+ _renderText: function(ws, className, getText, delta) {
+ var view = this,
+ existingTexts,
+ newtexts;
+
+ existingTexts = ws.select("g.blob-space").selectAll('text.' + className)
+ .data(ws.blobs)
+ .text(getText);
+
+ existingTexts.transition().call(fixIdPosition, view, delta);
+
+ newtexts = existingTexts.enter()
+ .insert('svg:text', ':first-child')
+ .classed(className, true)
+ .text(getText)
+ .call(fixIdPosition, view, delta);
+
+ existingTexts.exit()
+ .remove()
+ },
+ };
+
+ return Workspace;
+});