// 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() { this.rootCommit = null; this.refs = {}; this.HEAD = null; this.id_gen = 0; this.branches = []; // global variable to keep track of the options given // along with the command call. this.currentOptions = {}; events.on('gitCommandReady', _.bind(this.dispatch, this)); this.init(); } GitEngine.prototype.init = function() { // make an initial commit and a master branch this.rootCommit = new Commit({rootCommit: true}); this.refs[this.rootCommit.get('id')] = 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.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 Error('woah bad branch name!! This is not ok: ' + name); } if (/[hH][eE][aA][dD]/.test(name)) { throw new Error('branch name of "head" is ambiguous, dont name it that'); } return name; }; GitEngine.prototype.makeBranch = function(id, target) { id = this.validateBranchName(id); if (this.refs[id]) { throw new Error('that branch id already exists!'); } var branch = new Branch({ target: target, id: id }); this.branches.push(branch); this.refs[branch.get('id')] = branch; return branch; }; GitEngine.prototype.getBranches = function() { var toReturn = []; _.each(this.branches, function(branch) { toReturn.push({ id: branch.get('id'), selected: this.HEAD.get('target') === branch }); }, this); return toReturn; }; GitEngine.prototype.printBranches = function() { var branches = this.getBranches(); _.each(branches, function(branch) { console.log((branch.selected ? '* ' : '') + branch.id); }); }; GitEngine.prototype.makeCommit = function(parent) { var commit = new Commit({ parents: [parent] }); this.refs[commit.get('id')] = commit; return commit; }; GitEngine.prototype.commit = function() { var targetCommit = this.getCommitFromRef(this.HEAD); // if we want to ammend, go one above if (this.currentOptions['--amend']) { targetCommit = this.resolveId('HEAD~1'); } var newCommit = this.makeCommit(targetCommit); if (this.getDetachedHead()) { events.trigger('commandProcessWarn', "Warning!! Detached HEAD state"); } else { var targetBranch = this.HEAD.get('target'); targetBranch.set('target', newCommit); } }; GitEngine.prototype.resolveId = function(idOrTarget) { 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]); }], [/^([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 Error('unknown ref ' + ref); } if (!this.refs[startRef]) { throw new Error('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.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; } var sortFunc = function(cA, cB) { // why cant parse int handle leading characters? :( var numA = parseInt(cA.get('id').slice(1)); var numB = parseInt(cB.get('id').slice(1)); return numA - numB; }; var pQueue = [].concat(commit.get('parents')); pQueue.sort(sortFunc); numBack--; while (pQueue.length && numBack !== 0) { var popped = pQueue.shift(0); pQueue = pQueue.concat(popped.get('parents')); pQueue.sort(sortFunc); numBack--; } if (numBack !== 0 || pQueue.length == 0) { throw new Error('exhausted search, sorry'); } return pQueue.shift(0); }; GitEngine.prototype.checkout = function(idOrTarget) { console.log('the target', idOrTarget); var target = this.resolveId(idOrTarget); console.log(target); 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 Error('can only checkout branches and commits!'); } this.HEAD.set('target', target); }; GitEngine.prototype.branch = function(name, ref, options) { ref = ref || 'HEAD'; options = options || {}; if (options['-d'] || options['-D']) { this.deleteBranch(name); return; } 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 Error("You can't delete things that arent branches with branch command"); } if (target.get('id') == 'master') { throw new Error("You can't delete the master branch!"); } if (this.HEAD.get('target') === target) { throw new Error("Cannot delete the branch you are currently on"); } var id = target.get('id'); target.delete(); delete this.refs[id]; }; GitEngine.prototype.dispatch = function(commandObj) { this.currentOptions = commandObj.optionParser.supportedMap; this[commandObj.method](); }; GitEngine.prototype.add = function() { throw new Error( "This demo is meant to demonstrate git branching, so don't worry about " + "adding / staging files. Just go ahead and commit away!" ); }; GitEngine.prototype.execute = function(command, callback) { // execute command, and when it's finished, call the callback // we still need to figure this out var closures = this.getClosuresForCommand(command); // make a scheduler based on all the closures, and pass in our callback var s = new Scheduler(closures, { callback: callback }); s.start(); }; GitEngine.prototype.getClosuresForCommand = function(command) { var numbers = [1,2,3,4,5,6,7,8,9,10]; var closures = []; _.each(numbers, function(num) { var c = function() { console.log(num); }; closures.push(c); }); return closures; }; 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'); }, toString: function() { return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target')); }, delete: function() { console.log('DELETING ' + this.get('type') + ' ' + this.get('id')); } }); var Branch = Ref.extend({ initialize: function() { Ref.prototype.initialize.call(this); this.set('type', 'branch'); } }); var Commit = Backbone.Model.extend({ validateAtInit: function() { if (!this.get('id')) { this.set('id', uniqueId('C')); } this.set('type', 'commit'); this.set('children', []); // root commits have no parents if (this.get('rootCommit')) { this.set('parents', []); } else { if (!this.get('parents') || !this.get('parents').length) { throw new Error('needs parents'); } } }, addNodeToVisuals: function() { this.set('arborNode', sys.addNode(this.get('id'))); }, addEdgeToVisuals: function(parent) { sys.addEdge(this.get('arborNode'), parent.get('arborNode')); }, initialize: function() { this.validateAtInit(); this.addNodeToVisuals(); console.log('MAKING NEW COMMIT', this.get('id')); _.each(this.get('parents'), function(parent) { parent.get('children').push(this); this.addEdgeToVisuals(parent); }, this); } });