(function(){
var require = function (file, cwd) {
var resolved = require.resolve(file, cwd || '/');
var mod = require.modules[resolved];
if (!mod) throw new Error(
'Failed to resolve module ' + file + ', tried ' + resolved
);
var cached = require.cache[resolved];
var res = cached? cached.exports : mod();
return res;
};
require.paths = [];
require.modules = {};
require.cache = {};
require.extensions = [".js",".coffee",".json"];
require._core = {
'assert': true,
'events': true,
'fs': true,
'path': true,
'vm': true
};
require.resolve = (function () {
return function (x, cwd) {
if (!cwd) cwd = '/';
if (require._core[x]) return x;
var path = require.modules.path();
cwd = path.resolve('/', cwd);
var y = cwd || '/';
if (x.match(/^(?:\.\.?\/|\/)/)) {
var m = loadAsFileSync(path.resolve(y, x))
|| loadAsDirectorySync(path.resolve(y, x));
if (m) return m;
}
var n = loadNodeModulesSync(x, y);
if (n) return n;
throw new Error("Cannot find module '" + x + "'");
function loadAsFileSync (x) {
x = path.normalize(x);
if (require.modules[x]) {
return x;
}
for (var i = 0; i < require.extensions.length; i++) {
var ext = require.extensions[i];
if (require.modules[x + ext]) return x + ext;
}
}
function loadAsDirectorySync (x) {
x = x.replace(/\/+$/, '');
var pkgfile = path.normalize(x + '/package.json');
if (require.modules[pkgfile]) {
var pkg = require.modules[pkgfile]();
var b = pkg.browserify;
if (typeof b === 'object' && b.main) {
var m = loadAsFileSync(path.resolve(x, b.main));
if (m) return m;
}
else if (typeof b === 'string') {
var m = loadAsFileSync(path.resolve(x, b));
if (m) return m;
}
else if (pkg.main) {
var m = loadAsFileSync(path.resolve(x, pkg.main));
if (m) return m;
}
}
return loadAsFileSync(x + '/index');
}
function loadNodeModulesSync (x, start) {
var dirs = nodeModulesPathsSync(start);
for (var i = 0; i < dirs.length; i++) {
var dir = dirs[i];
var m = loadAsFileSync(dir + '/' + x);
if (m) return m;
var n = loadAsDirectorySync(dir + '/' + x);
if (n) return n;
}
var m = loadAsFileSync(x);
if (m) return m;
}
function nodeModulesPathsSync (start) {
var parts;
if (start === '/') parts = [ '' ];
else parts = path.normalize(start).split('/');
var dirs = [];
for (var i = parts.length - 1; i >= 0; i--) {
if (parts[i] === 'node_modules') continue;
var dir = parts.slice(0, i + 1).join('/') + '/node_modules';
dirs.push(dir);
}
return dirs;
}
};
})();
require.alias = function (from, to) {
var path = require.modules.path();
var res = null;
try {
res = require.resolve(from + '/package.json', '/');
}
catch (err) {
res = require.resolve(from, '/');
}
var basedir = path.dirname(res);
var keys = (Object.keys || function (obj) {
var res = [];
for (var key in obj) res.push(key);
return res;
})(require.modules);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key.slice(0, basedir.length + 1) === basedir + '/') {
var f = key.slice(basedir.length);
require.modules[to + f] = require.modules[basedir + f];
}
else if (key === basedir) {
require.modules[to] = require.modules[basedir];
}
}
};
(function () {
var process = {};
var global = typeof window !== 'undefined' ? window : {};
var definedProcess = false;
require.define = function (filename, fn) {
if (!definedProcess && require.modules.__browserify_process) {
process = require.modules.__browserify_process();
definedProcess = true;
}
var dirname = require._core[filename]
? ''
: require.modules.path().dirname(filename)
;
var require_ = function (file) {
var requiredModule = require(file, dirname);
var cached = require.cache[require.resolve(file, dirname)];
if (cached && cached.parent === null) {
cached.parent = module_;
}
return requiredModule;
};
require_.resolve = function (name) {
return require.resolve(name, dirname);
};
require_.modules = require.modules;
require_.define = require.define;
require_.cache = require.cache;
var module_ = {
id : filename,
filename: filename,
exports : {},
loaded : false,
parent: null
};
require.modules[filename] = function () {
require.cache[filename] = module_;
fn.call(
module_.exports,
require_,
module_,
module_.exports,
dirname,
filename,
process,
global
);
module_.loaded = true;
return module_.exports;
};
};
})();
require.define("path",function(require,module,exports,__dirname,__filename,process,global){function filter (xs, fn) {
var res = [];
for (var i = 0; i < xs.length; i++) {
if (fn(xs[i], i, xs)) res.push(xs[i]);
}
return res;
}
// resolves . and .. elements in a path array with directory names there
// must be no slashes, empty elements, or device names (c:\) in the array
// (so also no leading and trailing slashes - it does not distinguish
// relative and absolute paths)
function normalizeArray(parts, allowAboveRoot) {
// if the path tries to go above the root, `up` ends up > 0
var up = 0;
for (var i = parts.length; i >= 0; i--) {
var last = parts[i];
if (last == '.') {
parts.splice(i, 1);
} else if (last === '..') {
parts.splice(i, 1);
up++;
} else if (up) {
parts.splice(i, 1);
up--;
}
}
// if the path is allowed to go above the root, restore leading ..s
if (allowAboveRoot) {
for (; up--; up) {
parts.unshift('..');
}
}
return parts;
}
// Regex to split a filename into [*, dir, basename, ext]
// posix version
var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/;
// path.resolve([from ...], to)
// posix version
exports.resolve = function() {
var resolvedPath = '',
resolvedAbsolute = false;
for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) {
var path = (i >= 0)
? arguments[i]
: process.cwd();
// Skip empty and invalid entries
if (typeof path !== 'string' || !path) {
continue;
}
resolvedPath = path + '/' + resolvedPath;
resolvedAbsolute = path.charAt(0) === '/';
}
// At this point the path should be resolved to a full absolute path, but
// handle relative paths to be safe (might happen when process.cwd() fails)
// Normalize the path
resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
return !!p;
}), !resolvedAbsolute).join('/');
return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
};
// path.normalize(path)
// posix version
exports.normalize = function(path) {
var isAbsolute = path.charAt(0) === '/',
trailingSlash = path.slice(-1) === '/';
// Normalize the path
path = normalizeArray(filter(path.split('/'), function(p) {
return !!p;
}), !isAbsolute).join('/');
if (!path && !isAbsolute) {
path = '.';
}
if (path && trailingSlash) {
path += '/';
}
return (isAbsolute ? '/' : '') + path;
};
// posix version
exports.join = function() {
var paths = Array.prototype.slice.call(arguments, 0);
return exports.normalize(filter(paths, function(p, index) {
return p && typeof p === 'string';
}).join('/'));
};
exports.dirname = function(path) {
var dir = splitPathRe.exec(path)[1] || '';
var isWindows = false;
if (!dir) {
// No dirname
return '.';
} else if (dir.length === 1 ||
(isWindows && dir.length <= 3 && dir.charAt(1) === ':')) {
// It is just a slash or a drive letter with a slash
return dir;
} else {
// It is a full dirname, strip trailing slash
return dir.substring(0, dir.length - 1);
}
};
exports.basename = function(path, ext) {
var f = splitPathRe.exec(path)[2] || '';
// TODO: make this comparison case-insensitive on windows?
if (ext && f.substr(-1 * ext.length) === ext) {
f = f.substr(0, f.length - ext.length);
}
return f;
};
exports.extname = function(path) {
return splitPathRe.exec(path)[3] || '';
};
});
require.define("__browserify_process",function(require,module,exports,__dirname,__filename,process,global){var process = module.exports = {};
process.nextTick = (function () {
var canSetImmediate = typeof window !== 'undefined'
&& window.setImmediate;
var canPost = typeof window !== 'undefined'
&& window.postMessage && window.addEventListener
;
if (canSetImmediate) {
return function (f) { return window.setImmediate(f) };
}
if (canPost) {
var queue = [];
window.addEventListener('message', function (ev) {
if (ev.source === window && ev.data === 'browserify-tick') {
ev.stopPropagation();
if (queue.length > 0) {
var fn = queue.shift();
fn();
}
}
}, true);
return function nextTick(fn) {
queue.push(fn);
window.postMessage('browserify-tick', '*');
};
}
return function nextTick(fn) {
setTimeout(fn, 0);
};
})();
process.title = 'browser';
process.browser = true;
process.env = {};
process.argv = [];
process.binding = function (name) {
if (name === 'evals') return (require)('vm')
else throw new Error('No such module. (Possibly not yet loaded)')
};
(function () {
var cwd = '/';
var path;
process.cwd = function () { return cwd };
process.chdir = function (dir) {
if (!path) path = require('path');
cwd = path.resolve(dir, cwd);
};
})();
});
require.define("/animation/index.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.define("/constants/index.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.define("/visuals/index.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('../app');
var GRAPHICS = require('../constants').GRAPHICS;
var GLOBAL = require('../constants').GLOBAL;
var Collections = require('../collections');
var CommitCollection = Collections.CommitCollection;
var BranchCollection = Collections.BranchCollection;
var Tree = require('../visuals/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;
new 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("/app/index.js",function(require,module,exports,__dirname,__filename,process,global){/**
* Globals
*/
var events = _.clone(Backbone.Events);
var ui = null;
var mainVis = null;
///////////////////////////////////////////////////////////////////////
$(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('../views/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.define("/collections/index.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('../git').Commit;
var Branch = require('../git').Branch;
var Main = require('../app');
var Command = require('../models/commandModel').Command;
var CommandEntry = require('../models/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('../app').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/index.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactoryModule = require('../animation/animationFactory');
var animationFactory = new AnimationFactoryModule.AnimationFactory();
var Main = require('../app');
var AnimationQueue = require('../animation').AnimationQueue;
var InteractiveRebaseView = require('../views/miscViews').InteractiveRebaseView;
var Errors = require('../errors');
var GitError = Errors.GitError;
var CommandResult = Errors.CommandResult;
// backbone or something uses _.uniqueId, so we make our own here
var uniqueId = (function() {
var n = 0;
return function(prepend) {
return prepend? prepend + n++ : n++;
};
})();
function GitEngine(options) {
this.rootCommit = null;
this.refs = {};
this.HEAD = null;
this.branchCollection = options.branches;
this.commitCollection = options.collection;
this.gitVisuals = options.gitVisuals;
// global variable to keep track of the options given
// along with the command call.
this.commandOptions = {};
this.generalArgs = [];
Main.getEvents().on('processCommand', _.bind(this.dispatch, this));
}
GitEngine.prototype.defaultInit = function() {
var defaultTree = JSON.parse(unescape("%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22master%22%2C%22type%22%3A%22general%20ref%22%7D%7D"));
this.loadTree(defaultTree);
};
GitEngine.prototype.init = function() {
// make an initial commit and a master branch
this.rootCommit = this.makeCommit(null, null, {rootCommit: true});
this.commitCollection.add(this.rootCommit);
var master = this.makeBranch('master', this.rootCommit);
this.HEAD = new Ref({
id: 'HEAD',
target: master
});
this.refs[this.HEAD.get('id')] = this.HEAD;
// commit once to get things going
this.commit();
};
GitEngine.prototype.exportTree = function() {
// need to export all commits, their connectivity / messages, branches, and state of head.
// this would be simple if didn't have circular structures.... :P
// thus, we need to loop through and "flatten" our graph of objects referencing one another
var totalExport = {
branches: {},
commits: {},
HEAD: null
};
_.each(this.branchCollection.toJSON(), function(branch) {
branch.target = branch.target.get('id');
branch.visBranch = undefined;
totalExport.branches[branch.id] = branch;
});
_.each(this.commitCollection.toJSON(), function(commit) {
// clear out the fields that reference objects and create circular structure
_.each(Commit.prototype.constants.circularFields, function(field) {
commit[field] = undefined;
}, this);
// convert parents
var parents = [];
_.each(commit.parents, function(par) {
parents.push(par.get('id'));
});
commit.parents = parents;
totalExport.commits[commit.id] = commit;
}, this);
var HEAD = this.HEAD.toJSON();
HEAD.visBranch = undefined;
HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = undefined;
HEAD.target = HEAD.target.get('id');
totalExport.HEAD = HEAD;
return totalExport;
};
GitEngine.prototype.printTree = function() {
var str = escape(JSON.stringify(this.exportTree()));
return str;
};
GitEngine.prototype.printAndCopyTree = function() {
window.prompt('Copy the tree string below', this.printTree());
};
GitEngine.prototype.loadTree = function(tree) {
// deep copy in case we use it a bunch
tree = $.extend(true, {}, tree);
// first clear everything
this.removeAll();
this.instantiateFromTree(tree);
this.reloadGraphics();
};
GitEngine.prototype.loadTreeFromString = function(treeString) {
this.loadTree(JSON.parse(unescape(treeString)));
};
GitEngine.prototype.instantiateFromTree = function(tree) {
// now we do the loading part
var createdSoFar = {};
_.each(tree.commits, function(commitJSON) {
var commit = this.getOrMakeRecursive(tree, createdSoFar, commitJSON.id);
this.commitCollection.add(commit);
}, this);
_.each(tree.branches, function(branchJSON) {
var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id);
this.branchCollection.add(branch, {silent: true});
}, this);
var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id);
this.HEAD = HEAD;
this.rootCommit = createdSoFar['C0'];
if (!this.rootCommit) {
throw new Error('Need root commit of C0 for calculations');
}
this.refs = createdSoFar;
this.branchCollection.each(function(branch) {
this.gitVisuals.addBranch(branch);
}, this);
};
GitEngine.prototype.reloadGraphics = function() {
// get the root commit, no better way to do it
var rootCommit = null;
this.commitCollection.each(function(commit) {
if (commit.get('id') == 'C0') {
rootCommit = commit;
}
});
this.gitVisuals.rootCommit = rootCommit;
// this just basically makes the HEAD branch. the head branch really should have been
// a member of a collection and not this annoying edge case stuff... one day
this.gitVisuals.initHeadBranch();
// when the paper is ready
this.gitVisuals.drawTreeFromReload();
this.gitVisuals.refreshTreeHarsh();
};
GitEngine.prototype.getOrMakeRecursive = function(tree, createdSoFar, objID) {
if (createdSoFar[objID]) {
// base case
return createdSoFar[objID];
}
var getType = function(tree, id) {
if (tree.commits[id]) {
return 'commit';
} else if (tree.branches[id]) {
return 'branch';
} else if (id == 'HEAD') {
return 'HEAD';
}
throw new Error("bad type for " + id);
};
// figure out what type
var type = getType(tree, objID);
if (type == 'HEAD') {
var headJSON = tree.HEAD;
var HEAD = new Ref(_.extend(
tree.HEAD,
{
target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target)
}
));
createdSoFar[objID] = HEAD;
return HEAD;
}
if (type == 'branch') {
var branchJSON = tree.branches[objID];
var branch = new Branch(_.extend(
tree.branches[objID],
{
target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target)
}
));
createdSoFar[objID] = branch;
return branch;
}
if (type == 'commit') {
// for commits, we need to grab all the parents
var commitJSON = tree.commits[objID];
var parentObjs = [];
_.each(commitJSON.parents, function(parentID) {
parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID));
}, this);
var commit = new Commit(_.extend(
commitJSON,
{
parents: parentObjs,
gitVisuals: this.gitVisuals
}
));
createdSoFar[objID] = commit;
return commit;
}
throw new Error('ruh rho!! unsupported tyep for ' + objID);
};
GitEngine.prototype.removeAll = function() {
this.branchCollection.reset();
this.commitCollection.reset();
this.refs = {};
this.HEAD = null;
this.rootCommit = null;
this.gitVisuals.resetAll();
};
GitEngine.prototype.getDetachedHead = function() {
// detached head is if HEAD points to a commit instead of a branch...
var target = this.HEAD.get('target');
var targetType = target.get('type');
return targetType !== 'branch';
};
GitEngine.prototype.validateBranchName = function(name) {
name = name.replace(/\s/g, '');
if (!/^[a-zA-Z0-9]+$/.test(name)) {
throw new GitError({
msg: 'woah bad branch name!! This is not ok: ' + name
});
}
if (/[hH][eE][aA][dD]/.test(name)) {
throw new GitError({
msg: 'branch name of "head" is ambiguous, dont name it that'
});
}
if (name.length > 9) {
name = name.slice(0, 9);
this.command.addWarning(
'Sorry, we need to keep branch names short for the visuals. Your branch ' +
'name was truncated to 9 characters, resulting in ' + name
);
}
return name;
};
GitEngine.prototype.makeBranch = function(id, target) {
id = this.validateBranchName(id);
if (this.refs[id]) {
throw new GitError({
msg: 'that branch id either matches a commit hash or already exists!'
});
}
var branch = new Branch({
target: target,
id: id
});
this.branchCollection.add(branch);
this.refs[branch.get('id')] = branch;
return branch;
};
GitEngine.prototype.getHead = function() {
return _.clone(this.HEAD);
};
GitEngine.prototype.getBranches = function() {
var toReturn = [];
this.branchCollection.each(function(branch) {
toReturn.push({
id: branch.get('id'),
selected: this.HEAD.get('target') === branch,
target: branch.get('target'),
obj: branch
});
}, this);
return toReturn;
};
GitEngine.prototype.printBranchesWithout = function(without) {
var commitToBranches = this.getUpstreamBranchSet();
var commitID = this.getCommitFromRef(without).get('id');
var toPrint = [];
_.each(commitToBranches[commitID], function(branchJSON) {
branchJSON.selected = this.HEAD.get('target').get('id') == branchJSON.id;
toPrint.push(branchJSON);
}, this);
this.printBranches(toPrint);
};
GitEngine.prototype.printBranches = function(branches) {
var result = '';
_.each(branches, function(branch) {
result += (branch.selected ? '* ' : '') + branch.id + '\n';
});
throw new CommandResult({
msg: result
});
};
GitEngine.prototype.makeCommit = function(parents, id, options) {
// ok we need to actually manually create commit IDs now because
// people like nikita (thanks for finding this!) could
// make branches named C2 before creating the commit C2
if (!id) {
id = uniqueId('C');
while (this.refs[id]) {
id = uniqueId('C');
}
}
var commit = new Commit(_.extend({
parents: parents,
id: id,
gitVisuals: this.gitVisuals
},
options || {}
));
this.refs[commit.get('id')] = commit;
this.commitCollection.add(commit);
return commit;
};
GitEngine.prototype.acceptNoGeneralArgs = function() {
if (this.generalArgs.length) {
throw new GitError({
msg: "That command accepts no general arguments"
});
}
};
GitEngine.prototype.validateArgBounds = function(args, lower, upper, option) {
// this is a little utility class to help arg validation that happens over and over again
var what = (option === undefined) ?
'git ' + this.command.get('method') :
this.command.get('method') + ' ' + option + ' ';
what = 'with ' + what;
if (args.length < lower) {
throw new GitError({
msg: 'I expect at least ' + String(lower) + ' argument(s) ' + what
});
}
if (args.length > upper) {
throw new GitError({
msg: 'I expect at most ' + String(upper) + ' argument(s) ' + what
});
}
};
GitEngine.prototype.oneArgImpliedHead = function(args, option) {
// for log, show, etc
this.validateArgBounds(args, 0, 1, option);
if (args.length === 0) {
args.push('HEAD');
}
};
GitEngine.prototype.twoArgsImpliedHead = function(args, option) {
// our args we expect to be between 1 and 2
this.validateArgBounds(args, 1, 2, option);
// and if it's one, add a HEAD to the back
if (args.length == 1) {
args.push('HEAD');
}
};
GitEngine.prototype.revertStarter = function() {
this.validateArgBounds(this.generalArgs, 1, NaN);
var response = this.revert(this.generalArgs);
if (response) {
animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
}
};
GitEngine.prototype.revert = function(whichCommits) {
// for each commit, we want to revert it
var toRebase = [];
_.each(whichCommits, function(stringRef) {
toRebase.push(this.getCommitFromRef(stringRef));
}, this);
// we animate reverts now!! we use the rebase animation though so that's
// why the terminology is like it is
var animationResponse = {};
animationResponse.destinationBranch = this.resolveID(toRebase[0]);
animationResponse.toRebaseArray = toRebase.slice(0);
animationResponse.rebaseSteps = [];
var beforeSnapshot = this.gitVisuals.genSnapshot();
var afterSnapshot;
// now make a bunch of commits on top of where we are
var base = this.getCommitFromRef('HEAD');
_.each(toRebase, function(oldCommit) {
var newId = this.rebaseAltID(oldCommit.get('id'));
var newCommit = this.makeCommit([base], newId, {
commitMessage: 'Reverting ' + this.resolveName(oldCommit) +
': "' + oldCommit.get('commitMessage') + '"'
});
base = newCommit;
// animation stuff
afterSnapshot = this.gitVisuals.genSnapshot();
animationResponse.rebaseSteps.push({
oldCommit: oldCommit,
newCommit: newCommit,
beforeSnapshot: beforeSnapshot,
afterSnapshot: afterSnapshot
});
beforeSnapshot = afterSnapshot;
}, this);
// done! update our location
this.setTargetLocation('HEAD', base);
// animation
return animationResponse;
};
GitEngine.prototype.resetStarter = function() {
if (this.commandOptions['--soft']) {
throw new GitError({
msg: "You can't use --soft because there is no concept of stashing" +
" changes or staging files, so you will lose your progress." +
" Try using interactive rebasing (or just rebasing) to move commits."
});
}
if (this.commandOptions['--hard']) {
this.command.addWarning(
'Nice! You are using --hard. The default behavior is a hard reset in ' +
"this demo, so don't worry about specifying the option explicity"
);
// dont absorb the arg off of --hard
this.generalArgs = this.generalArgs.concat(this.commandOptions['--hard']);
}
this.validateArgBounds(this.generalArgs, 1, 1);
if (this.getDetachedHead()) {
throw new GitError({
msg: "Cant reset in detached head! Use checkout if you want to move"
});
}
this.reset(this.generalArgs[0]);
};
GitEngine.prototype.reset = function(target) {
this.setTargetLocation('HEAD', this.getCommitFromRef(target));
};
GitEngine.prototype.cherrypickStarter = function() {
this.validateArgBounds(this.generalArgs, 1, 1);
var newCommit = this.cherrypick(this.generalArgs[0]);
animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
};
GitEngine.prototype.cherrypick = function(ref) {
var commit = this.getCommitFromRef(ref);
// check if we already have that
var set = this.getUpstreamSet('HEAD');
if (set[commit.get('id')]) {
throw new GitError({
msg: "We already have that commit in our changes history! You can't cherry-pick it " +
"if it shows up in git log."
});
}
// alter the ID slightly
var id = this.rebaseAltID(commit.get('id'));
// now commit with that id onto HEAD
var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id);
this.setTargetLocation(this.HEAD, newCommit);
return newCommit;
};
GitEngine.prototype.commitStarter = function() {
this.acceptNoGeneralArgs();
if (this.commandOptions['-am'] && (
this.commandOptions['-a'] || this.commandOptions['-m'])) {
throw new GitError({
msg: "You can't have -am with another -m or -a!"
});
}
var msg = null;
var args = null;
if (this.commandOptions['-a']) {
this.command.addWarning('No need to add files in this demo');
}
if (this.commandOptions['-am']) {
args = this.commandOptions['-am'];
this.validateArgBounds(args, 1, 1, '-am');
this.command.addWarning("Don't worry about adding files in this demo. I'll take " +
"down your commit message anyways, but you can commit without a message " +
"in this demo as well");
msg = args[0];
}
if (this.commandOptions['-m']) {
args = this.commandOptions['-m'];
this.validateArgBounds(args, 1, 1, '-m');
msg = args[0];
}
var newCommit = this.commit();
if (msg) {
msg = msg
.replace(/"/g, '"')
.replace(/^"/g, '')
.replace(/"$/g, '');
newCommit.set('commitMessage', msg);
}
animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
};
GitEngine.prototype.commit = function() {
var targetCommit = this.getCommitFromRef(this.HEAD);
var id = null;
// if we want to ammend, go one above
if (this.commandOptions['--amend']) {
targetCommit = this.resolveID('HEAD~1');
id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
}
var newCommit = this.makeCommit([targetCommit], id);
if (this.getDetachedHead()) {
this.command.addWarning('Warning!! Detached HEAD state');
}
this.setTargetLocation(this.HEAD, newCommit);
return newCommit;
};
GitEngine.prototype.resolveName = function(someRef) {
// first get the obj
var obj = this.resolveID(someRef);
if (obj.get('type') == 'commit') {
return 'commit ' + obj.get('id');
}
if (obj.get('type') == 'branch') {
return 'branch "' + obj.get('id') + '"';
}
// we are dealing with HEAD
return this.resolveName(obj.get('target'));
};
GitEngine.prototype.resolveID = function(idOrTarget) {
if (idOrTarget === null || idOrTarget === undefined) {
throw new Error('Dont call this with null / undefined');
}
if (typeof idOrTarget !== 'string') {
return idOrTarget;
}
return this.resolveStringRef(idOrTarget);
};
GitEngine.prototype.resolveStringRef = function(ref) {
if (this.refs[ref]) {
return this.refs[ref];
}
// may be something like HEAD~2 or master^^
var relativeRefs = [
[/^([a-zA-Z0-9]+)~(\d+)\s*$/, function(matches) {
return parseInt(matches[2], 10);
}],
[/^([a-zA-Z0-9]+)(\^+)\s*$/, function(matches) {
return matches[2].length;
}]
];
var startRef = null;
var numBack = null;
_.each(relativeRefs, function(config) {
var regex = config[0];
var parse = config[1];
if (regex.test(ref)) {
var matches = regex.exec(ref);
numBack = parse(matches);
startRef = matches[1];
}
}, this);
if (!startRef) {
throw new GitError({
msg: 'unknown ref ' + ref
});
}
if (!this.refs[startRef]) {
throw new GitError({
msg: 'the ref ' + startRef +' does not exist.'
});
}
var commit = this.getCommitFromRef(startRef);
return this.numBackFrom(commit, numBack);
};
GitEngine.prototype.getCommitFromRef = function(ref) {
var start = this.resolveID(ref);
// works for both HEAD and just a single layer. aka branch
while (start.get('type') !== 'commit') {
start = start.get('target');
}
return start;
};
GitEngine.prototype.getType = function(ref) {
return this.resolveID(ref).get('type');
};
GitEngine.prototype.setTargetLocation = function(ref, target) {
if (this.getType(ref) == 'commit') {
// nothing to do
return;
}
// sets whatever ref is (branch, HEAD, etc) to a target. so if
// you pass in HEAD, and HEAD is pointing to a branch, it will update
// the branch to that commit, not the HEAD
ref = this.getOneBeforeCommit(ref);
ref.set('target', target);
};
GitEngine.prototype.getUpstreamBranchSet = function() {
// this is expensive!! so only call once in a while
var commitToSet = {};
var inArray = function(arr, id) {
var found = false;
_.each(arr, function(wrapper) {
if (wrapper.id == id) {
found = true;
}
});
return found;
};
var bfsSearch = function(commit) {
var set = [];
var pQueue = [commit];
while (pQueue.length) {
var popped = pQueue.pop();
set.push(popped.get('id'));
if (popped.get('parents') && popped.get('parents').length) {
pQueue = pQueue.concat(popped.get('parents'));
}
}
return set;
};
this.branchCollection.each(function(branch) {
var set = bfsSearch(branch.get('target'));
_.each(set, function(id) {
commitToSet[id] = commitToSet[id] || [];
// only add it if it's not there, so hue blending is ok
if (!inArray(commitToSet[id], branch.get('id'))) {
commitToSet[id].push({
obj: branch,
id: branch.get('id')
});
}
});
});
return commitToSet;
};
GitEngine.prototype.getUpstreamHeadSet = function() {
var set = this.getUpstreamSet('HEAD');
var including = this.getCommitFromRef('HEAD').get('id');
set[including] = true;
return set;
};
GitEngine.prototype.getOneBeforeCommit = function(ref) {
// you can call this command on HEAD in detached, HEAD, or on a branch
// and it will return the ref that is one above a commit. aka
// it resolves HEAD to something that we can move the ref with
var start = this.resolveID(ref);
if (start === this.HEAD && !this.getDetachedHead()) {
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:
//
// * merge master into new
// |\
// | \* commit here
// |* \ commit there
// | |* commit here
// \ /
// | * root
//
//
// hence we need to do a BFS search, with the commit date being the
// value to sort off of (rather than just purely the level)
if (numBack === 0) {
return commit;
}
// we use a special sorting function here that
// prefers the later commits over the earlier ones
var sortQueue = _.bind(function(queue) {
queue.sort(this.idSortFunc);
queue.reverse();
}, this);
var pQueue = [].concat(commit.get('parents') || []);
sortQueue(pQueue);
numBack--;
while (pQueue.length && numBack !== 0) {
var popped = pQueue.shift(0);
var parents = popped.get('parents');
if (parents && parents.length) {
pQueue = pQueue.concat(parents);
}
sortQueue(pQueue);
numBack--;
}
if (numBack !== 0 || pQueue.length === 0) {
throw new GitError({
msg: "Sorry, I can't go that many commits back"
});
}
return pQueue.shift(0);
};
GitEngine.prototype.scrapeBaseID = function(id) {
var results = /^C(\d+)/.exec(id);
if (!results) {
throw new Error('regex failed on ' + id);
}
return 'C' + results[1];
};
GitEngine.prototype.rebaseAltID = function(id) {
// this function alters an ID to add a quote to the end,
// indicating that it was rebased. it also checks existence
var regexMap = [
[/^C(\d+)[']{0,2}$/, function(bits) {
// this id can use another quote, so just add it
return bits[0] + "'";
}],
[/^C(\d+)[']{3}$/, function(bits) {
// here we switch from C''' to C'^4
return bits[0].slice(0, -3) + "'^4";
}],
[/^C(\d+)['][\^](\d+)$/, function(bits) {
return 'C' + String(bits[1]) + "'^" + String(Number(bits[2]) + 1);
}]
];
for (var i = 0; i < regexMap.length; i++) {
var regex = regexMap[i][0];
var func = regexMap[i][1];
var results = regex.exec(id);
if (results) {
var newId = func(results);
// if this id exists, continue down the rabbit hole
if (this.refs[newId]) {
return this.rebaseAltID(newId);
} else {
return newId;
}
}
}
throw new Error('could not modify the id ' + id);
};
GitEngine.prototype.idSortFunc = function(cA, cB) {
// commit IDs can come in many forms:
// C4
// C4' (from a rebase)
// C4'' (from multiple rebases)
// C4'^3 (from a BUNCH of rebases)
var scale = 1000;
var regexMap = [
[/^C(\d+)$/, function(bits) {
// return the 4 from C4
return scale * bits[1];
}],
[/^C(\d+)([']+)$/, function(bits) {
// return the 4 from C4, plus the length of the quotes
return scale * bits[1] + bits[2].length;
}],
[/^C(\d+)['][\^](\d+)$/, function(bits) {
return scale * bits[1] + Number(bits[2]);
}]
];
var getNumToSort = function(id) {
for (var i = 0; i < regexMap.length; i++) {
var regex = regexMap[i][0];
var func = regexMap[i][1];
var results = regex.exec(id);
if (results) {
return func(results);
}
}
throw new Error('Could not parse commit ID ' + id);
};
return getNumToSort(cA.get('id')) - getNumToSort(cB.get('id'));
};
GitEngine.prototype.rebaseInteractiveStarter = function() {
var args = this.commandOptions['-i'];
this.twoArgsImpliedHead(args, ' -i');
this.rebaseInteractive(args[0], args[1]);
};
GitEngine.prototype.rebaseStarter = function() {
if (this.commandOptions['-i']) {
this.rebaseInteractiveStarter();
return;
}
this.twoArgsImpliedHead(this.generalArgs);
var response = this.rebase(this.generalArgs[0], this.generalArgs[1]);
if (response === undefined) {
// was a fastforward or already up to date. returning now
// will trigger the refresh animation by not adding anything to
// the animation queue
return;
}
animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
};
GitEngine.prototype.rebase = function(targetSource, currentLocation) {
// first some conditions
if (this.isUpstreamOf(targetSource, currentLocation)) {
this.command.setResult('Branch already up-to-date');
// git for some reason always checks out the branch you are rebasing,
// no matter the result of the rebase
this.checkout(currentLocation);
// returning instead of throwing makes a tree refresh
return;
}
if (this.isUpstreamOf(currentLocation, targetSource)) {
// just set the target of this current location to the source
this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
// we need the refresh tree animation to happen, so set the result directly
// instead of throwing
this.command.setResult('Fast-forwarding...');
this.checkout(currentLocation);
return;
}
// now the part of actually rebasing.
// We need to get the downstream set of targetSource first.
// then we BFS from currentLocation, using the downstream set as our stopping point.
// we need to BFS because we need to include all commits below
// pop these commits on top of targetSource and modify their ids with quotes
var stopSet = this.getUpstreamSet(targetSource);
// now BFS from here on out
var toRebaseRough = [];
var pQueue = [this.getCommitFromRef(currentLocation)];
while (pQueue.length) {
var popped = pQueue.pop();
// if its in the set, dont add it
if (stopSet[popped.get('id')]) {
continue;
}
// it's not in the set, so we need to rebase this commit
toRebaseRough.push(popped);
toRebaseRough.sort(this.idSortFunc);
toRebaseRough.reverse();
// keep searching
pQueue = pQueue.concat(popped.get('parents'));
}
return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation);
};
GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation) {
// there are a reduced set of checks now, so we can't exactly use parts of the rebase function
// but it will look similar.
// first if we are upstream of the target
if (this.isUpstreamOf(currentLocation, targetSource)) {
throw new GitError({
msg: 'Nothing to do... (git throws a "noop" status here); ' +
'Your source is upstream of your rebase target'
});
}
// now get the stop set
var stopSet = this.getUpstreamSet(targetSource);
var toRebaseRough = [];
// standard BFS
var pQueue = [this.getCommitFromRef(currentLocation)];
while (pQueue.length) {
var popped = pQueue.pop();
if (stopSet[popped.get('id')]) {
continue;
}
toRebaseRough.push(popped);
pQueue = pQueue.concat(popped.get('parents'));
pQueue.sort(this.idSortFunc);
}
// throw our merge's real fast and see if we have anything to do
var toRebase = [];
_.each(toRebaseRough, function(commit) {
if (commit.get('parents').length == 1) {
toRebase.push(commit);
}
});
if (!toRebase.length) {
throw new GitError({
msg: 'No commits to rebase! Everything is a merge commit'
});
}
// now do stuff :D since all our validation checks have passed, we are going to defer animation
// and actually launch the dialog
this.animationQueue.set('defer', true);
var callback = _.bind(function(userSpecifiedRebase) {
// first, they might have dropped everything (annoying)
if (!userSpecifiedRebase.length) {
this.command.setResult('Nothing to do...');
this.animationQueue.start();
return;
}
// finish the rebase crap and animate!
var animationData = this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation);
animationFactory.rebaseAnimation(this.animationQueue, animationData, this, this.gitVisuals);
this.animationQueue.start();
}, this);
new InteractiveRebaseView({
callback: callback,
toRebase: toRebase,
el: $('#dialogHolder')
});
};
GitEngine.prototype.rebaseFinish = function(toRebaseRough, stopSet, targetSource, currentLocation) {
// now we have the all the commits between currentLocation and the set of target to rebase.
var animationResponse = {};
animationResponse.destinationBranch = this.resolveID(targetSource);
// we need to throw out merge commits
var toRebase = [];
_.each(toRebaseRough, function(commit) {
if (commit.get('parents').length == 1) {
toRebase.push(commit);
}
});
// we ALSO need to throw out commits that will do the same changes. like
// if the upstream set has a commit C4 and we have C4', we dont rebase the C4' again.
// get this by doing ID scraping
var changesAlreadyMade = {};
_.each(stopSet, function(val, key) {
changesAlreadyMade[this.scrapeBaseID(key)] = val; // val == true
}, this);
// now get rid of the commits that will redo same changes
toRebaseRough = toRebase;
toRebase = [];
_.each(toRebaseRough, function(commit) {
var baseID = this.scrapeBaseID(commit.get('id'));
if (!changesAlreadyMade[baseID]) {
toRebase.push(commit);
}
}, this);
if (!toRebase.length) {
throw new GitError({
msg: 'No Commits to Rebase! Everything else is merge commits or changes already have been applied'
});
}
// now reverse it once more to get it in the right order
toRebase.reverse();
animationResponse.toRebaseArray = toRebase.slice(0);
// now pop all of these commits onto targetLocation
var base = this.getCommitFromRef(targetSource);
// do the rebase, and also maintain all our animation info during this
animationResponse.rebaseSteps = [];
var beforeSnapshot = this.gitVisuals.genSnapshot();
var afterSnapshot;
_.each(toRebase, function(old) {
var newId = this.rebaseAltID(old.get('id'));
var newCommit = this.makeCommit([base], newId);
base = newCommit;
// animation info
afterSnapshot = this.gitVisuals.genSnapshot();
animationResponse.rebaseSteps.push({
oldCommit: old,
newCommit: newCommit,
beforeSnapshot: beforeSnapshot,
afterSnapshot: afterSnapshot
});
beforeSnapshot = afterSnapshot;
}, this);
if (this.resolveID(currentLocation).get('type') == 'commit') {
// we referenced a commit like git rebase C2 C1, so we have
// to manually check out C1'
var steps = animationResponse.rebaseSteps;
var newestCommit = steps[steps.length - 1].newCommit;
this.checkout(newestCommit);
} else {
// now we just need to update the rebased branch is
this.setTargetLocation(currentLocation, base);
this.checkout(currentLocation);
}
// for animation
return animationResponse;
};
GitEngine.prototype.mergeStarter = function() {
this.twoArgsImpliedHead(this.generalArgs);
var newCommit = this.merge(this.generalArgs[0], this.generalArgs[1]);
if (newCommit === undefined) {
// its just a fast forwrard
animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
return;
}
animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
};
GitEngine.prototype.merge = function(targetSource, currentLocation) {
// first some conditions
if (this.isUpstreamOf(targetSource, currentLocation) ||
this.getCommitFromRef(targetSource) === this.getCommitFromRef(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
this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
// get fresh animation to happen
this.command.setResult('Fast-forwarding...');
return;
}
// now the part of making a merge commit
var parent1 = this.getCommitFromRef(currentLocation);
var parent2 = this.getCommitFromRef(targetSource);
// we need a fancy commit message
var msg = 'Merge ' + this.resolveName(targetSource) +
' into ' + this.resolveName(currentLocation);
// since we specify parent 1 as the first parent, it is the "main" parent
// and the node will be displayed below that branch / commit / whatever
var mergeCommit = this.makeCommit(
[parent1, parent2],
null,
{
commitMessage: msg
}
);
this.setTargetLocation(currentLocation, mergeCommit);
return mergeCommit;
};
GitEngine.prototype.checkoutStarter = function() {
var args = null;
if (this.commandOptions['-b']) {
// the user is really trying to just make a branch and then switch to it. so first:
args = this.commandOptions['-b'];
this.twoArgsImpliedHead(args, '-b');
var validId = this.validateBranchName(args[0]);
this.branch(validId, args[1]);
this.checkout(validId);
return;
}
if (this.commandOptions['-']) {
// get the heads last location
var lastPlace = this.HEAD.get('lastLastTarget');
if (!lastPlace) {
throw new GitError({
msg: 'Need a previous location to do - switching'
});
}
this.HEAD.set('target', lastPlace);
return;
}
if (this.commandOptions['-B']) {
args = this.commandOptions['-B'];
this.twoArgsImpliedHead(args, '-B');
this.forceBranch(args[0], args[1]);
this.checkout(args[0]);
return;
}
this.validateArgBounds(this.generalArgs, 1, 1);
this.checkout(this.unescapeQuotes(this.generalArgs[0]));
};
GitEngine.prototype.checkout = function(idOrTarget) {
var target = this.resolveID(idOrTarget);
if (target.get('id') === 'HEAD') {
// git checkout HEAD is a
// meaningless command but i used to do this back in the day
return;
}
var type = target.get('type');
if (type !== 'branch' && type !== 'commit') {
throw new GitError({
msg: 'can only checkout branches and commits!'
});
}
this.HEAD.set('target', target);
};
GitEngine.prototype.branchStarter = function() {
var args = null;
// handle deletion first
if (this.commandOptions['-d'] || this.commandOptions['-D']) {
var names = this.commandOptions['-d'] || this.commandOptions['-D'];
this.validateArgBounds(names, 1, NaN, '-d');
_.each(names, function(name) {
this.deleteBranch(name);
}, this);
return;
}
if (this.commandOptions['--contains']) {
args = this.commandOptions['--contains'];
this.validateArgBounds(args, 1, 1, '--contains');
this.printBranchesWithout(args[0]);
return;
}
if (this.commandOptions['-f']) {
args = this.commandOptions['-f'];
this.twoArgsImpliedHead(args, '-f');
// we want to force a branch somewhere
this.forceBranch(args[0], args[1]);
return;
}
if (this.generalArgs.length === 0) {
this.printBranches(this.getBranches());
return;
}
this.twoArgsImpliedHead(this.generalArgs);
this.branch(this.generalArgs[0], this.generalArgs[1]);
};
GitEngine.prototype.forceBranch = function(branchName, where) {
// if branchname doesn't exist...
if (!this.refs[branchName]) {
this.branch(branchName, where);
}
var branch = this.resolveID(branchName);
if (branch.get('type') !== 'branch') {
throw new GitError({
msg: "Can't force move anything but a branch!!"
});
}
var whereCommit = this.getCommitFromRef(where);
this.setTargetLocation(branch, whereCommit);
};
GitEngine.prototype.branch = function(name, ref) {
var target = this.getCommitFromRef(ref);
this.makeBranch(name, target);
};
GitEngine.prototype.deleteBranch = function(name) {
// trying to delete, lets check our refs
var target = this.resolveID(name);
if (target.get('type') !== 'branch') {
throw new GitError({
msg: "You can't delete things that arent branches with branch command"
});
}
if (target.get('id') == 'master') {
throw new GitError({
msg: "You can't delete the master branch!"
});
}
if (this.HEAD.get('target') === target) {
throw new GitError({
msg: "Cannot delete the branch you are currently on"
});
}
// now we know it's a branch
var branch = target;
this.branchCollection.remove(branch);
this.refs[branch.get('id')] = undefined;
delete this.refs[branch.get('id')];
if (branch.get('visBranch')) {
branch.get('visBranch').remove();
}
};
GitEngine.prototype.unescapeQuotes = function(str) {
return str.replace(/'/g, "'");
};
GitEngine.prototype.dispatch = function(command, callback) {
// current command, options, and args are stored in the gitEngine
// for easy reference during processing.
this.command = command;
this.commandOptions = command.get('supportedMap');
this.generalArgs = command.get('generalArgs');
// set up the animation queue
var whenDone = _.bind(function() {
command.set('status', 'finished');
callback();
}, this);
this.animationQueue = new AnimationQueue({
callback: whenDone
});
command.set('status', 'processing');
try {
var methodName = command.get('method').replace(/-/g, '') + 'Starter';
this[methodName]();
} catch (err) {
if (err instanceof GitError ||
err instanceof CommandResult) {
// short circuit animation by just setting error and returning
command.set('error', err);
callback();
return;
} else {
throw err;
}
}
// only add the refresh if we didn't do manual animations
if (!this.animationQueue.get('animations').length && !this.animationQueue.get('defer')) {
animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
}
// animation queue will call the callback when its done
if (!this.animationQueue.get('defer')) {
this.animationQueue.start();
}
};
GitEngine.prototype.showStarter = function() {
this.oneArgImpliedHead(this.generalArgs);
this.show(this.generalArgs[0]);
};
GitEngine.prototype.show = function(ref) {
var commit = this.getCommitFromRef(ref);
throw new CommandResult({
msg: commit.getShowEntry()
});
};
GitEngine.prototype.statusStarter = function() {
var lines = [];
if (this.getDetachedHead()) {
lines.push('Detached Head!');
} else {
var branchName = this.HEAD.get('target').get('id');
lines.push('On branch ' + branchName);
}
lines.push('Changes to be committed:');
lines.push('');
lines.push(' modified: cal/OskiCostume.stl');
lines.push('');
lines.push('Ready to commit! (as always in this demo)');
var msg = '';
_.each(lines, function(line) {
msg += '# ' + line + '\n';
});
throw new CommandResult({
msg: msg
});
};
GitEngine.prototype.logStarter = function() {
if (this.generalArgs.length == 2) {
// do fancy git log branchA ^branchB
if (this.generalArgs[1][0] == '^') {
this.logWithout(this.generalArgs[0], this.generalArgs[1]);
} else {
throw new GitError({
msg: 'I need a not branch (^branchName) when getting two arguments!'
});
}
}
this.oneArgImpliedHead(this.generalArgs);
this.log(this.generalArgs[0]);
};
GitEngine.prototype.logWithout = function(ref, omitBranch) {
// slice off the ^branch
omitBranch = omitBranch.slice(1);
this.log(ref, this.getUpstreamSet(omitBranch));
};
GitEngine.prototype.log = function(ref, omitSet) {
// omit set is for doing stuff like git log branchA ^branchB
omitSet = omitSet || {};
// first get the commit we referenced
var commit = this.getCommitFromRef(ref);
// then get as many far back as we can from here, order by commit date
var toDump = [];
var pQueue = [commit];
var seen = {};
while (pQueue.length) {
var popped = pQueue.shift(0);
if (seen[popped.get('id')] || omitSet[popped.get('id')]) {
continue;
}
seen[popped.get('id')] = true;
toDump.push(popped);
if (popped.get('parents') && popped.get('parents').length) {
pQueue = pQueue.concat(popped.get('parents'));
}
}
// now go through and collect logs
var bigLogStr = '';
_.each(toDump, function(c) {
bigLogStr += c.getLogEntry();
}, this);
throw new CommandResult({
msg: bigLogStr
});
};
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.getCommonAncestor = function(ancestor, cousin) {
if (this.isUpstreamOf(cousin, ancestor)) {
throw new Error('Dont use common ancestor if we are upstream!');
}
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 = queue.concat(here.get('parents'));
}
throw new Error('something has gone very wrong... two nodes arent connected!');
};
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 ancestorID = commit.get('id');
var queue = [commit];
var exploredSet = {};
exploredSet[ancestorID] = true;
var addToExplored = function(rent) {
exploredSet[rent.get('id')] = true;
queue.push(rent);
};
while (queue.length) {
var here = queue.pop();
var rents = here.get('parents');
_.each(rents, addToExplored);
}
return exploredSet;
};
var Ref = Backbone.Model.extend({
initialize: function() {
if (!this.get('target')) {
throw new Error('must be initialized with target');
}
if (!this.get('id')) {
throw new Error('must be given an id');
}
this.set('type', 'general ref');
if (this.get('id') == 'HEAD') {
this.set('lastLastTarget', null);
this.set('lastTarget', this.get('target'));
// have HEAD remember where it is for checkout -
this.on('change:target', this.targetChanged, this);
}
},
targetChanged: function(model, targetValue, ev) {
// push our little 3 stack back. we need to do this because
// backbone doesn't give you what the value WAS, only what it was changed
// TO
this.set('lastLastTarget', this.get('lastTarget'));
this.set('lastTarget', targetValue);
},
toString: function() {
return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target'));
}
});
var Branch = Ref.extend({
defaults: {
visBranch: null
},
initialize: function() {
Ref.prototype.initialize.call(this);
this.set('type', 'branch');
}
});
var Commit = Backbone.Model.extend({
defaults: {
type: 'commit',
children: null,
parents: null,
author: 'Peter Cottle',
createTime: null,
commitMessage: null,
visNode: null,
gitVisuals: null
},
constants: {
circularFields: ['gitVisuals', 'visNode', 'children']
},
getLogEntry: function() {
// for now we are just joining all these things with newlines which
// will get placed by paragraph tags. Not really a fan of this, but
// it's better than making an entire template and all that jazz
return [
'Author: ' + this.get('author'),
'Date: ' + this.get('createTime'),
'
',
this.get('commitMessage'),
'
',
'Commit: ' + this.get('id')
].join('\n' ) + '\n';
},
getShowEntry: function() {
// same deal as above, show log entry and some fake changes
return [
this.getLogEntry(),
'diff --git a/bigGameResults.html b/bigGameResults.html',
'--- bigGameResults.html',
'+++ bigGameResults.html',
'@@ 13,27 @@ Winner, Score',
'- Stanfurd, 14-7',
'+ Cal, 21-14'
].join('\n') + '\n';
},
validateAtInit: function() {
if (!this.get('id')) {
throw new Error('Need ID!!');
}
if (!this.get('createTime')) {
this.set('createTime', new Date().toString());
}
if (!this.get('commitMessage')) {
this.set('commitMessage', 'Quick Commit. Go Bears!');
}
this.set('children', []);
// root commits have no parents
if (!this.get('rootCommit')) {
if (!this.get('parents') || !this.get('parents').length) {
throw new Error('needs parents');
}
}
},
addNodeToVisuals: function() {
var visNode = this.get('gitVisuals').addNode(this.get('id'), this);
this.set('visNode', visNode);
},
addEdgeToVisuals: function(parent) {
this.get('gitVisuals').addEdge(this.get('id'), parent.get('id'));
},
isMainParent: function(parent) {
var index = this.get('parents').indexOf(parent);
return index === 0;
},
initialize: function(options) {
this.validateAtInit();
this.addNodeToVisuals();
_.each(this.get('parents'), function(parent) {
parent.get('children').push(this);
this.addEdgeToVisuals(parent);
}, this);
}
});
exports.GitEngine = GitEngine;
exports.Commit = Commit;
exports.Branch = Branch;
exports.Ref = Ref;
});
require.define("/animation/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('./index').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("/views/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('');
// garbage collection will get us
},
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("/errors/index.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.define("/models/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', '' + 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/index.js"); require.define("/git/index.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactoryModule = require('../animation/animationFactory'); var animationFactory = new AnimationFactoryModule.AnimationFactory(); var Main = require('../app'); var AnimationQueue = require('../animation').AnimationQueue; var InteractiveRebaseView = require('../views/miscViews').InteractiveRebaseView; var Errors = require('../errors'); var GitError = Errors.GitError; var CommandResult = Errors.CommandResult; // backbone or something uses _.uniqueId, so we make our own here var uniqueId = (function() { var n = 0; return function(prepend) { return prepend? prepend + n++ : n++; }; })(); function GitEngine(options) { this.rootCommit = null; this.refs = {}; this.HEAD = null; this.branchCollection = options.branches; this.commitCollection = options.collection; this.gitVisuals = options.gitVisuals; // global variable to keep track of the options given // along with the command call. this.commandOptions = {}; this.generalArgs = []; Main.getEvents().on('processCommand', _.bind(this.dispatch, this)); } GitEngine.prototype.defaultInit = function() { var defaultTree = JSON.parse(unescape("%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22master%22%2C%22type%22%3A%22general%20ref%22%7D%7D")); this.loadTree(defaultTree); }; GitEngine.prototype.init = function() { // make an initial commit and a master branch this.rootCommit = this.makeCommit(null, null, {rootCommit: true}); this.commitCollection.add(this.rootCommit); var master = this.makeBranch('master', this.rootCommit); this.HEAD = new Ref({ id: 'HEAD', target: master }); this.refs[this.HEAD.get('id')] = this.HEAD; // commit once to get things going this.commit(); }; GitEngine.prototype.exportTree = function() { // need to export all commits, their connectivity / messages, branches, and state of head. // this would be simple if didn't have circular structures.... :P // thus, we need to loop through and "flatten" our graph of objects referencing one another var totalExport = { branches: {}, commits: {}, HEAD: null }; _.each(this.branchCollection.toJSON(), function(branch) { branch.target = branch.target.get('id'); branch.visBranch = undefined; totalExport.branches[branch.id] = branch; }); _.each(this.commitCollection.toJSON(), function(commit) { // clear out the fields that reference objects and create circular structure _.each(Commit.prototype.constants.circularFields, function(field) { commit[field] = undefined; }, this); // convert parents var parents = []; _.each(commit.parents, function(par) { parents.push(par.get('id')); }); commit.parents = parents; totalExport.commits[commit.id] = commit; }, this); var HEAD = this.HEAD.toJSON(); HEAD.visBranch = undefined; HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = undefined; HEAD.target = HEAD.target.get('id'); totalExport.HEAD = HEAD; return totalExport; }; GitEngine.prototype.printTree = function() { var str = escape(JSON.stringify(this.exportTree())); return str; }; GitEngine.prototype.printAndCopyTree = function() { window.prompt('Copy the tree string below', this.printTree()); }; GitEngine.prototype.loadTree = function(tree) { // deep copy in case we use it a bunch tree = $.extend(true, {}, tree); // first clear everything this.removeAll(); this.instantiateFromTree(tree); this.reloadGraphics(); }; GitEngine.prototype.loadTreeFromString = function(treeString) { this.loadTree(JSON.parse(unescape(treeString))); }; GitEngine.prototype.instantiateFromTree = function(tree) { // now we do the loading part var createdSoFar = {}; _.each(tree.commits, function(commitJSON) { var commit = this.getOrMakeRecursive(tree, createdSoFar, commitJSON.id); this.commitCollection.add(commit); }, this); _.each(tree.branches, function(branchJSON) { var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id); this.branchCollection.add(branch, {silent: true}); }, this); var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id); this.HEAD = HEAD; this.rootCommit = createdSoFar['C0']; if (!this.rootCommit) { throw new Error('Need root commit of C0 for calculations'); } this.refs = createdSoFar; this.branchCollection.each(function(branch) { this.gitVisuals.addBranch(branch); }, this); }; GitEngine.prototype.reloadGraphics = function() { // get the root commit, no better way to do it var rootCommit = null; this.commitCollection.each(function(commit) { if (commit.get('id') == 'C0') { rootCommit = commit; } }); this.gitVisuals.rootCommit = rootCommit; // this just basically makes the HEAD branch. the head branch really should have been // a member of a collection and not this annoying edge case stuff... one day this.gitVisuals.initHeadBranch(); // when the paper is ready this.gitVisuals.drawTreeFromReload(); this.gitVisuals.refreshTreeHarsh(); }; GitEngine.prototype.getOrMakeRecursive = function(tree, createdSoFar, objID) { if (createdSoFar[objID]) { // base case return createdSoFar[objID]; } var getType = function(tree, id) { if (tree.commits[id]) { return 'commit'; } else if (tree.branches[id]) { return 'branch'; } else if (id == 'HEAD') { return 'HEAD'; } throw new Error("bad type for " + id); }; // figure out what type var type = getType(tree, objID); if (type == 'HEAD') { var headJSON = tree.HEAD; var HEAD = new Ref(_.extend( tree.HEAD, { target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target) } )); createdSoFar[objID] = HEAD; return HEAD; } if (type == 'branch') { var branchJSON = tree.branches[objID]; var branch = new Branch(_.extend( tree.branches[objID], { target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target) } )); createdSoFar[objID] = branch; return branch; } if (type == 'commit') { // for commits, we need to grab all the parents var commitJSON = tree.commits[objID]; var parentObjs = []; _.each(commitJSON.parents, function(parentID) { parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID)); }, this); var commit = new Commit(_.extend( commitJSON, { parents: parentObjs, gitVisuals: this.gitVisuals } )); createdSoFar[objID] = commit; return commit; } throw new Error('ruh rho!! unsupported tyep for ' + objID); }; GitEngine.prototype.removeAll = function() { this.branchCollection.reset(); this.commitCollection.reset(); this.refs = {}; this.HEAD = null; this.rootCommit = null; this.gitVisuals.resetAll(); }; GitEngine.prototype.getDetachedHead = function() { // detached head is if HEAD points to a commit instead of a branch... var target = this.HEAD.get('target'); var targetType = target.get('type'); return targetType !== 'branch'; }; GitEngine.prototype.validateBranchName = function(name) { name = name.replace(/\s/g, ''); if (!/^[a-zA-Z0-9]+$/.test(name)) { throw new GitError({ msg: 'woah bad branch name!! This is not ok: ' + name }); } if (/[hH][eE][aA][dD]/.test(name)) { throw new GitError({ msg: 'branch name of "head" is ambiguous, dont name it that' }); } if (name.length > 9) { name = name.slice(0, 9); this.command.addWarning( 'Sorry, we need to keep branch names short for the visuals. Your branch ' + 'name was truncated to 9 characters, resulting in ' + name ); } return name; }; GitEngine.prototype.makeBranch = function(id, target) { id = this.validateBranchName(id); if (this.refs[id]) { throw new GitError({ msg: 'that branch id either matches a commit hash or already exists!' }); } var branch = new Branch({ target: target, id: id }); this.branchCollection.add(branch); this.refs[branch.get('id')] = branch; return branch; }; GitEngine.prototype.getHead = function() { return _.clone(this.HEAD); }; GitEngine.prototype.getBranches = function() { var toReturn = []; this.branchCollection.each(function(branch) { toReturn.push({ id: branch.get('id'), selected: this.HEAD.get('target') === branch, target: branch.get('target'), obj: branch }); }, this); return toReturn; }; GitEngine.prototype.printBranchesWithout = function(without) { var commitToBranches = this.getUpstreamBranchSet(); var commitID = this.getCommitFromRef(without).get('id'); var toPrint = []; _.each(commitToBranches[commitID], function(branchJSON) { branchJSON.selected = this.HEAD.get('target').get('id') == branchJSON.id; toPrint.push(branchJSON); }, this); this.printBranches(toPrint); }; GitEngine.prototype.printBranches = function(branches) { var result = ''; _.each(branches, function(branch) { result += (branch.selected ? '* ' : '') + branch.id + '\n'; }); throw new CommandResult({ msg: result }); }; GitEngine.prototype.makeCommit = function(parents, id, options) { // ok we need to actually manually create commit IDs now because // people like nikita (thanks for finding this!) could // make branches named C2 before creating the commit C2 if (!id) { id = uniqueId('C'); while (this.refs[id]) { id = uniqueId('C'); } } var commit = new Commit(_.extend({ parents: parents, id: id, gitVisuals: this.gitVisuals }, options || {} )); this.refs[commit.get('id')] = commit; this.commitCollection.add(commit); return commit; }; GitEngine.prototype.acceptNoGeneralArgs = function() { if (this.generalArgs.length) { throw new GitError({ msg: "That command accepts no general arguments" }); } }; GitEngine.prototype.validateArgBounds = function(args, lower, upper, option) { // this is a little utility class to help arg validation that happens over and over again var what = (option === undefined) ? 'git ' + this.command.get('method') : this.command.get('method') + ' ' + option + ' '; what = 'with ' + what; if (args.length < lower) { throw new GitError({ msg: 'I expect at least ' + String(lower) + ' argument(s) ' + what }); } if (args.length > upper) { throw new GitError({ msg: 'I expect at most ' + String(upper) + ' argument(s) ' + what }); } }; GitEngine.prototype.oneArgImpliedHead = function(args, option) { // for log, show, etc this.validateArgBounds(args, 0, 1, option); if (args.length === 0) { args.push('HEAD'); } }; GitEngine.prototype.twoArgsImpliedHead = function(args, option) { // our args we expect to be between 1 and 2 this.validateArgBounds(args, 1, 2, option); // and if it's one, add a HEAD to the back if (args.length == 1) { args.push('HEAD'); } }; GitEngine.prototype.revertStarter = function() { this.validateArgBounds(this.generalArgs, 1, NaN); var response = this.revert(this.generalArgs); if (response) { animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals); } }; GitEngine.prototype.revert = function(whichCommits) { // for each commit, we want to revert it var toRebase = []; _.each(whichCommits, function(stringRef) { toRebase.push(this.getCommitFromRef(stringRef)); }, this); // we animate reverts now!! we use the rebase animation though so that's // why the terminology is like it is var animationResponse = {}; animationResponse.destinationBranch = this.resolveID(toRebase[0]); animationResponse.toRebaseArray = toRebase.slice(0); animationResponse.rebaseSteps = []; var beforeSnapshot = this.gitVisuals.genSnapshot(); var afterSnapshot; // now make a bunch of commits on top of where we are var base = this.getCommitFromRef('HEAD'); _.each(toRebase, function(oldCommit) { var newId = this.rebaseAltID(oldCommit.get('id')); var newCommit = this.makeCommit([base], newId, { commitMessage: 'Reverting ' + this.resolveName(oldCommit) + ': "' + oldCommit.get('commitMessage') + '"' }); base = newCommit; // animation stuff afterSnapshot = this.gitVisuals.genSnapshot(); animationResponse.rebaseSteps.push({ oldCommit: oldCommit, newCommit: newCommit, beforeSnapshot: beforeSnapshot, afterSnapshot: afterSnapshot }); beforeSnapshot = afterSnapshot; }, this); // done! update our location this.setTargetLocation('HEAD', base); // animation return animationResponse; }; GitEngine.prototype.resetStarter = function() { if (this.commandOptions['--soft']) { throw new GitError({ msg: "You can't use --soft because there is no concept of stashing" + " changes or staging files, so you will lose your progress." + " Try using interactive rebasing (or just rebasing) to move commits." }); } if (this.commandOptions['--hard']) { this.command.addWarning( 'Nice! You are using --hard. The default behavior is a hard reset in ' + "this demo, so don't worry about specifying the option explicity" ); // dont absorb the arg off of --hard this.generalArgs = this.generalArgs.concat(this.commandOptions['--hard']); } this.validateArgBounds(this.generalArgs, 1, 1); if (this.getDetachedHead()) { throw new GitError({ msg: "Cant reset in detached head! Use checkout if you want to move" }); } this.reset(this.generalArgs[0]); }; GitEngine.prototype.reset = function(target) { this.setTargetLocation('HEAD', this.getCommitFromRef(target)); }; GitEngine.prototype.cherrypickStarter = function() { this.validateArgBounds(this.generalArgs, 1, 1); var newCommit = this.cherrypick(this.generalArgs[0]); animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals); }; GitEngine.prototype.cherrypick = function(ref) { var commit = this.getCommitFromRef(ref); // check if we already have that var set = this.getUpstreamSet('HEAD'); if (set[commit.get('id')]) { throw new GitError({ msg: "We already have that commit in our changes history! You can't cherry-pick it " + "if it shows up in git log." }); } // alter the ID slightly var id = this.rebaseAltID(commit.get('id')); // now commit with that id onto HEAD var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id); this.setTargetLocation(this.HEAD, newCommit); return newCommit; }; GitEngine.prototype.commitStarter = function() { this.acceptNoGeneralArgs(); if (this.commandOptions['-am'] && ( this.commandOptions['-a'] || this.commandOptions['-m'])) { throw new GitError({ msg: "You can't have -am with another -m or -a!" }); } var msg = null; var args = null; if (this.commandOptions['-a']) { this.command.addWarning('No need to add files in this demo'); } if (this.commandOptions['-am']) { args = this.commandOptions['-am']; this.validateArgBounds(args, 1, 1, '-am'); this.command.addWarning("Don't worry about adding files in this demo. I'll take " + "down your commit message anyways, but you can commit without a message " + "in this demo as well"); msg = args[0]; } if (this.commandOptions['-m']) { args = this.commandOptions['-m']; this.validateArgBounds(args, 1, 1, '-m'); msg = args[0]; } var newCommit = this.commit(); if (msg) { msg = msg .replace(/"/g, '"') .replace(/^"/g, '') .replace(/"$/g, ''); newCommit.set('commitMessage', msg); } animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals); }; GitEngine.prototype.commit = function() { var targetCommit = this.getCommitFromRef(this.HEAD); var id = null; // if we want to ammend, go one above if (this.commandOptions['--amend']) { targetCommit = this.resolveID('HEAD~1'); id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id')); } var newCommit = this.makeCommit([targetCommit], id); if (this.getDetachedHead()) { this.command.addWarning('Warning!! Detached HEAD state'); } this.setTargetLocation(this.HEAD, newCommit); return newCommit; }; GitEngine.prototype.resolveName = function(someRef) { // first get the obj var obj = this.resolveID(someRef); if (obj.get('type') == 'commit') { return 'commit ' + obj.get('id'); } if (obj.get('type') == 'branch') { return 'branch "' + obj.get('id') + '"'; } // we are dealing with HEAD return this.resolveName(obj.get('target')); }; GitEngine.prototype.resolveID = function(idOrTarget) { if (idOrTarget === null || idOrTarget === undefined) { throw new Error('Dont call this with null / undefined'); } if (typeof idOrTarget !== 'string') { return idOrTarget; } return this.resolveStringRef(idOrTarget); }; GitEngine.prototype.resolveStringRef = function(ref) { if (this.refs[ref]) { return this.refs[ref]; } // may be something like HEAD~2 or master^^ var relativeRefs = [ [/^([a-zA-Z0-9]+)~(\d+)\s*$/, function(matches) { return parseInt(matches[2], 10); }], [/^([a-zA-Z0-9]+)(\^+)\s*$/, function(matches) { return matches[2].length; }] ]; var startRef = null; var numBack = null; _.each(relativeRefs, function(config) { var regex = config[0]; var parse = config[1]; if (regex.test(ref)) { var matches = regex.exec(ref); numBack = parse(matches); startRef = matches[1]; } }, this); if (!startRef) { throw new GitError({ msg: 'unknown ref ' + ref }); } if (!this.refs[startRef]) { throw new GitError({ msg: 'the ref ' + startRef +' does not exist.' }); } var commit = this.getCommitFromRef(startRef); return this.numBackFrom(commit, numBack); }; GitEngine.prototype.getCommitFromRef = function(ref) { var start = this.resolveID(ref); // works for both HEAD and just a single layer. aka branch while (start.get('type') !== 'commit') { start = start.get('target'); } return start; }; GitEngine.prototype.getType = function(ref) { return this.resolveID(ref).get('type'); }; GitEngine.prototype.setTargetLocation = function(ref, target) { if (this.getType(ref) == 'commit') { // nothing to do return; } // sets whatever ref is (branch, HEAD, etc) to a target. so if // you pass in HEAD, and HEAD is pointing to a branch, it will update // the branch to that commit, not the HEAD ref = this.getOneBeforeCommit(ref); ref.set('target', target); }; GitEngine.prototype.getUpstreamBranchSet = function() { // this is expensive!! so only call once in a while var commitToSet = {}; var inArray = function(arr, id) { var found = false; _.each(arr, function(wrapper) { if (wrapper.id == id) { found = true; } }); return found; }; var bfsSearch = function(commit) { var set = []; var pQueue = [commit]; while (pQueue.length) { var popped = pQueue.pop(); set.push(popped.get('id')); if (popped.get('parents') && popped.get('parents').length) { pQueue = pQueue.concat(popped.get('parents')); } } return set; }; this.branchCollection.each(function(branch) { var set = bfsSearch(branch.get('target')); _.each(set, function(id) { commitToSet[id] = commitToSet[id] || []; // only add it if it's not there, so hue blending is ok if (!inArray(commitToSet[id], branch.get('id'))) { commitToSet[id].push({ obj: branch, id: branch.get('id') }); } }); }); return commitToSet; }; GitEngine.prototype.getUpstreamHeadSet = function() { var set = this.getUpstreamSet('HEAD'); var including = this.getCommitFromRef('HEAD').get('id'); set[including] = true; return set; }; GitEngine.prototype.getOneBeforeCommit = function(ref) { // you can call this command on HEAD in detached, HEAD, or on a branch // and it will return the ref that is one above a commit. aka // it resolves HEAD to something that we can move the ref with var start = this.resolveID(ref); if (start === this.HEAD && !this.getDetachedHead()) { 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: // // * merge master into new // |\ // | \* commit here // |* \ commit there // | |* commit here // \ / // | * root // // // hence we need to do a BFS search, with the commit date being the // value to sort off of (rather than just purely the level) if (numBack === 0) { return commit; } // we use a special sorting function here that // prefers the later commits over the earlier ones var sortQueue = _.bind(function(queue) { queue.sort(this.idSortFunc); queue.reverse(); }, this); var pQueue = [].concat(commit.get('parents') || []); sortQueue(pQueue); numBack--; while (pQueue.length && numBack !== 0) { var popped = pQueue.shift(0); var parents = popped.get('parents'); if (parents && parents.length) { pQueue = pQueue.concat(parents); } sortQueue(pQueue); numBack--; } if (numBack !== 0 || pQueue.length === 0) { throw new GitError({ msg: "Sorry, I can't go that many commits back" }); } return pQueue.shift(0); }; GitEngine.prototype.scrapeBaseID = function(id) { var results = /^C(\d+)/.exec(id); if (!results) { throw new Error('regex failed on ' + id); } return 'C' + results[1]; }; GitEngine.prototype.rebaseAltID = function(id) { // this function alters an ID to add a quote to the end, // indicating that it was rebased. it also checks existence var regexMap = [ [/^C(\d+)[']{0,2}$/, function(bits) { // this id can use another quote, so just add it return bits[0] + "'"; }], [/^C(\d+)[']{3}$/, function(bits) { // here we switch from C''' to C'^4 return bits[0].slice(0, -3) + "'^4"; }], [/^C(\d+)['][\^](\d+)$/, function(bits) { return 'C' + String(bits[1]) + "'^" + String(Number(bits[2]) + 1); }] ]; for (var i = 0; i < regexMap.length; i++) { var regex = regexMap[i][0]; var func = regexMap[i][1]; var results = regex.exec(id); if (results) { var newId = func(results); // if this id exists, continue down the rabbit hole if (this.refs[newId]) { return this.rebaseAltID(newId); } else { return newId; } } } throw new Error('could not modify the id ' + id); }; GitEngine.prototype.idSortFunc = function(cA, cB) { // commit IDs can come in many forms: // C4 // C4' (from a rebase) // C4'' (from multiple rebases) // C4'^3 (from a BUNCH of rebases) var scale = 1000; var regexMap = [ [/^C(\d+)$/, function(bits) { // return the 4 from C4 return scale * bits[1]; }], [/^C(\d+)([']+)$/, function(bits) { // return the 4 from C4, plus the length of the quotes return scale * bits[1] + bits[2].length; }], [/^C(\d+)['][\^](\d+)$/, function(bits) { return scale * bits[1] + Number(bits[2]); }] ]; var getNumToSort = function(id) { for (var i = 0; i < regexMap.length; i++) { var regex = regexMap[i][0]; var func = regexMap[i][1]; var results = regex.exec(id); if (results) { return func(results); } } throw new Error('Could not parse commit ID ' + id); }; return getNumToSort(cA.get('id')) - getNumToSort(cB.get('id')); }; GitEngine.prototype.rebaseInteractiveStarter = function() { var args = this.commandOptions['-i']; this.twoArgsImpliedHead(args, ' -i'); this.rebaseInteractive(args[0], args[1]); }; GitEngine.prototype.rebaseStarter = function() { if (this.commandOptions['-i']) { this.rebaseInteractiveStarter(); return; } this.twoArgsImpliedHead(this.generalArgs); var response = this.rebase(this.generalArgs[0], this.generalArgs[1]); if (response === undefined) { // was a fastforward or already up to date. returning now // will trigger the refresh animation by not adding anything to // the animation queue return; } animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals); }; GitEngine.prototype.rebase = function(targetSource, currentLocation) { // first some conditions if (this.isUpstreamOf(targetSource, currentLocation)) { this.command.setResult('Branch already up-to-date'); // git for some reason always checks out the branch you are rebasing, // no matter the result of the rebase this.checkout(currentLocation); // returning instead of throwing makes a tree refresh return; } if (this.isUpstreamOf(currentLocation, targetSource)) { // just set the target of this current location to the source this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource)); // we need the refresh tree animation to happen, so set the result directly // instead of throwing this.command.setResult('Fast-forwarding...'); this.checkout(currentLocation); return; } // now the part of actually rebasing. // We need to get the downstream set of targetSource first. // then we BFS from currentLocation, using the downstream set as our stopping point. // we need to BFS because we need to include all commits below // pop these commits on top of targetSource and modify their ids with quotes var stopSet = this.getUpstreamSet(targetSource); // now BFS from here on out var toRebaseRough = []; var pQueue = [this.getCommitFromRef(currentLocation)]; while (pQueue.length) { var popped = pQueue.pop(); // if its in the set, dont add it if (stopSet[popped.get('id')]) { continue; } // it's not in the set, so we need to rebase this commit toRebaseRough.push(popped); toRebaseRough.sort(this.idSortFunc); toRebaseRough.reverse(); // keep searching pQueue = pQueue.concat(popped.get('parents')); } return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation); }; GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation) { // there are a reduced set of checks now, so we can't exactly use parts of the rebase function // but it will look similar. // first if we are upstream of the target if (this.isUpstreamOf(currentLocation, targetSource)) { throw new GitError({ msg: 'Nothing to do... (git throws a "noop" status here); ' + 'Your source is upstream of your rebase target' }); } // now get the stop set var stopSet = this.getUpstreamSet(targetSource); var toRebaseRough = []; // standard BFS var pQueue = [this.getCommitFromRef(currentLocation)]; while (pQueue.length) { var popped = pQueue.pop(); if (stopSet[popped.get('id')]) { continue; } toRebaseRough.push(popped); pQueue = pQueue.concat(popped.get('parents')); pQueue.sort(this.idSortFunc); } // throw our merge's real fast and see if we have anything to do var toRebase = []; _.each(toRebaseRough, function(commit) { if (commit.get('parents').length == 1) { toRebase.push(commit); } }); if (!toRebase.length) { throw new GitError({ msg: 'No commits to rebase! Everything is a merge commit' }); } // now do stuff :D since all our validation checks have passed, we are going to defer animation // and actually launch the dialog this.animationQueue.set('defer', true); var callback = _.bind(function(userSpecifiedRebase) { // first, they might have dropped everything (annoying) if (!userSpecifiedRebase.length) { this.command.setResult('Nothing to do...'); this.animationQueue.start(); return; } // finish the rebase crap and animate! var animationData = this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation); animationFactory.rebaseAnimation(this.animationQueue, animationData, this, this.gitVisuals); this.animationQueue.start(); }, this); new InteractiveRebaseView({ callback: callback, toRebase: toRebase, el: $('#dialogHolder') }); }; GitEngine.prototype.rebaseFinish = function(toRebaseRough, stopSet, targetSource, currentLocation) { // now we have the all the commits between currentLocation and the set of target to rebase. var animationResponse = {}; animationResponse.destinationBranch = this.resolveID(targetSource); // we need to throw out merge commits var toRebase = []; _.each(toRebaseRough, function(commit) { if (commit.get('parents').length == 1) { toRebase.push(commit); } }); // we ALSO need to throw out commits that will do the same changes. like // if the upstream set has a commit C4 and we have C4', we dont rebase the C4' again. // get this by doing ID scraping var changesAlreadyMade = {}; _.each(stopSet, function(val, key) { changesAlreadyMade[this.scrapeBaseID(key)] = val; // val == true }, this); // now get rid of the commits that will redo same changes toRebaseRough = toRebase; toRebase = []; _.each(toRebaseRough, function(commit) { var baseID = this.scrapeBaseID(commit.get('id')); if (!changesAlreadyMade[baseID]) { toRebase.push(commit); } }, this); if (!toRebase.length) { throw new GitError({ msg: 'No Commits to Rebase! Everything else is merge commits or changes already have been applied' }); } // now reverse it once more to get it in the right order toRebase.reverse(); animationResponse.toRebaseArray = toRebase.slice(0); // now pop all of these commits onto targetLocation var base = this.getCommitFromRef(targetSource); // do the rebase, and also maintain all our animation info during this animationResponse.rebaseSteps = []; var beforeSnapshot = this.gitVisuals.genSnapshot(); var afterSnapshot; _.each(toRebase, function(old) { var newId = this.rebaseAltID(old.get('id')); var newCommit = this.makeCommit([base], newId); base = newCommit; // animation info afterSnapshot = this.gitVisuals.genSnapshot(); animationResponse.rebaseSteps.push({ oldCommit: old, newCommit: newCommit, beforeSnapshot: beforeSnapshot, afterSnapshot: afterSnapshot }); beforeSnapshot = afterSnapshot; }, this); if (this.resolveID(currentLocation).get('type') == 'commit') { // we referenced a commit like git rebase C2 C1, so we have // to manually check out C1' var steps = animationResponse.rebaseSteps; var newestCommit = steps[steps.length - 1].newCommit; this.checkout(newestCommit); } else { // now we just need to update the rebased branch is this.setTargetLocation(currentLocation, base); this.checkout(currentLocation); } // for animation return animationResponse; }; GitEngine.prototype.mergeStarter = function() { this.twoArgsImpliedHead(this.generalArgs); var newCommit = this.merge(this.generalArgs[0], this.generalArgs[1]); if (newCommit === undefined) { // its just a fast forwrard animationFactory.refreshTree(this.animationQueue, this.gitVisuals); return; } animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals); }; GitEngine.prototype.merge = function(targetSource, currentLocation) { // first some conditions if (this.isUpstreamOf(targetSource, currentLocation) || this.getCommitFromRef(targetSource) === this.getCommitFromRef(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 this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource)); // get fresh animation to happen this.command.setResult('Fast-forwarding...'); return; } // now the part of making a merge commit var parent1 = this.getCommitFromRef(currentLocation); var parent2 = this.getCommitFromRef(targetSource); // we need a fancy commit message var msg = 'Merge ' + this.resolveName(targetSource) + ' into ' + this.resolveName(currentLocation); // since we specify parent 1 as the first parent, it is the "main" parent // and the node will be displayed below that branch / commit / whatever var mergeCommit = this.makeCommit( [parent1, parent2], null, { commitMessage: msg } ); this.setTargetLocation(currentLocation, mergeCommit); return mergeCommit; }; GitEngine.prototype.checkoutStarter = function() { var args = null; if (this.commandOptions['-b']) { // the user is really trying to just make a branch and then switch to it. so first: args = this.commandOptions['-b']; this.twoArgsImpliedHead(args, '-b'); var validId = this.validateBranchName(args[0]); this.branch(validId, args[1]); this.checkout(validId); return; } if (this.commandOptions['-']) { // get the heads last location var lastPlace = this.HEAD.get('lastLastTarget'); if (!lastPlace) { throw new GitError({ msg: 'Need a previous location to do - switching' }); } this.HEAD.set('target', lastPlace); return; } if (this.commandOptions['-B']) { args = this.commandOptions['-B']; this.twoArgsImpliedHead(args, '-B'); this.forceBranch(args[0], args[1]); this.checkout(args[0]); return; } this.validateArgBounds(this.generalArgs, 1, 1); this.checkout(this.unescapeQuotes(this.generalArgs[0])); }; GitEngine.prototype.checkout = function(idOrTarget) { var target = this.resolveID(idOrTarget); if (target.get('id') === 'HEAD') { // git checkout HEAD is a // meaningless command but i used to do this back in the day return; } var type = target.get('type'); if (type !== 'branch' && type !== 'commit') { throw new GitError({ msg: 'can only checkout branches and commits!' }); } this.HEAD.set('target', target); }; GitEngine.prototype.branchStarter = function() { var args = null; // handle deletion first if (this.commandOptions['-d'] || this.commandOptions['-D']) { var names = this.commandOptions['-d'] || this.commandOptions['-D']; this.validateArgBounds(names, 1, NaN, '-d'); _.each(names, function(name) { this.deleteBranch(name); }, this); return; } if (this.commandOptions['--contains']) { args = this.commandOptions['--contains']; this.validateArgBounds(args, 1, 1, '--contains'); this.printBranchesWithout(args[0]); return; } if (this.commandOptions['-f']) { args = this.commandOptions['-f']; this.twoArgsImpliedHead(args, '-f'); // we want to force a branch somewhere this.forceBranch(args[0], args[1]); return; } if (this.generalArgs.length === 0) { this.printBranches(this.getBranches()); return; } this.twoArgsImpliedHead(this.generalArgs); this.branch(this.generalArgs[0], this.generalArgs[1]); }; GitEngine.prototype.forceBranch = function(branchName, where) { // if branchname doesn't exist... if (!this.refs[branchName]) { this.branch(branchName, where); } var branch = this.resolveID(branchName); if (branch.get('type') !== 'branch') { throw new GitError({ msg: "Can't force move anything but a branch!!" }); } var whereCommit = this.getCommitFromRef(where); this.setTargetLocation(branch, whereCommit); }; GitEngine.prototype.branch = function(name, ref) { var target = this.getCommitFromRef(ref); this.makeBranch(name, target); }; GitEngine.prototype.deleteBranch = function(name) { // trying to delete, lets check our refs var target = this.resolveID(name); if (target.get('type') !== 'branch') { throw new GitError({ msg: "You can't delete things that arent branches with branch command" }); } if (target.get('id') == 'master') { throw new GitError({ msg: "You can't delete the master branch!" }); } if (this.HEAD.get('target') === target) { throw new GitError({ msg: "Cannot delete the branch you are currently on" }); } // now we know it's a branch var branch = target; this.branchCollection.remove(branch); this.refs[branch.get('id')] = undefined; delete this.refs[branch.get('id')]; if (branch.get('visBranch')) { branch.get('visBranch').remove(); } }; GitEngine.prototype.unescapeQuotes = function(str) { return str.replace(/'/g, "'"); }; GitEngine.prototype.dispatch = function(command, callback) { // current command, options, and args are stored in the gitEngine // for easy reference during processing. this.command = command; this.commandOptions = command.get('supportedMap'); this.generalArgs = command.get('generalArgs'); // set up the animation queue var whenDone = _.bind(function() { command.set('status', 'finished'); callback(); }, this); this.animationQueue = new AnimationQueue({ callback: whenDone }); command.set('status', 'processing'); try { var methodName = command.get('method').replace(/-/g, '') + 'Starter'; this[methodName](); } catch (err) { if (err instanceof GitError || err instanceof CommandResult) { // short circuit animation by just setting error and returning command.set('error', err); callback(); return; } else { throw err; } } // only add the refresh if we didn't do manual animations if (!this.animationQueue.get('animations').length && !this.animationQueue.get('defer')) { animationFactory.refreshTree(this.animationQueue, this.gitVisuals); } // animation queue will call the callback when its done if (!this.animationQueue.get('defer')) { this.animationQueue.start(); } }; GitEngine.prototype.showStarter = function() { this.oneArgImpliedHead(this.generalArgs); this.show(this.generalArgs[0]); }; GitEngine.prototype.show = function(ref) { var commit = this.getCommitFromRef(ref); throw new CommandResult({ msg: commit.getShowEntry() }); }; GitEngine.prototype.statusStarter = function() { var lines = []; if (this.getDetachedHead()) { lines.push('Detached Head!'); } else { var branchName = this.HEAD.get('target').get('id'); lines.push('On branch ' + branchName); } lines.push('Changes to be committed:'); lines.push(''); lines.push(' modified: cal/OskiCostume.stl'); lines.push(''); lines.push('Ready to commit! (as always in this demo)'); var msg = ''; _.each(lines, function(line) { msg += '# ' + line + '\n'; }); throw new CommandResult({ msg: msg }); }; GitEngine.prototype.logStarter = function() { if (this.generalArgs.length == 2) { // do fancy git log branchA ^branchB if (this.generalArgs[1][0] == '^') { this.logWithout(this.generalArgs[0], this.generalArgs[1]); } else { throw new GitError({ msg: 'I need a not branch (^branchName) when getting two arguments!' }); } } this.oneArgImpliedHead(this.generalArgs); this.log(this.generalArgs[0]); }; GitEngine.prototype.logWithout = function(ref, omitBranch) { // slice off the ^branch omitBranch = omitBranch.slice(1); this.log(ref, this.getUpstreamSet(omitBranch)); }; GitEngine.prototype.log = function(ref, omitSet) { // omit set is for doing stuff like git log branchA ^branchB omitSet = omitSet || {}; // first get the commit we referenced var commit = this.getCommitFromRef(ref); // then get as many far back as we can from here, order by commit date var toDump = []; var pQueue = [commit]; var seen = {}; while (pQueue.length) { var popped = pQueue.shift(0); if (seen[popped.get('id')] || omitSet[popped.get('id')]) { continue; } seen[popped.get('id')] = true; toDump.push(popped); if (popped.get('parents') && popped.get('parents').length) { pQueue = pQueue.concat(popped.get('parents')); } } // now go through and collect logs var bigLogStr = ''; _.each(toDump, function(c) { bigLogStr += c.getLogEntry(); }, this); throw new CommandResult({ msg: bigLogStr }); }; 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.getCommonAncestor = function(ancestor, cousin) { if (this.isUpstreamOf(cousin, ancestor)) { throw new Error('Dont use common ancestor if we are upstream!'); } 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 = queue.concat(here.get('parents')); } throw new Error('something has gone very wrong... two nodes arent connected!'); }; 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 ancestorID = commit.get('id'); var queue = [commit]; var exploredSet = {}; exploredSet[ancestorID] = true; var addToExplored = function(rent) { exploredSet[rent.get('id')] = true; queue.push(rent); }; while (queue.length) { var here = queue.pop(); var rents = here.get('parents'); _.each(rents, addToExplored); } return exploredSet; }; var Ref = Backbone.Model.extend({ initialize: function() { if (!this.get('target')) { throw new Error('must be initialized with target'); } if (!this.get('id')) { throw new Error('must be given an id'); } this.set('type', 'general ref'); if (this.get('id') == 'HEAD') { this.set('lastLastTarget', null); this.set('lastTarget', this.get('target')); // have HEAD remember where it is for checkout - this.on('change:target', this.targetChanged, this); } }, targetChanged: function(model, targetValue, ev) { // push our little 3 stack back. we need to do this because // backbone doesn't give you what the value WAS, only what it was changed // TO this.set('lastLastTarget', this.get('lastTarget')); this.set('lastTarget', targetValue); }, toString: function() { return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target')); } }); var Branch = Ref.extend({ defaults: { visBranch: null }, initialize: function() { Ref.prototype.initialize.call(this); this.set('type', 'branch'); } }); var Commit = Backbone.Model.extend({ defaults: { type: 'commit', children: null, parents: null, author: 'Peter Cottle', createTime: null, commitMessage: null, visNode: null, gitVisuals: null }, constants: { circularFields: ['gitVisuals', 'visNode', 'children'] }, getLogEntry: function() { // for now we are just joining all these things with newlines which // will get placed by paragraph tags. Not really a fan of this, but // it's better than making an entire template and all that jazz return [ 'Author: ' + this.get('author'), 'Date: ' + this.get('createTime'), '' + 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', '