(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("/async.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.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("/git.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactoryModule = require('./animationFactory'); var animationFactory = new AnimationFactoryModule.AnimationFactory(); var Main = require('./main'); var AnimationQueue = require('./async').AnimationQueue; var InteractiveRebaseView = require('./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() { if (this.commandOptions['-b']) { // the user is really trying to just make a branch and then switch to it. so first: var args = this.commandOptions['-b']; 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']) { var 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() { // 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']) { var args = this.commandOptions['--contains']; this.validateArgBounds(args, 1, 1, '--contains'); this.printBranchesWithout(args[0]); return; } if (this.commandOptions['-f']) { var 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; while (queue.length) { var here = queue.pop(); var rents = here.get('parents'); _.each(rents, function(rent) { exploredSet[rent.get('id')] = true; queue.push(rent); }); } return exploredSet; }; var Ref = Backbone.Model.extend({ initialize: function() { if (!this.get('target')) { 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("/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('./async').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("/main.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('./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("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); 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('./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("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit; var Branch = require('./git').Branch; var Main = require('./main'); var Command = require('./commandModel').Command; var CommandEntry = require('./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('./main').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("/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', '
', 'Usage:', _.escape('\t git []'), '
', 'Supported commands:', '
', ]; var commands = OptionParser.prototype.getMasterOptionMap(); // build up a nice display of what we support _.each(commands, function(commandOptions, command) { lines.push('git ' + command); _.each(commandOptions, function(vals, optionName) { lines.push('\t ' + optionName); }, this); }, this); // format and throw var msg = lines.join('\n'); msg = msg.replace(/\t/g, '   '); throw new CommandResult({ msg: msg }); }], [/^refresh$/, function() { events.trigger('refreshTree'); throw new CommandResult({ msg: "Refreshing tree..." }); }], [/^rollup (\d+)$/, function(bits) { // go roll up these commands by joining them with semicolons events.trigger('rollupCommands', bits[1]); throw new CommandResult({ msg: 'Commands combined!' }); }] ]; }, parse: function() { var str = this.get('rawStr'); // first if the string is empty, they just want a blank line if (!str.length) { throw new CommandResult({msg: ""}); } // then check if it's one of our sandbox commands _.each(this.getSandboxCommands(), function(tuple) { var regex = tuple[0]; var results = regex.exec(str); if (results) { tuple[1](results); } }); // then check if shortcut exists, and replace, but // preserve options if so _.each(this.getShortcutMap(), function(regex, method) { var results = regex.exec(str); if (results) { str = method + ' ' + str.slice(results[0].length); } }); // see if begins with git if (str.slice(0,3) !== 'git') { throw new CommandProcessError({ msg: 'That command is not supported, sorry!' }); } // ok, we have a (probably) valid command. actually parse it this.gitParse(str); }, gitParse: function(str) { // now slice off command part var fullCommand = str.slice('git '.length); // see if we support this particular command _.each(this.getRegexMap(), function(regex, method) { if (regex.exec(fullCommand)) { this.set('options', fullCommand.slice(method.length + 1)); this.set('method', method); // we should stop iterating, but the regex will only match // one command in practice. we could stop iterating if we used // jqeurys for each but im using underscore (for no real reason other // than style) } }, this); if (!this.get('method')) { throw new CommandProcessError({ msg: "Sorry, this demo does not support that git command: " + fullCommand }); } // parse off the options and assemble the map / general args var optionParser = new OptionParser(this.get('method'), this.get('options')); // steal these away so we can be completely JSON this.set('generalArgs', optionParser.generalArgs); this.set('supportedMap', optionParser.supportedMap); }, }); /** * OptionParser */ function OptionParser(method, options) { this.method = method; this.rawOptions = options; this.supportedMap = this.getMasterOptionMap()[method]; if (this.supportedMap === undefined) { throw new Error('No option map for ' + method); } this.generalArgs = []; this.explodeAndSet(); } OptionParser.prototype.getMasterOptionMap = function() { // here a value of false means that we support it, even if its just a // pass-through option. If the value is not here (aka will be undefined // when accessed), we do not support it. return { commit: { '--amend': false, '-a': false, // warning '-am': false, // warning '-m': false }, status: {}, log: {}, add: {}, 'cherry-pick': {}, branch: { '-d': false, '-D': false, '-f': false, '--contains': false }, checkout: { '-b': false, '-B': false, '-': false }, reset: { '--hard': false, '--soft': false, // this will raise an error but we catch it in gitEngine }, merge: {}, rebase: { '-i': false // the mother of all options }, revert: {}, show: {} }; }; OptionParser.prototype.explodeAndSet = function() { // split on spaces, except when inside quotes var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/g) || []; for (var i = 0; i < exploded.length; i++) { var part = exploded[i]; if (part.slice(0,1) == '-') { // it's an option, check supportedMap if (this.supportedMap[part] === undefined) { throw new CommandProcessError({ msg: 'The option "' + part + '" is not supported' }); } // go through and include all the next args until we hit another option or the end var optionArgs = []; var next = i + 1; while (next < exploded.length && exploded[next].slice(0,1) != '-') { optionArgs.push(exploded[next]); next += 1; } i = next - 1; // **phew** we are done grabbing those. theseArgs is truthy even with an empty array this.supportedMap[part] = optionArgs; } else { // must be a general arg this.generalArgs.push(part); } } // done! }; // command entry is for the commandview var CommandEntry = Backbone.Model.extend({ defaults: { text: '' }, localStorage: new Backbone.LocalStorage('CommandEntries') }); exports.CommandEntry = CommandEntry; exports.Command = Command; }); require.define("/errors.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("/tree.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); var GRAPHICS = require('./constants').GRAPHICS; var randomHueString = function() { var hue = Math.random(); var str = 'hsb(' + String(hue) + ',0.7,1)'; return str; }; var VisBase = Backbone.Model.extend({ removeKeys: function(keys) { _.each(keys, function(key) { if (this.get(key)) { this.get(key).remove(); } }, this); } }); var VisBranch = VisBase.extend({ defaults: { pos: null, text: null, rect: null, arrow: null, isHead: false, flip: 1, fill: GRAPHICS.rectFill, stroke: GRAPHICS.rectStroke, 'stroke-width': GRAPHICS.rectStrokeWidth, offsetX: GRAPHICS.nodeRadius * 4.75, offsetY: 0, arrowHeight: 14, arrowInnerSkew: 0, arrowEdgeHeight: 6, arrowLength: 14, arrowOffsetFromCircleX: 10, vPad: 5, hPad: 5, animationSpeed: GRAPHICS.defaultAnimationTime, animationEasing: GRAPHICS.defaultEasing }, validateAtInit: function() { if (!this.get('branch')) { throw new Error('need a branch!'); } }, getID: function() { return this.get('branch').get('id'); }, initialize: function() { this.validateAtInit(); // shorthand notation for the main objects this.gitVisuals = this.get('gitVisuals'); this.gitEngine = this.get('gitEngine'); if (!this.gitEngine) { console.log('throw damnit'); throw new Error('asd'); } this.get('branch').set('visBranch', this); var id = this.get('branch').get('id'); if (id == 'HEAD') { // switch to a head ref this.set('isHead', true); this.set('flip', -1); this.set('fill', GRAPHICS.headRectFill); } else if (id !== 'master') { // we need to set our color to something random this.set('fill', randomHueString()); } }, getCommitPosition: function() { var commit = this.gitEngine.getCommitFromRef(this.get('branch')); var visNode = commit.get('visNode'); return visNode.getScreenCoords(); }, getBranchStackIndex: function() { if (this.get('isHead')) { // head is never stacked with other branches return 0; } var myArray = this.getBranchStackArray(); var index = -1; _.each(myArray, function(branch, i) { if (branch.obj == this.get('branch')) { index = i; } }, this); return index; }, getBranchStackLength: function() { if (this.get('isHead')) { // head is always by itself return 1; } return this.getBranchStackArray().length; }, getBranchStackArray: function() { var arr = this.gitVisuals.branchStackMap[this.get('branch').get('target').get('id')]; if (arr === undefined) { // this only occurs when we are generating graphics inside of // a new Branch instantiation, so we need to force the update this.gitVisuals.calcBranchStacks(); return this.getBranchStackArray(); } return arr; }, getTextPosition: function() { var pos = this.getCommitPosition(); // then order yourself accordingly. we use alphabetical sorting // so everything is independent var myPos = this.getBranchStackIndex(); return { x: pos.x + this.get('flip') * this.get('offsetX'), y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY') }; }, getRectPosition: function() { var pos = this.getTextPosition(); var f = this.get('flip'); // first get text width and height var textSize = this.getTextSize(); return { x: pos.x - 0.5 * textSize.w - this.get('hPad'), y: pos.y - 0.5 * textSize.h - this.get('vPad') }; }, getArrowPath: function() { // should make these util functions... var offset2d = function(pos, x, y) { return { x: pos.x + x, y: pos.y + y }; }; var toStringCoords = function(pos) { return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); }; var f = this.get('flip'); var arrowTip = offset2d(this.getCommitPosition(), f * this.get('arrowOffsetFromCircleX'), 0 ); var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight')); var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight')); var arrowInnerUp = offset2d(arrowEdgeUp, f * this.get('arrowInnerSkew'), this.get('arrowEdgeHeight') ); var arrowInnerLow = offset2d(arrowEdgeLow, f * this.get('arrowInnerSkew'), -this.get('arrowEdgeHeight') ); var tailLength = 49; var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0); var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0); var pathStr = ''; pathStr += 'M' + toStringCoords(arrowStartUp) + ' '; var coords = [ arrowInnerUp, arrowEdgeUp, arrowTip, arrowEdgeLow, arrowInnerLow, arrowStartLow ]; _.each(coords, function(pos) { pathStr += 'L' + toStringCoords(pos) + ' '; }, this); pathStr += 'z'; return pathStr; }, getTextSize: function() { var getTextWidth = function(visBranch) { var textNode = visBranch.get('text').node; return (textNode === null) ? 1 : textNode.clientWidth; }; var textNode = this.get('text').node; if (this.get('isHead')) { // HEAD is a special case return { w: textNode.clientWidth, h: textNode.clientHeight }; } var maxWidth = 0; _.each(this.getBranchStackArray(), function(branch) { maxWidth = Math.max(maxWidth, getTextWidth( branch.obj.get('visBranch') )); }); return { w: maxWidth, h: textNode.clientHeight }; }, getSingleRectSize: function() { var textSize = this.getTextSize(); var vPad = this.get('vPad'); var hPad = this.get('hPad'); return { w: textSize.w + vPad * 2, h: textSize.h + hPad * 2 }; }, getRectSize: function() { var textSize = this.getTextSize(); // enforce padding var vPad = this.get('vPad'); var hPad = this.get('hPad'); // number of other branch names we are housing var totalNum = this.getBranchStackLength(); return { w: textSize.w + vPad * 2, h: textSize.h * totalNum * 1.1 + hPad * 2 }; }, getName: function() { var name = this.get('branch').get('id'); var selected = this.gitEngine.HEAD.get('target').get('id'); var add = (selected == name) ? '*' : ''; return name + add; }, nonTextToFront: function() { this.get('arrow').toFront(); this.get('rect').toFront(); }, textToFront: function() { this.get('text').toFront(); }, getFill: function() { // in the easy case, just return your own fill if you are: // - the HEAD ref // - by yourself (length of 1) // - part of a multi branch, but your thing is hidden if (this.get('isHead') || this.getBranchStackLength() == 1 || this.getBranchStackIndex() !== 0) { return this.get('fill'); } // woof. now it's hard, we need to blend hues... return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray()); }, remove: function() { this.removeKeys(['text', 'arrow', 'rect']); // also need to remove from this.gitVisuals this.gitVisuals.removeVisBranch(this); }, genGraphics: function(paper) { var textPos = this.getTextPosition(); var name = this.getName(); var text; // when from a reload, we dont need to generate the text text = paper.text(textPos.x, textPos.y, String(name)); text.attr({ 'font-size': 14, 'font-family': 'Monaco, Courier, font-monospace', opacity: this.getTextOpacity() }); this.set('text', text); var rectPos = this.getRectPosition(); var sizeOfRect = this.getRectSize(); var rect = paper .rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8) .attr(this.getAttributes().rect); this.set('rect', rect); var arrowPath = this.getArrowPath(); var arrow = paper .path(arrowPath) .attr(this.getAttributes().arrow); this.set('arrow', arrow); rect.toFront(); text.toFront(); }, updateName: function() { this.get('text').attr({ text: this.getName() }); }, getNonTextOpacity: function() { if (this.get('isHead')) { return this.gitEngine.getDetachedHead() ? 1 : 0; } return this.getBranchStackIndex() === 0 ? 1 : 0.0; }, getTextOpacity: function() { if (this.get('isHead')) { return this.gitEngine.getDetachedHead() ? 1 : 0; } return 1; }, getAttributes: function() { var nonTextOpacity = this.getNonTextOpacity(); var textOpacity = this.getTextOpacity(); this.updateName(); var textPos = this.getTextPosition(); var rectPos = this.getRectPosition(); var rectSize = this.getRectSize(); var arrowPath = this.getArrowPath(); return { text: { x: textPos.x, y: textPos.y, opacity: textOpacity }, rect: { x: rectPos.x, y: rectPos.y, width: rectSize.w, height: rectSize.h, opacity: nonTextOpacity, fill: this.getFill(), stroke: this.get('stroke'), 'stroke-width': this.get('stroke-width') }, arrow: { path: arrowPath, opacity: nonTextOpacity, fill: this.getFill(), stroke: this.get('stroke'), 'stroke-width': this.get('stroke-width') } }; }, animateUpdatedPos: function(speed, easing) { var attr = this.getAttributes(); this.animateToAttr(attr, speed, easing); }, animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { // an animation of 0 is essentially setting the attribute directly this.animateToAttr(fromAttr, 0); this.animateToAttr(toAttr, speed, easing); }, animateToAttr: function(attr, speed, easing) { if (speed === 0) { this.get('text').attr(attr.text); this.get('rect').attr(attr.rect); this.get('arrow').attr(attr.arrow); return; } var s = speed !== undefined ? speed : this.get('animationSpeed'); var e = easing || this.get('animationEasing'); this.get('text').stop().animate(attr.text, s, e); this.get('rect').stop().animate(attr.rect, s, e); this.get('arrow').stop().animate(attr.arrow, s, e); } }); var VisNode = VisBase.extend({ defaults: { depth: undefined, maxWidth: null, outgoingEdges: null, circle: null, text: null, id: null, pos: null, radius: null, commit: null, animationSpeed: GRAPHICS.defaultAnimationTime, animationEasing: GRAPHICS.defaultEasing, fill: GRAPHICS.defaultNodeFill, 'stroke-width': GRAPHICS.defaultNodeStrokeWidth, stroke: GRAPHICS.defaultNodeStroke }, getID: function() { return this.get('id'); }, validateAtInit: function() { if (!this.get('id')) { throw new Error('need id for mapping'); } if (!this.get('commit')) { throw new Error('need commit for linking'); } if (!this.get('pos')) { this.set('pos', { x: Math.random(), y: Math.random() }); } }, initialize: function() { this.validateAtInit(); // shorthand for the main objects this.gitVisuals = this.get('gitVisuals'); this.gitEngine = this.get('gitEngine'); this.set('outgoingEdges', []); }, setDepth: function(depth) { // for merge commits we need to max the depths across all this.set('depth', Math.max(this.get('depth') || 0, depth)); }, setDepthBasedOn: function(depthIncrement) { if (this.get('depth') === undefined) { debugger; throw new Error('no depth yet!'); } var pos = this.get('pos'); pos.y = this.get('depth') * depthIncrement; }, getMaxWidthScaled: function() { // returns our max width scaled based on if we are visible // from a branch or not var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); var map = { branch: 1, head: 0.3, none: 0.1 }; if (map[stat] === undefined) { throw new Error('bad stat'); } return map[stat] * this.get('maxWidth'); }, toFront: function() { this.get('circle').toFront(); this.get('text').toFront(); }, getOpacity: function() { var map = { 'branch': 1, 'head': GRAPHICS.upstreamHeadOpacity, 'none': GRAPHICS.upstreamNoneOpacity }; var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); if (map[stat] === undefined) { throw new Error('invalid status'); } return map[stat]; }, getTextScreenCoords: function() { return this.getScreenCoords(); }, getAttributes: function() { var pos = this.getScreenCoords(); var textPos = this.getTextScreenCoords(); var opacity = this.getOpacity(); return { circle: { cx: pos.x, cy: pos.y, opacity: opacity, r: this.getRadius(), fill: this.getFill(), 'stroke-width': this.get('stroke-width'), stroke: this.get('stroke') }, text: { x: textPos.x, y: textPos.y, opacity: opacity } }; }, highlightTo: function(visObj, speed, easing) { // a small function to highlight the color of a node for demonstration purposes var color = visObj.get('fill'); var attr = { circle: { fill: color, stroke: color, 'stroke-width': this.get('stroke-width') * 5 }, text: {} }; this.animateToAttr(attr, speed, easing); }, animateUpdatedPosition: function(speed, easing) { var attr = this.getAttributes(); this.animateToAttr(attr, speed, easing); }, animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { // an animation of 0 is essentially setting the attribute directly this.animateToAttr(fromAttr, 0); this.animateToAttr(toAttr, speed, easing); }, animateToSnapshot: function(snapShot, speed, easing) { if (!snapShot[this.getID()]) { return; } this.animateToAttr(snapShot[this.getID()], speed, easing); }, animateToAttr: function(attr, speed, easing) { if (speed === 0) { this.get('circle').attr(attr.circle); this.get('text').attr(attr.text); return; } var s = speed !== undefined ? speed : this.get('animationSpeed'); var e = easing || this.get('animationEasing'); this.get('circle').stop().animate(attr.circle, s, e); this.get('text').stop().animate(attr.text, s, e); // animate the x attribute without bouncing so it looks like there's // gravity in only one direction. Just a small animation polish this.get('circle').animate(attr.circle.cx, s, 'easeInOut'); this.get('text').animate(attr.text.x, s, 'easeInOut'); }, getScreenCoords: function() { var pos = this.get('pos'); return this.gitVisuals.toScreenCoords(pos); }, getRadius: function() { return this.get('radius') || GRAPHICS.nodeRadius; }, getParentScreenCoords: function() { return this.get('commit').get('parents')[0].get('visNode').getScreenCoords(); }, setBirthPosition: function() { // utility method for animating it out from underneath a parent var parentCoords = this.getParentScreenCoords(); this.get('circle').attr({ cx: parentCoords.x, cy: parentCoords.y, opacity: 0, r: 0 }); this.get('text').attr({ x: parentCoords.x, y: parentCoords.y, opacity: 0 }); }, setBirthFromSnapshot: function(beforeSnapshot) { // first get parent attribute // woof bad data access. TODO var parentID = this.get('commit').get('parents')[0].get('visNode').getID(); var parentAttr = beforeSnapshot[parentID]; // then set myself faded on top of parent this.get('circle').attr({ opacity: 0, r: 0, cx: parentAttr.circle.cx, cy: parentAttr.circle.cy }); this.get('text').attr({ opacity: 0, x: parentAttr.text.x, y: parentAttr.text.y }); // then do edges var parentCoords = { x: parentAttr.circle.cx, y: parentAttr.circle.cy }; this.setOutgoingEdgesBirthPosition(parentCoords); }, setBirth: function() { this.setBirthPosition(); this.setOutgoingEdgesBirthPosition(this.getParentScreenCoords()); }, setOutgoingEdgesOpacity: function(opacity) { _.each(this.get('outgoingEdges'), function(edge) { edge.setOpacity(opacity); }); }, animateOutgoingEdgesToAttr: function(snapShot, speed, easing) { _.each(this.get('outgoingEdges'), function(edge) { var attr = snapShot[edge.getID()]; edge.animateToAttr(attr); }, this); }, animateOutgoingEdges: function(speed, easing) { _.each(this.get('outgoingEdges'), function(edge) { edge.animateUpdatedPath(speed, easing); }, this); }, animateOutgoingEdgesFromSnapshot: function(snapshot, speed, easing) { _.each(this.get('outgoingEdges'), function(edge) { var attr = snapshot[edge.getID()]; edge.animateToAttr(attr, speed, easing); }, this); }, setOutgoingEdgesBirthPosition: function(parentCoords) { _.each(this.get('outgoingEdges'), function(edge) { var headPos = edge.get('head').getScreenCoords(); var path = edge.genSmoothBezierPathStringFromCoords(parentCoords, headPos); edge.get('path').stop().attr({ path: path, opacity: 0 }); }, this); }, parentInFront: function() { // woof! talk about bad data access this.get('commit').get('parents')[0].get('visNode').toFront(); }, getFontSize: function(str) { if (str.length < 3) { return 12; } else if (str.length < 5) { return 10; } else { return 8; } }, getFill: function() { // first get our status, might be easy from this var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); if (stat == 'head') { return GRAPHICS.headRectFill; } else if (stat == 'none') { return GRAPHICS.orphanNodeFill; } // now we need to get branch hues return this.gitVisuals.getBlendedHuesForCommit(this.get('commit')); }, attachClickHandlers: function() { var commandStr = 'git show ' + this.get('commit').get('id'); _.each([this.get('circle'), this.get('text')], function(rObj) { rObj.click(function() { Main.getEvents().trigger('processCommandFromEvent', commandStr); }); }); }, setOpacity: function(opacity) { opacity = (opacity === undefined) ? 1 : opacity; // set the opacity on my stuff var keys = ['circle', 'text']; _.each(keys, function(key) { this.get(key).attr({ opacity: opacity }); }, this); }, remove: function() { this.removeKeys(['circle'], ['text']); // needs a manual removal of text for whatever reason this.get('text').remove(); this.gitVisuals.removeVisNode(this); }, removeAll: function() { this.remove(); _.each(this.get('outgoingEdges'), function(edge) { edge.remove(); }, this); }, genGraphics: function() { var paper = this.gitVisuals.paper; var pos = this.getScreenCoords(); var textPos = this.getTextScreenCoords(); var circle = paper.circle( pos.x, pos.y, this.getRadius() ).attr(this.getAttributes().circle); var text = paper.text(textPos.x, textPos.y, String(this.get('id'))); text.attr({ 'font-size': this.getFontSize(this.get('id')), 'font-weight': 'bold', 'font-family': 'Monaco, Courier, font-monospace', opacity: this.getOpacity() }); this.set('circle', circle); this.set('text', text); this.attachClickHandlers(); } }); var VisEdge = VisBase.extend({ defaults: { tail: null, head: null, animationSpeed: GRAPHICS.defaultAnimationTime, animationEasing: GRAPHICS.defaultEasing }, validateAtInit: function() { var required = ['tail', 'head']; _.each(required, function(key) { if (!this.get(key)) { throw new Error(key + ' is required!'); } }, this); }, getID: function() { return this.get('tail').get('id') + '.' + this.get('head').get('id'); }, initialize: function() { this.validateAtInit(); // shorthand for the main objects this.gitVisuals = this.get('gitVisuals'); this.gitEngine = this.get('gitEngine'); this.get('tail').get('outgoingEdges').push(this); }, remove: function() { this.removeKeys(['path']); this.gitVisuals.removeVisEdge(this); }, genSmoothBezierPathString: function(tail, head) { var tailPos = tail.getScreenCoords(); var headPos = head.getScreenCoords(); return this.genSmoothBezierPathStringFromCoords(tailPos, headPos); }, genSmoothBezierPathStringFromCoords: function(tailPos, headPos) { // we need to generate the path and control points for the bezier. format // is M(move abs) C (curve to) (control point 1) (control point 2) (final point) // the control points have to be __below__ to get the curve starting off straight. var coords = function(pos) { return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); }; var offset = function(pos, dir, delta) { delta = delta || GRAPHICS.curveControlPointOffset; return { x: pos.x, y: pos.y + delta * dir }; }; var offset2d = function(pos, x, y) { return { x: pos.x + x, y: pos.y + y }; }; // first offset tail and head by radii tailPos = offset(tailPos, -1, this.get('tail').getRadius()); headPos = offset(headPos, 1, this.get('head').getRadius()); var str = ''; // first move to bottom of tail str += 'M' + coords(tailPos) + ' '; // start bezier str += 'C'; // then control points above tail and below head str += coords(offset(tailPos, -1)) + ' '; str += coords(offset(headPos, 1)) + ' '; // now finish str += coords(headPos); // arrow head var delta = GRAPHICS.arrowHeadSize || 10; str += ' L' + coords(offset2d(headPos, -delta, delta)); str += ' L' + coords(offset2d(headPos, delta, delta)); str += ' L' + coords(headPos); // then go back, so we can fill correctly str += 'C'; str += coords(offset(headPos, 1)) + ' '; str += coords(offset(tailPos, -1)) + ' '; str += coords(tailPos); return str; }, getBezierCurve: function() { return this.genSmoothBezierPathString(this.get('tail'), this.get('head')); }, getStrokeColor: function() { return GRAPHICS.visBranchStrokeColorNone; }, setOpacity: function(opacity) { opacity = (opacity === undefined) ? 1 : opacity; this.get('path').attr({opacity: opacity}); }, genGraphics: function(paper) { var pathString = this.getBezierCurve(); var path = paper.path(pathString).attr({ 'stroke-width': GRAPHICS.visBranchStrokeWidth, 'stroke': this.getStrokeColor(), 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'fill': this.getStrokeColor() }); path.toBack(); this.set('path', path); }, getOpacity: function() { var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail')); var map = { 'branch': 1, 'head': GRAPHICS.edgeUpstreamHeadOpacity, 'none': GRAPHICS.edgeUpstreamNoneOpacity }; if (map[stat] === undefined) { throw new Error('bad stat'); } return map[stat]; }, getAttributes: function() { var newPath = this.getBezierCurve(); var opacity = this.getOpacity(); return { path: { path: newPath, opacity: opacity } }; }, animateUpdatedPath: function(speed, easing) { var attr = this.getAttributes(); this.animateToAttr(attr, speed, easing); }, animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { // an animation of 0 is essentially setting the attribute directly this.animateToAttr(fromAttr, 0); this.animateToAttr(toAttr, speed, easing); }, animateToAttr: function(attr, speed, easing) { if (speed === 0) { this.get('path').attr(attr.path); return; } this.get('path').toBack(); this.get('path').stop().animate( attr.path, speed !== undefined ? speed : this.get('animationSpeed'), easing || this.get('animationEasing') ); } }); var VisEdgeCollection = Backbone.Collection.extend({ model: VisEdge }); var VisBranchCollection = Backbone.Collection.extend({ model: VisBranch }); exports.VisEdgeCollection = VisEdgeCollection; exports.VisBranchCollection = VisBranchCollection; exports.VisNode = VisNode; exports.VisEdge = VisEdge; exports.VisBranch = VisBranch; }); require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection; var Main = require('./main'); var Command = require('./commandModel').Command; var CommandEntry = require('./commandModel').CommandEntry; var Errors = require('./errors'); var Warning = Errors.Warning; var CommandPromptView = Backbone.View.extend({ initialize: function(options) { this.collection = options.collection; // uses local storage this.commands = new CommandEntryCollection(); this.commands.fetch({ success: _.bind(function() { // reverse the commands. this is ugly but needs to be done... var commands = []; this.commands.each(function(c) { commands.push(c); }); commands.reverse(); this.commands.reset(); _.each(commands, function(c) { this.commands.add(c); }, this); }, this) }); this.index = -1; this.commandSpan = this.$('#prompt span.command')[0]; this.commandCursor = this.$('#prompt span.cursor')[0]; // this is evil, but we will refer to HTML outside the document // and attach a click event listener so we can focus / unfocus $(document).delegate('#commandLineHistory', 'click', _.bind(function() { this.focus(); }, this)); $(document).delegate('#commandTextField', 'blur', _.bind(function() { this.blur(); }, this)); Main.getEvents().on('processCommandFromEvent', this.addToCollection, this); Main.getEvents().on('submitCommandValueFromEvent', this.submitValue, this); Main.getEvents().on('rollupCommands', this.rollupCommands, this); // hacky timeout focus setTimeout(_.bind(function() { this.focus(); }, this), 100); }, events: { 'keydown #commandTextField': 'onKey', 'keyup #commandTextField': 'onKeyUp', 'blur #commandTextField': 'hideCursor', 'focus #commandTextField': 'showCursor' }, blur: function() { $(this.commandCursor).toggleClass('shown', false); }, focus: function() { this.$('#commandTextField').focus(); this.showCursor(); }, hideCursor: function() { this.toggleCursor(false); }, showCursor: function() { this.toggleCursor(true); }, toggleCursor: function(state) { $(this.commandCursor).toggleClass('shown', state); }, onKey: function(e) { var el = e.srcElement; this.updatePrompt(el) }, onKeyUp: function(e) { this.onKey(e); // we need to capture some of these events. // WARNING: this key map is not internationalized :( var keyMap = { // enter 13: _.bind(function() { this.submit(); }, this), // up 38: _.bind(function() { this.commandSelectChange(1); }, this), // down 40: _.bind(function() { this.commandSelectChange(-1); }, this) }; if (keyMap[e.which] !== undefined) { e.preventDefault(); keyMap[e.which](); this.onKey(e); } }, badHtmlEncode: function(text) { return text.replace(/&/g,'&') .replace(/= this.commands.length || this.index < 0) { this.clear(); this.index = -1; return; } // yay! we actually can display something var commandEntry = this.commands.toArray()[this.index].get('text'); this.setTextField(commandEntry); }, clearLocalStorage: function() { this.commands.each(function(c) { Backbone.sync('delete', c, function() { }); }, this); localStorage.setItem('CommandEntries', ''); }, setTextField: function(value) { this.$('#commandTextField').val(value); }, clear: function() { this.setTextField(''); }, submit: function() { var value = this.$('#commandTextField').val().replace('\n', ''); this.clear(); this.submitValue(value); }, rollupCommands: function(numBack) { var which = this.commands.toArray().slice(1, Number(numBack) + 1); which.reverse(); var str = ''; _.each(which, function(commandEntry) { str += commandEntry.get('text') + ';'; }, this); console.log('the str', str); var rolled = new CommandEntry({text: str}); this.commands.unshift(rolled); Backbone.sync('create', rolled, function() { }); }, submitValue: function(value) { // we should add if it's not a blank line and this is a new command... // or if we edited the command var shouldAdd = (value.length && this.index == -1) || ((value.length && this.index !== -1 && this.commands.toArray()[this.index].get('text') !== value)); if (shouldAdd) { var commandEntry = new CommandEntry({text: value}); this.commands.unshift(commandEntry); // store to local storage Backbone.sync('create', commandEntry, function() { }); // if our length is too egregious, reset if (this.commands.length > 100) { this.clearLocalStorage(); } } this.index = -1; // split commands on semicolon _.each(value.split(';'), _.bind(function(command, index) { command = _.escape(command); command = command .replace(/^(\s+)/, '') .replace(/(\s+)$/, '') .replace(/"/g, '"') .replace(/'/g, "'"); if (index > 0 && !command.length) { return; } this.addToCollection(command); }, this)); }, addToCollection: function(value) { var command = new Command({ rawStr: value }); this.collection.add(command); } }); // This is the view for all commands -- it will represent // their status (inqueue, processing, finished, error), // their value ("git commit --amend"), // and the result (either errors or warnings or whatever) var CommandView = Backbone.View.extend({ tagName: 'div', model: Command, template: _.template($('#command-template').html()), events: { 'click': 'clicked' }, clicked: function(e) { }, initialize: function() { this.model.bind('change', this.wasChanged, this); this.model.bind('destroy', this.remove, this); }, wasChanged: function(model, changeEvent) { // for changes that are just comestic, we actually only want to toggle classes // with jquery rather than brutally delete a html of HTML var changes = changeEvent.changes; var changeKeys = _.keys(changes); if (_.difference(changeKeys, ['status']) == 0) { this.updateStatus(); } else if (_.difference(changeKeys, ['error']) == 0) { // the above will this.render(); } else { this.render(); } }, updateStatus: function() { var statuses = ['inqueue', 'processing', 'finished']; var toggleMap = {}; _.each(statuses, function(status) { toggleMap[status] = false; }); toggleMap[this.model.get('status')] = true; var query = this.$('p.commandLine'); _.each(toggleMap, function(value, key) { query.toggleClass(key, value); }); }, render: function() { var json = _.extend( { resultType: '', result: '', formattedWarnings: this.model.getFormattedWarnings() }, this.model.toJSON() ); this.$el.html(this.template(json)); return this; }, remove: function() { $(this.el).hide(); } }); var CommandLineHistoryView = Backbone.View.extend({ initialize: function(options) { this.collection = options.collection; this.collection.on('add', this.addOne, this); this.collection.on('reset', this.addAll, this); this.collection.on('all', this.render, this); this.collection.on('change', this.scrollDown, this); Main.getEvents().on('issueWarning', this.addWarning, this); Main.getEvents().on('commandScrollDown', this.scrollDown, this); }, addWarning: function(msg) { var err = new Warning({ msg: msg }); var command = new Command({ error: err, rawStr: 'Warning:' }); this.collection.add(command); }, scrollDown: function() { // if commandDisplay is ever bigger than #terminal, we need to // add overflow-y to terminal and scroll down var cD = $('#commandDisplay')[0]; var t = $('#terminal')[0]; if ($(t).hasClass('scrolling')) { t.scrollTop = t.scrollHeight; return; } if (cD.clientHeight > t.clientHeight) { $(t).css('overflow-y', 'scroll'); $(t).css('overflow-x', 'hidden'); $(t).addClass('scrolling'); t.scrollTop = t.scrollHeight; } }, addOne: function(command) { var view = new CommandView({ model: command }); this.$('#commandDisplay').append(view.render().el); this.scrollDown(); }, addAll: function() { this.collection.each(this.addOne); } }); exports.CommandPromptView = CommandPromptView; exports.CommandLineHistoryView = CommandLineHistoryView; }); require.define("/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("/levels.js",function(require,module,exports,__dirname,__filename,process,global){// static class... function LevelEngine() { } LevelEngine.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) { var result = true; _.each(branches, function(branchName) { result = result && this.compareBranchWithinTrees(treeA, treeB, branchName); }, this); return result; }; LevelEngine.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) { treeA = this.convertTreeSafe(treeA); treeB = this.convertTreeSafe(treeB); this.stripTreeFields([treeA, treeB]); // we need a recursive comparison function to bubble up the branch var recurseCompare = function(commitA, commitB) { // this is the short-circuit base case var result = _.isEqual(commitA, commitB); if (!result) { return false; } // we loop through each parent ID. we sort the parent ID's beforehand // so the index lookup is valid _.each(commitA.parents, function(pAid, index) { var pBid = commitB.parents[index]; var childA = treeA.commits[pAid]; var childB = treeB.commits[pBid]; result = result && recurseCompare(childA, childB); }, this); // if each of our children recursively are equal, we are good return result; }; var branchA = treeA.branches[branchName]; var branchB = treeB.branches[branchName]; return _.isEqual(branchA, branchB) && recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); }; LevelEngine.prototype.convertTreeSafe = function(tree) { if (typeof tree == 'string') { return JSON.parse(unescape(tree)); } return tree; }; LevelEngine.prototype.stripTreeFields = function(trees) { var stripFields = ['createTime', 'author', 'commitMessage']; var sortFields = ['children', 'parents']; _.each(trees, function(tree) { _.each(tree.commits, function(commit) { _.each(stripFields, function(field) { commit[field] = undefined; }); _.each(sortFields, function(field) { if (commit[field]) { commit[field] = commit[field].sort(); } }); }); }); }; LevelEngine.prototype.compareTrees = function(treeA, treeB) { treeA = this.convertTreeSafe(treeA); treeB = this.convertTreeSafe(treeB); // now we need to strip out the fields we don't care about, aka things // like createTime, message, author this.stripTreeFields([treeA, treeB]); return _.isEqual(treeA, treeB); }; var levelEngine = new LevelEngine(); exports.LevelEngine = LevelEngine; }); require.define("/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('./async').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("/animationFactory.js"); require.define("/async.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("/async.js"); require.define("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit; var Branch = require('./git').Branch; var Main = require('./main'); var Command = require('./commandModel').Command; var CommandEntry = require('./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('./main').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("/collections.js"); require.define("/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', '
', 'Usage:', _.escape('\t git []'), '
', 'Supported commands:', '
', ]; var commands = OptionParser.prototype.getMasterOptionMap(); // build up a nice display of what we support _.each(commands, function(commandOptions, command) { lines.push('git ' + command); _.each(commandOptions, function(vals, optionName) { lines.push('\t ' + optionName); }, this); }, this); // format and throw var msg = lines.join('\n'); msg = msg.replace(/\t/g, '   '); throw new CommandResult({ msg: msg }); }], [/^refresh$/, function() { events.trigger('refreshTree'); throw new CommandResult({ msg: "Refreshing tree..." }); }], [/^rollup (\d+)$/, function(bits) { // go roll up these commands by joining them with semicolons events.trigger('rollupCommands', bits[1]); throw new CommandResult({ msg: 'Commands combined!' }); }] ]; }, parse: function() { var str = this.get('rawStr'); // first if the string is empty, they just want a blank line if (!str.length) { throw new CommandResult({msg: ""}); } // then check if it's one of our sandbox commands _.each(this.getSandboxCommands(), function(tuple) { var regex = tuple[0]; var results = regex.exec(str); if (results) { tuple[1](results); } }); // then check if shortcut exists, and replace, but // preserve options if so _.each(this.getShortcutMap(), function(regex, method) { var results = regex.exec(str); if (results) { str = method + ' ' + str.slice(results[0].length); } }); // see if begins with git if (str.slice(0,3) !== 'git') { throw new CommandProcessError({ msg: 'That command is not supported, sorry!' }); } // ok, we have a (probably) valid command. actually parse it this.gitParse(str); }, gitParse: function(str) { // now slice off command part var fullCommand = str.slice('git '.length); // see if we support this particular command _.each(this.getRegexMap(), function(regex, method) { if (regex.exec(fullCommand)) { this.set('options', fullCommand.slice(method.length + 1)); this.set('method', method); // we should stop iterating, but the regex will only match // one command in practice. we could stop iterating if we used // jqeurys for each but im using underscore (for no real reason other // than style) } }, this); if (!this.get('method')) { throw new CommandProcessError({ msg: "Sorry, this demo does not support that git command: " + fullCommand }); } // parse off the options and assemble the map / general args var optionParser = new OptionParser(this.get('method'), this.get('options')); // steal these away so we can be completely JSON this.set('generalArgs', optionParser.generalArgs); this.set('supportedMap', optionParser.supportedMap); }, }); /** * OptionParser */ function OptionParser(method, options) { this.method = method; this.rawOptions = options; this.supportedMap = this.getMasterOptionMap()[method]; if (this.supportedMap === undefined) { throw new Error('No option map for ' + method); } this.generalArgs = []; this.explodeAndSet(); } OptionParser.prototype.getMasterOptionMap = function() { // here a value of false means that we support it, even if its just a // pass-through option. If the value is not here (aka will be undefined // when accessed), we do not support it. return { commit: { '--amend': false, '-a': false, // warning '-am': false, // warning '-m': false }, status: {}, log: {}, add: {}, 'cherry-pick': {}, branch: { '-d': false, '-D': false, '-f': false, '--contains': false }, checkout: { '-b': false, '-B': false, '-': false }, reset: { '--hard': false, '--soft': false, // this will raise an error but we catch it in gitEngine }, merge: {}, rebase: { '-i': false // the mother of all options }, revert: {}, show: {} }; }; OptionParser.prototype.explodeAndSet = function() { // split on spaces, except when inside quotes var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/g) || []; for (var i = 0; i < exploded.length; i++) { var part = exploded[i]; if (part.slice(0,1) == '-') { // it's an option, check supportedMap if (this.supportedMap[part] === undefined) { throw new CommandProcessError({ msg: 'The option "' + part + '" is not supported' }); } // go through and include all the next args until we hit another option or the end var optionArgs = []; var next = i + 1; while (next < exploded.length && exploded[next].slice(0,1) != '-') { optionArgs.push(exploded[next]); next += 1; } i = next - 1; // **phew** we are done grabbing those. theseArgs is truthy even with an empty array this.supportedMap[part] = optionArgs; } else { // must be a general arg this.generalArgs.push(part); } } // done! }; // command entry is for the commandview var CommandEntry = Backbone.Model.extend({ defaults: { text: '' }, localStorage: new Backbone.LocalStorage('CommandEntries') }); exports.CommandEntry = CommandEntry; exports.Command = Command; }); require("/commandModel.js"); require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection; var Main = require('./main'); var Command = require('./commandModel').Command; var CommandEntry = require('./commandModel').CommandEntry; var Errors = require('./errors'); var Warning = Errors.Warning; var CommandPromptView = Backbone.View.extend({ initialize: function(options) { this.collection = options.collection; // uses local storage this.commands = new CommandEntryCollection(); this.commands.fetch({ success: _.bind(function() { // reverse the commands. this is ugly but needs to be done... var commands = []; this.commands.each(function(c) { commands.push(c); }); commands.reverse(); this.commands.reset(); _.each(commands, function(c) { this.commands.add(c); }, this); }, this) }); this.index = -1; this.commandSpan = this.$('#prompt span.command')[0]; this.commandCursor = this.$('#prompt span.cursor')[0]; // this is evil, but we will refer to HTML outside the document // and attach a click event listener so we can focus / unfocus $(document).delegate('#commandLineHistory', 'click', _.bind(function() { this.focus(); }, this)); $(document).delegate('#commandTextField', 'blur', _.bind(function() { this.blur(); }, this)); Main.getEvents().on('processCommandFromEvent', this.addToCollection, this); Main.getEvents().on('submitCommandValueFromEvent', this.submitValue, this); Main.getEvents().on('rollupCommands', this.rollupCommands, this); // hacky timeout focus setTimeout(_.bind(function() { this.focus(); }, this), 100); }, events: { 'keydown #commandTextField': 'onKey', 'keyup #commandTextField': 'onKeyUp', 'blur #commandTextField': 'hideCursor', 'focus #commandTextField': 'showCursor' }, blur: function() { $(this.commandCursor).toggleClass('shown', false); }, focus: function() { this.$('#commandTextField').focus(); this.showCursor(); }, hideCursor: function() { this.toggleCursor(false); }, showCursor: function() { this.toggleCursor(true); }, toggleCursor: function(state) { $(this.commandCursor).toggleClass('shown', state); }, onKey: function(e) { var el = e.srcElement; this.updatePrompt(el) }, onKeyUp: function(e) { this.onKey(e); // we need to capture some of these events. // WARNING: this key map is not internationalized :( var keyMap = { // enter 13: _.bind(function() { this.submit(); }, this), // up 38: _.bind(function() { this.commandSelectChange(1); }, this), // down 40: _.bind(function() { this.commandSelectChange(-1); }, this) }; if (keyMap[e.which] !== undefined) { e.preventDefault(); keyMap[e.which](); this.onKey(e); } }, badHtmlEncode: function(text) { return text.replace(/&/g,'&') .replace(/= this.commands.length || this.index < 0) { this.clear(); this.index = -1; return; } // yay! we actually can display something var commandEntry = this.commands.toArray()[this.index].get('text'); this.setTextField(commandEntry); }, clearLocalStorage: function() { this.commands.each(function(c) { Backbone.sync('delete', c, function() { }); }, this); localStorage.setItem('CommandEntries', ''); }, setTextField: function(value) { this.$('#commandTextField').val(value); }, clear: function() { this.setTextField(''); }, submit: function() { var value = this.$('#commandTextField').val().replace('\n', ''); this.clear(); this.submitValue(value); }, rollupCommands: function(numBack) { var which = this.commands.toArray().slice(1, Number(numBack) + 1); which.reverse(); var str = ''; _.each(which, function(commandEntry) { str += commandEntry.get('text') + ';'; }, this); console.log('the str', str); var rolled = new CommandEntry({text: str}); this.commands.unshift(rolled); Backbone.sync('create', rolled, function() { }); }, submitValue: function(value) { // we should add if it's not a blank line and this is a new command... // or if we edited the command var shouldAdd = (value.length && this.index == -1) || ((value.length && this.index !== -1 && this.commands.toArray()[this.index].get('text') !== value)); if (shouldAdd) { var commandEntry = new CommandEntry({text: value}); this.commands.unshift(commandEntry); // store to local storage Backbone.sync('create', commandEntry, function() { }); // if our length is too egregious, reset if (this.commands.length > 100) { this.clearLocalStorage(); } } this.index = -1; // split commands on semicolon _.each(value.split(';'), _.bind(function(command, index) { command = _.escape(command); command = command .replace(/^(\s+)/, '') .replace(/(\s+)$/, '') .replace(/"/g, '"') .replace(/'/g, "'"); if (index > 0 && !command.length) { return; } this.addToCollection(command); }, this)); }, addToCollection: function(value) { var command = new Command({ rawStr: value }); this.collection.add(command); } }); // This is the view for all commands -- it will represent // their status (inqueue, processing, finished, error), // their value ("git commit --amend"), // and the result (either errors or warnings or whatever) var CommandView = Backbone.View.extend({ tagName: 'div', model: Command, template: _.template($('#command-template').html()), events: { 'click': 'clicked' }, clicked: function(e) { }, initialize: function() { this.model.bind('change', this.wasChanged, this); this.model.bind('destroy', this.remove, this); }, wasChanged: function(model, changeEvent) { // for changes that are just comestic, we actually only want to toggle classes // with jquery rather than brutally delete a html of HTML var changes = changeEvent.changes; var changeKeys = _.keys(changes); if (_.difference(changeKeys, ['status']) == 0) { this.updateStatus(); } else if (_.difference(changeKeys, ['error']) == 0) { // the above will this.render(); } else { this.render(); } }, updateStatus: function() { var statuses = ['inqueue', 'processing', 'finished']; var toggleMap = {}; _.each(statuses, function(status) { toggleMap[status] = false; }); toggleMap[this.model.get('status')] = true; var query = this.$('p.commandLine'); _.each(toggleMap, function(value, key) { query.toggleClass(key, value); }); }, render: function() { var json = _.extend( { resultType: '', result: '', formattedWarnings: this.model.getFormattedWarnings() }, this.model.toJSON() ); this.$el.html(this.template(json)); return this; }, remove: function() { $(this.el).hide(); } }); var CommandLineHistoryView = Backbone.View.extend({ initialize: function(options) { this.collection = options.collection; this.collection.on('add', this.addOne, this); this.collection.on('reset', this.addAll, this); this.collection.on('all', this.render, this); this.collection.on('change', this.scrollDown, this); Main.getEvents().on('issueWarning', this.addWarning, this); Main.getEvents().on('commandScrollDown', this.scrollDown, this); }, addWarning: function(msg) { var err = new Warning({ msg: msg }); var command = new Command({ error: err, rawStr: 'Warning:' }); this.collection.add(command); }, scrollDown: function() { // if commandDisplay is ever bigger than #terminal, we need to // add overflow-y to terminal and scroll down var cD = $('#commandDisplay')[0]; var t = $('#terminal')[0]; if ($(t).hasClass('scrolling')) { t.scrollTop = t.scrollHeight; return; } if (cD.clientHeight > t.clientHeight) { $(t).css('overflow-y', 'scroll'); $(t).css('overflow-x', 'hidden'); $(t).addClass('scrolling'); t.scrollTop = t.scrollHeight; } }, addOne: function(command) { var view = new CommandView({ model: command }); this.$('#commandDisplay').append(view.render().el); this.scrollDown(); }, addAll: function() { this.collection.each(this.addOne); } }); exports.CommandPromptView = CommandPromptView; exports.CommandLineHistoryView = CommandLineHistoryView; }); require("/commandViews.js"); require.define("/constants.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("/constants.js"); require.define("/debug.js",function(require,module,exports,__dirname,__filename,process,global){var toGlobalize = { Tree: require('./tree'), Visuals: require('./visuals'), Git: require('./git'), CommandModel: require('./commandModel'), Levels: require('./levels'), Constants: require('./constants'), Collections: require('./collections'), Async: require('./async'), AnimationFactory: require('./animationFactory'), Main: require('./main') }; _.each(toGlobalize, function(module) { _.extend(window, module); }); window.events = toGlobalize.Main.getEvents(); }); require("/debug.js"); require.define("/errors.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("/errors.js"); require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactoryModule = require('./animationFactory'); var animationFactory = new AnimationFactoryModule.AnimationFactory(); var Main = require('./main'); var AnimationQueue = require('./async').AnimationQueue; var InteractiveRebaseView = require('./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() { if (this.commandOptions['-b']) { // the user is really trying to just make a branch and then switch to it. so first: var args = this.commandOptions['-b']; 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']) { var 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() { // 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']) { var args = this.commandOptions['--contains']; this.validateArgBounds(args, 1, 1, '--contains'); this.printBranchesWithout(args[0]); return; } if (this.commandOptions['-f']) { var 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; while (queue.length) { var here = queue.pop(); var rents = here.get('parents'); _.each(rents, function(rent) { exploredSet[rent.get('id')] = true; queue.push(rent); }); } return exploredSet; }; var Ref = Backbone.Model.extend({ initialize: function() { if (!this.get('target')) { 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("/git.js"); require.define("/levels.js",function(require,module,exports,__dirname,__filename,process,global){// static class... function LevelEngine() { } LevelEngine.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) { var result = true; _.each(branches, function(branchName) { result = result && this.compareBranchWithinTrees(treeA, treeB, branchName); }, this); return result; }; LevelEngine.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) { treeA = this.convertTreeSafe(treeA); treeB = this.convertTreeSafe(treeB); this.stripTreeFields([treeA, treeB]); // we need a recursive comparison function to bubble up the branch var recurseCompare = function(commitA, commitB) { // this is the short-circuit base case var result = _.isEqual(commitA, commitB); if (!result) { return false; } // we loop through each parent ID. we sort the parent ID's beforehand // so the index lookup is valid _.each(commitA.parents, function(pAid, index) { var pBid = commitB.parents[index]; var childA = treeA.commits[pAid]; var childB = treeB.commits[pBid]; result = result && recurseCompare(childA, childB); }, this); // if each of our children recursively are equal, we are good return result; }; var branchA = treeA.branches[branchName]; var branchB = treeB.branches[branchName]; return _.isEqual(branchA, branchB) && recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); }; LevelEngine.prototype.convertTreeSafe = function(tree) { if (typeof tree == 'string') { return JSON.parse(unescape(tree)); } return tree; }; LevelEngine.prototype.stripTreeFields = function(trees) { var stripFields = ['createTime', 'author', 'commitMessage']; var sortFields = ['children', 'parents']; _.each(trees, function(tree) { _.each(tree.commits, function(commit) { _.each(stripFields, function(field) { commit[field] = undefined; }); _.each(sortFields, function(field) { if (commit[field]) { commit[field] = commit[field].sort(); } }); }); }); }; LevelEngine.prototype.compareTrees = function(treeA, treeB) { treeA = this.convertTreeSafe(treeA); treeB = this.convertTreeSafe(treeB); // now we need to strip out the fields we don't care about, aka things // like createTime, message, author this.stripTreeFields([treeA, treeB]); return _.isEqual(treeA, treeB); }; var levelEngine = new LevelEngine(); exports.LevelEngine = LevelEngine; }); require("/levels.js"); require.define("/main.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('./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("/main.js"); require.define("/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("/miscViews.js"); require.define("/tree.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); var GRAPHICS = require('./constants').GRAPHICS; var randomHueString = function() { var hue = Math.random(); var str = 'hsb(' + String(hue) + ',0.7,1)'; return str; }; var VisBase = Backbone.Model.extend({ removeKeys: function(keys) { _.each(keys, function(key) { if (this.get(key)) { this.get(key).remove(); } }, this); } }); var VisBranch = VisBase.extend({ defaults: { pos: null, text: null, rect: null, arrow: null, isHead: false, flip: 1, fill: GRAPHICS.rectFill, stroke: GRAPHICS.rectStroke, 'stroke-width': GRAPHICS.rectStrokeWidth, offsetX: GRAPHICS.nodeRadius * 4.75, offsetY: 0, arrowHeight: 14, arrowInnerSkew: 0, arrowEdgeHeight: 6, arrowLength: 14, arrowOffsetFromCircleX: 10, vPad: 5, hPad: 5, animationSpeed: GRAPHICS.defaultAnimationTime, animationEasing: GRAPHICS.defaultEasing }, validateAtInit: function() { if (!this.get('branch')) { throw new Error('need a branch!'); } }, getID: function() { return this.get('branch').get('id'); }, initialize: function() { this.validateAtInit(); // shorthand notation for the main objects this.gitVisuals = this.get('gitVisuals'); this.gitEngine = this.get('gitEngine'); if (!this.gitEngine) { console.log('throw damnit'); throw new Error('asd'); } this.get('branch').set('visBranch', this); var id = this.get('branch').get('id'); if (id == 'HEAD') { // switch to a head ref this.set('isHead', true); this.set('flip', -1); this.set('fill', GRAPHICS.headRectFill); } else if (id !== 'master') { // we need to set our color to something random this.set('fill', randomHueString()); } }, getCommitPosition: function() { var commit = this.gitEngine.getCommitFromRef(this.get('branch')); var visNode = commit.get('visNode'); return visNode.getScreenCoords(); }, getBranchStackIndex: function() { if (this.get('isHead')) { // head is never stacked with other branches return 0; } var myArray = this.getBranchStackArray(); var index = -1; _.each(myArray, function(branch, i) { if (branch.obj == this.get('branch')) { index = i; } }, this); return index; }, getBranchStackLength: function() { if (this.get('isHead')) { // head is always by itself return 1; } return this.getBranchStackArray().length; }, getBranchStackArray: function() { var arr = this.gitVisuals.branchStackMap[this.get('branch').get('target').get('id')]; if (arr === undefined) { // this only occurs when we are generating graphics inside of // a new Branch instantiation, so we need to force the update this.gitVisuals.calcBranchStacks(); return this.getBranchStackArray(); } return arr; }, getTextPosition: function() { var pos = this.getCommitPosition(); // then order yourself accordingly. we use alphabetical sorting // so everything is independent var myPos = this.getBranchStackIndex(); return { x: pos.x + this.get('flip') * this.get('offsetX'), y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY') }; }, getRectPosition: function() { var pos = this.getTextPosition(); var f = this.get('flip'); // first get text width and height var textSize = this.getTextSize(); return { x: pos.x - 0.5 * textSize.w - this.get('hPad'), y: pos.y - 0.5 * textSize.h - this.get('vPad') }; }, getArrowPath: function() { // should make these util functions... var offset2d = function(pos, x, y) { return { x: pos.x + x, y: pos.y + y }; }; var toStringCoords = function(pos) { return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); }; var f = this.get('flip'); var arrowTip = offset2d(this.getCommitPosition(), f * this.get('arrowOffsetFromCircleX'), 0 ); var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight')); var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight')); var arrowInnerUp = offset2d(arrowEdgeUp, f * this.get('arrowInnerSkew'), this.get('arrowEdgeHeight') ); var arrowInnerLow = offset2d(arrowEdgeLow, f * this.get('arrowInnerSkew'), -this.get('arrowEdgeHeight') ); var tailLength = 49; var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0); var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0); var pathStr = ''; pathStr += 'M' + toStringCoords(arrowStartUp) + ' '; var coords = [ arrowInnerUp, arrowEdgeUp, arrowTip, arrowEdgeLow, arrowInnerLow, arrowStartLow ]; _.each(coords, function(pos) { pathStr += 'L' + toStringCoords(pos) + ' '; }, this); pathStr += 'z'; return pathStr; }, getTextSize: function() { var getTextWidth = function(visBranch) { var textNode = visBranch.get('text').node; return (textNode === null) ? 1 : textNode.clientWidth; }; var textNode = this.get('text').node; if (this.get('isHead')) { // HEAD is a special case return { w: textNode.clientWidth, h: textNode.clientHeight }; } var maxWidth = 0; _.each(this.getBranchStackArray(), function(branch) { maxWidth = Math.max(maxWidth, getTextWidth( branch.obj.get('visBranch') )); }); return { w: maxWidth, h: textNode.clientHeight }; }, getSingleRectSize: function() { var textSize = this.getTextSize(); var vPad = this.get('vPad'); var hPad = this.get('hPad'); return { w: textSize.w + vPad * 2, h: textSize.h + hPad * 2 }; }, getRectSize: function() { var textSize = this.getTextSize(); // enforce padding var vPad = this.get('vPad'); var hPad = this.get('hPad'); // number of other branch names we are housing var totalNum = this.getBranchStackLength(); return { w: textSize.w + vPad * 2, h: textSize.h * totalNum * 1.1 + hPad * 2 }; }, getName: function() { var name = this.get('branch').get('id'); var selected = this.gitEngine.HEAD.get('target').get('id'); var add = (selected == name) ? '*' : ''; return name + add; }, nonTextToFront: function() { this.get('arrow').toFront(); this.get('rect').toFront(); }, textToFront: function() { this.get('text').toFront(); }, getFill: function() { // in the easy case, just return your own fill if you are: // - the HEAD ref // - by yourself (length of 1) // - part of a multi branch, but your thing is hidden if (this.get('isHead') || this.getBranchStackLength() == 1 || this.getBranchStackIndex() !== 0) { return this.get('fill'); } // woof. now it's hard, we need to blend hues... return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray()); }, remove: function() { this.removeKeys(['text', 'arrow', 'rect']); // also need to remove from this.gitVisuals this.gitVisuals.removeVisBranch(this); }, genGraphics: function(paper) { var textPos = this.getTextPosition(); var name = this.getName(); var text; // when from a reload, we dont need to generate the text text = paper.text(textPos.x, textPos.y, String(name)); text.attr({ 'font-size': 14, 'font-family': 'Monaco, Courier, font-monospace', opacity: this.getTextOpacity() }); this.set('text', text); var rectPos = this.getRectPosition(); var sizeOfRect = this.getRectSize(); var rect = paper .rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8) .attr(this.getAttributes().rect); this.set('rect', rect); var arrowPath = this.getArrowPath(); var arrow = paper .path(arrowPath) .attr(this.getAttributes().arrow); this.set('arrow', arrow); rect.toFront(); text.toFront(); }, updateName: function() { this.get('text').attr({ text: this.getName() }); }, getNonTextOpacity: function() { if (this.get('isHead')) { return this.gitEngine.getDetachedHead() ? 1 : 0; } return this.getBranchStackIndex() === 0 ? 1 : 0.0; }, getTextOpacity: function() { if (this.get('isHead')) { return this.gitEngine.getDetachedHead() ? 1 : 0; } return 1; }, getAttributes: function() { var nonTextOpacity = this.getNonTextOpacity(); var textOpacity = this.getTextOpacity(); this.updateName(); var textPos = this.getTextPosition(); var rectPos = this.getRectPosition(); var rectSize = this.getRectSize(); var arrowPath = this.getArrowPath(); return { text: { x: textPos.x, y: textPos.y, opacity: textOpacity }, rect: { x: rectPos.x, y: rectPos.y, width: rectSize.w, height: rectSize.h, opacity: nonTextOpacity, fill: this.getFill(), stroke: this.get('stroke'), 'stroke-width': this.get('stroke-width') }, arrow: { path: arrowPath, opacity: nonTextOpacity, fill: this.getFill(), stroke: this.get('stroke'), 'stroke-width': this.get('stroke-width') } }; }, animateUpdatedPos: function(speed, easing) { var attr = this.getAttributes(); this.animateToAttr(attr, speed, easing); }, animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { // an animation of 0 is essentially setting the attribute directly this.animateToAttr(fromAttr, 0); this.animateToAttr(toAttr, speed, easing); }, animateToAttr: function(attr, speed, easing) { if (speed === 0) { this.get('text').attr(attr.text); this.get('rect').attr(attr.rect); this.get('arrow').attr(attr.arrow); return; } var s = speed !== undefined ? speed : this.get('animationSpeed'); var e = easing || this.get('animationEasing'); this.get('text').stop().animate(attr.text, s, e); this.get('rect').stop().animate(attr.rect, s, e); this.get('arrow').stop().animate(attr.arrow, s, e); } }); var VisNode = VisBase.extend({ defaults: { depth: undefined, maxWidth: null, outgoingEdges: null, circle: null, text: null, id: null, pos: null, radius: null, commit: null, animationSpeed: GRAPHICS.defaultAnimationTime, animationEasing: GRAPHICS.defaultEasing, fill: GRAPHICS.defaultNodeFill, 'stroke-width': GRAPHICS.defaultNodeStrokeWidth, stroke: GRAPHICS.defaultNodeStroke }, getID: function() { return this.get('id'); }, validateAtInit: function() { if (!this.get('id')) { throw new Error('need id for mapping'); } if (!this.get('commit')) { throw new Error('need commit for linking'); } if (!this.get('pos')) { this.set('pos', { x: Math.random(), y: Math.random() }); } }, initialize: function() { this.validateAtInit(); // shorthand for the main objects this.gitVisuals = this.get('gitVisuals'); this.gitEngine = this.get('gitEngine'); this.set('outgoingEdges', []); }, setDepth: function(depth) { // for merge commits we need to max the depths across all this.set('depth', Math.max(this.get('depth') || 0, depth)); }, setDepthBasedOn: function(depthIncrement) { if (this.get('depth') === undefined) { debugger; throw new Error('no depth yet!'); } var pos = this.get('pos'); pos.y = this.get('depth') * depthIncrement; }, getMaxWidthScaled: function() { // returns our max width scaled based on if we are visible // from a branch or not var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); var map = { branch: 1, head: 0.3, none: 0.1 }; if (map[stat] === undefined) { throw new Error('bad stat'); } return map[stat] * this.get('maxWidth'); }, toFront: function() { this.get('circle').toFront(); this.get('text').toFront(); }, getOpacity: function() { var map = { 'branch': 1, 'head': GRAPHICS.upstreamHeadOpacity, 'none': GRAPHICS.upstreamNoneOpacity }; var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); if (map[stat] === undefined) { throw new Error('invalid status'); } return map[stat]; }, getTextScreenCoords: function() { return this.getScreenCoords(); }, getAttributes: function() { var pos = this.getScreenCoords(); var textPos = this.getTextScreenCoords(); var opacity = this.getOpacity(); return { circle: { cx: pos.x, cy: pos.y, opacity: opacity, r: this.getRadius(), fill: this.getFill(), 'stroke-width': this.get('stroke-width'), stroke: this.get('stroke') }, text: { x: textPos.x, y: textPos.y, opacity: opacity } }; }, highlightTo: function(visObj, speed, easing) { // a small function to highlight the color of a node for demonstration purposes var color = visObj.get('fill'); var attr = { circle: { fill: color, stroke: color, 'stroke-width': this.get('stroke-width') * 5 }, text: {} }; this.animateToAttr(attr, speed, easing); }, animateUpdatedPosition: function(speed, easing) { var attr = this.getAttributes(); this.animateToAttr(attr, speed, easing); }, animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { // an animation of 0 is essentially setting the attribute directly this.animateToAttr(fromAttr, 0); this.animateToAttr(toAttr, speed, easing); }, animateToSnapshot: function(snapShot, speed, easing) { if (!snapShot[this.getID()]) { return; } this.animateToAttr(snapShot[this.getID()], speed, easing); }, animateToAttr: function(attr, speed, easing) { if (speed === 0) { this.get('circle').attr(attr.circle); this.get('text').attr(attr.text); return; } var s = speed !== undefined ? speed : this.get('animationSpeed'); var e = easing || this.get('animationEasing'); this.get('circle').stop().animate(attr.circle, s, e); this.get('text').stop().animate(attr.text, s, e); // animate the x attribute without bouncing so it looks like there's // gravity in only one direction. Just a small animation polish this.get('circle').animate(attr.circle.cx, s, 'easeInOut'); this.get('text').animate(attr.text.x, s, 'easeInOut'); }, getScreenCoords: function() { var pos = this.get('pos'); return this.gitVisuals.toScreenCoords(pos); }, getRadius: function() { return this.get('radius') || GRAPHICS.nodeRadius; }, getParentScreenCoords: function() { return this.get('commit').get('parents')[0].get('visNode').getScreenCoords(); }, setBirthPosition: function() { // utility method for animating it out from underneath a parent var parentCoords = this.getParentScreenCoords(); this.get('circle').attr({ cx: parentCoords.x, cy: parentCoords.y, opacity: 0, r: 0 }); this.get('text').attr({ x: parentCoords.x, y: parentCoords.y, opacity: 0 }); }, setBirthFromSnapshot: function(beforeSnapshot) { // first get parent attribute // woof bad data access. TODO var parentID = this.get('commit').get('parents')[0].get('visNode').getID(); var parentAttr = beforeSnapshot[parentID]; // then set myself faded on top of parent this.get('circle').attr({ opacity: 0, r: 0, cx: parentAttr.circle.cx, cy: parentAttr.circle.cy }); this.get('text').attr({ opacity: 0, x: parentAttr.text.x, y: parentAttr.text.y }); // then do edges var parentCoords = { x: parentAttr.circle.cx, y: parentAttr.circle.cy }; this.setOutgoingEdgesBirthPosition(parentCoords); }, setBirth: function() { this.setBirthPosition(); this.setOutgoingEdgesBirthPosition(this.getParentScreenCoords()); }, setOutgoingEdgesOpacity: function(opacity) { _.each(this.get('outgoingEdges'), function(edge) { edge.setOpacity(opacity); }); }, animateOutgoingEdgesToAttr: function(snapShot, speed, easing) { _.each(this.get('outgoingEdges'), function(edge) { var attr = snapShot[edge.getID()]; edge.animateToAttr(attr); }, this); }, animateOutgoingEdges: function(speed, easing) { _.each(this.get('outgoingEdges'), function(edge) { edge.animateUpdatedPath(speed, easing); }, this); }, animateOutgoingEdgesFromSnapshot: function(snapshot, speed, easing) { _.each(this.get('outgoingEdges'), function(edge) { var attr = snapshot[edge.getID()]; edge.animateToAttr(attr, speed, easing); }, this); }, setOutgoingEdgesBirthPosition: function(parentCoords) { _.each(this.get('outgoingEdges'), function(edge) { var headPos = edge.get('head').getScreenCoords(); var path = edge.genSmoothBezierPathStringFromCoords(parentCoords, headPos); edge.get('path').stop().attr({ path: path, opacity: 0 }); }, this); }, parentInFront: function() { // woof! talk about bad data access this.get('commit').get('parents')[0].get('visNode').toFront(); }, getFontSize: function(str) { if (str.length < 3) { return 12; } else if (str.length < 5) { return 10; } else { return 8; } }, getFill: function() { // first get our status, might be easy from this var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); if (stat == 'head') { return GRAPHICS.headRectFill; } else if (stat == 'none') { return GRAPHICS.orphanNodeFill; } // now we need to get branch hues return this.gitVisuals.getBlendedHuesForCommit(this.get('commit')); }, attachClickHandlers: function() { var commandStr = 'git show ' + this.get('commit').get('id'); _.each([this.get('circle'), this.get('text')], function(rObj) { rObj.click(function() { Main.getEvents().trigger('processCommandFromEvent', commandStr); }); }); }, setOpacity: function(opacity) { opacity = (opacity === undefined) ? 1 : opacity; // set the opacity on my stuff var keys = ['circle', 'text']; _.each(keys, function(key) { this.get(key).attr({ opacity: opacity }); }, this); }, remove: function() { this.removeKeys(['circle'], ['text']); // needs a manual removal of text for whatever reason this.get('text').remove(); this.gitVisuals.removeVisNode(this); }, removeAll: function() { this.remove(); _.each(this.get('outgoingEdges'), function(edge) { edge.remove(); }, this); }, genGraphics: function() { var paper = this.gitVisuals.paper; var pos = this.getScreenCoords(); var textPos = this.getTextScreenCoords(); var circle = paper.circle( pos.x, pos.y, this.getRadius() ).attr(this.getAttributes().circle); var text = paper.text(textPos.x, textPos.y, String(this.get('id'))); text.attr({ 'font-size': this.getFontSize(this.get('id')), 'font-weight': 'bold', 'font-family': 'Monaco, Courier, font-monospace', opacity: this.getOpacity() }); this.set('circle', circle); this.set('text', text); this.attachClickHandlers(); } }); var VisEdge = VisBase.extend({ defaults: { tail: null, head: null, animationSpeed: GRAPHICS.defaultAnimationTime, animationEasing: GRAPHICS.defaultEasing }, validateAtInit: function() { var required = ['tail', 'head']; _.each(required, function(key) { if (!this.get(key)) { throw new Error(key + ' is required!'); } }, this); }, getID: function() { return this.get('tail').get('id') + '.' + this.get('head').get('id'); }, initialize: function() { this.validateAtInit(); // shorthand for the main objects this.gitVisuals = this.get('gitVisuals'); this.gitEngine = this.get('gitEngine'); this.get('tail').get('outgoingEdges').push(this); }, remove: function() { this.removeKeys(['path']); this.gitVisuals.removeVisEdge(this); }, genSmoothBezierPathString: function(tail, head) { var tailPos = tail.getScreenCoords(); var headPos = head.getScreenCoords(); return this.genSmoothBezierPathStringFromCoords(tailPos, headPos); }, genSmoothBezierPathStringFromCoords: function(tailPos, headPos) { // we need to generate the path and control points for the bezier. format // is M(move abs) C (curve to) (control point 1) (control point 2) (final point) // the control points have to be __below__ to get the curve starting off straight. var coords = function(pos) { return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); }; var offset = function(pos, dir, delta) { delta = delta || GRAPHICS.curveControlPointOffset; return { x: pos.x, y: pos.y + delta * dir }; }; var offset2d = function(pos, x, y) { return { x: pos.x + x, y: pos.y + y }; }; // first offset tail and head by radii tailPos = offset(tailPos, -1, this.get('tail').getRadius()); headPos = offset(headPos, 1, this.get('head').getRadius()); var str = ''; // first move to bottom of tail str += 'M' + coords(tailPos) + ' '; // start bezier str += 'C'; // then control points above tail and below head str += coords(offset(tailPos, -1)) + ' '; str += coords(offset(headPos, 1)) + ' '; // now finish str += coords(headPos); // arrow head var delta = GRAPHICS.arrowHeadSize || 10; str += ' L' + coords(offset2d(headPos, -delta, delta)); str += ' L' + coords(offset2d(headPos, delta, delta)); str += ' L' + coords(headPos); // then go back, so we can fill correctly str += 'C'; str += coords(offset(headPos, 1)) + ' '; str += coords(offset(tailPos, -1)) + ' '; str += coords(tailPos); return str; }, getBezierCurve: function() { return this.genSmoothBezierPathString(this.get('tail'), this.get('head')); }, getStrokeColor: function() { return GRAPHICS.visBranchStrokeColorNone; }, setOpacity: function(opacity) { opacity = (opacity === undefined) ? 1 : opacity; this.get('path').attr({opacity: opacity}); }, genGraphics: function(paper) { var pathString = this.getBezierCurve(); var path = paper.path(pathString).attr({ 'stroke-width': GRAPHICS.visBranchStrokeWidth, 'stroke': this.getStrokeColor(), 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'fill': this.getStrokeColor() }); path.toBack(); this.set('path', path); }, getOpacity: function() { var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail')); var map = { 'branch': 1, 'head': GRAPHICS.edgeUpstreamHeadOpacity, 'none': GRAPHICS.edgeUpstreamNoneOpacity }; if (map[stat] === undefined) { throw new Error('bad stat'); } return map[stat]; }, getAttributes: function() { var newPath = this.getBezierCurve(); var opacity = this.getOpacity(); return { path: { path: newPath, opacity: opacity } }; }, animateUpdatedPath: function(speed, easing) { var attr = this.getAttributes(); this.animateToAttr(attr, speed, easing); }, animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { // an animation of 0 is essentially setting the attribute directly this.animateToAttr(fromAttr, 0); this.animateToAttr(toAttr, speed, easing); }, animateToAttr: function(attr, speed, easing) { if (speed === 0) { this.get('path').attr(attr.path); return; } this.get('path').toBack(); this.get('path').stop().animate( attr.path, speed !== undefined ? speed : this.get('animationSpeed'), easing || this.get('animationEasing') ); } }); var VisEdgeCollection = Backbone.Collection.extend({ model: VisEdge }); var VisBranchCollection = Backbone.Collection.extend({ model: VisBranch }); exports.VisEdgeCollection = VisEdgeCollection; exports.VisBranchCollection = VisBranchCollection; exports.VisNode = VisNode; exports.VisEdge = VisEdge; exports.VisBranch = VisBranch; }); require("/tree.js"); require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./main'); 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('./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("/visuals.js"); })();