var _ = require('underscore'); var Backbone = require('backbone'); var Q = require('q'); var intl = require('../intl'); var AnimationFactory = require('../visuals/animation/animationFactory').AnimationFactory; var AnimationQueue = require('../visuals/animation').AnimationQueue; var TreeCompare = require('../graph/treeCompare'); var Graph = require('../graph'); var Errors = require('../util/errors'); var Main = require('../app'); var Commands = require('../commands'); var GitError = Errors.GitError; var CommandResult = Errors.CommandResult; var ORIGIN_PREFIX = 'o/'; var TAB = '   '; var SHORT_CIRCUIT_CHAIN = 'STAPH'; function catchShortCircuit(err) { if (err !== SHORT_CIRCUIT_CHAIN) { throw err; } } function GitEngine(options) { this.rootCommit = null; this.refs = {}; this.HEAD = null; this.origin = null; this.mode = 'git'; this.localRepo = null; this.branchCollection = options.branches; this.tagCollection = options.tags; this.commitCollection = options.collection; this.gitVisuals = options.gitVisuals; this.eventBaton = options.eventBaton; this.eventBaton.stealBaton('processGitCommand', this.dispatch, this); // poor man's dependency injection. we can't reassign // the module variable because its get clobbered :P this.animationFactory = (options.animationFactory) ? options.animationFactory : AnimationFactory; this.initUniqueID(); } GitEngine.prototype.initUniqueID = function() { // backbone or something uses _ .uniqueId, so we make our own here this.uniqueId = (function() { var n = 0; return function(prepend) { return prepend ? prepend + n++ : n++; }; })(); }; GitEngine.prototype.handleModeChange = function(vcs, callback) { if (this.mode === vcs) { // don't fire event aggressively callback(); return; } Main.getEvents().trigger('vcsModeChange', {mode: vcs}); var chain = this.setMode(vcs); if (this.origin) { this.origin.setMode(vcs, function() {}); } if (!chain) { callback(); return; } // we have to do it async chain.then(callback); }; GitEngine.prototype.getIsHg = function() { return this.mode === 'hg'; }; GitEngine.prototype.setMode = function(vcs) { var switchedToHg = (this.mode === 'git' && vcs === 'hg'); this.mode = vcs; if (!switchedToHg) { return; } // if we are switching to mercurial then we have some // garbage collection and other tidying up to do. this // may or may not require a refresh so lets check. var deferred = Q.defer(); deferred.resolve(); var chain = deferred.promise; // this stuff is tricky because we don't animate when // we didn't do anything, but we DO animate when // either of the operations happen. so a lot of // branching ahead... var neededUpdate = this.updateAllBranchesForHg(); if (neededUpdate) { chain = chain.then(function() { return this.animationFactory.playRefreshAnimationSlow(this.gitVisuals); }.bind(this)); // ok we need to refresh anyways, so do the prune after chain = chain.then(function() { var neededPrune = this.pruneTree(); if (!neededPrune) { return; } return this.animationFactory.playRefreshAnimation(this.gitVisuals); }.bind(this)); return chain; } // ok might need prune though var pruned = this.pruneTree(); if (!pruned) { // do sync return; } return this.animationFactory.playRefreshAnimation(this.gitVisuals); }; GitEngine.prototype.assignLocalRepo = function(repo) { this.localRepo = repo; }; GitEngine.prototype.defaultInit = function() { var defaultTree = Graph.getDefaultTree(); 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.hasOrigin = function() { return !!this.origin; }; GitEngine.prototype.isOrigin = function() { return !!this.localRepo; }; GitEngine.prototype.exportTreeForBranch = function(branchName) { // this method exports the tree and then prunes everything that // is not connected to branchname var tree = this.exportTree(); // get the upstream set var set = Graph.getUpstreamSet(this, branchName); // now loop through and delete commits var commitsToLoop = tree.commits; tree.commits = {}; _.each(commitsToLoop, function(commit, id) { if (set[id]) { // if included in target branch tree.commits[id] = commit; } }); var branchesToLoop = tree.branches; tree.branches = {}; _.each(branchesToLoop, function(branch, id) { if (id === branchName) { tree.branches[id] = branch; } }); tree.HEAD.target = branchName; return tree; }; 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: {}, tags: {}, HEAD: null }; _.each(this.branchCollection.toJSON(), function(branch) { branch.target = branch.target.get('id'); delete branch.visBranch; 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) { delete commit[field]; }, this); // convert parents var parents = []; _.each(commit.parents, function(par) { parents.push(par.get('id')); }); commit.parents = parents; totalExport.commits[commit.id] = commit; }, this); _.each(this.tagCollection.toJSON(), function(tag) { delete tag.visTag; tag.target = tag.target.get('id'); totalExport.tags[tag.id] = tag; }, this); var HEAD = this.HEAD.toJSON(); HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = HEAD.visTag = undefined; HEAD.target = HEAD.target.get('id'); totalExport.HEAD = HEAD; if (this.hasOrigin()) { totalExport.originTree = this.origin.exportTree(); } return totalExport; }; GitEngine.prototype.printTree = function(tree) { tree = tree || this.exportTree(); TreeCompare.reduceTreeFields([tree]); var str = JSON.stringify(tree); if (/'/.test(str)) { // escape it to make it more copy paste friendly str = escape(str); } return str; }; GitEngine.prototype.printAndCopyTree = function() { window.prompt( intl.str('Copy the tree string below'), this.printTree() ); }; GitEngine.prototype.loadTree = function(tree) { // deep copy in case we use it a bunch. lol awesome copy method tree = JSON.parse(JSON.stringify(tree)); // first clear everything this.removeAll(); this.instantiateFromTree(tree); this.reloadGraphics(); this.initUniqueID(); }; GitEngine.prototype.loadTreeFromString = function(treeString) { this.loadTree(JSON.parse(unescape(this.crappyUnescape(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.gitVisuals); this.commitCollection.add(commit); }, this); _.each(tree.branches, function(branchJSON) { var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id, this.gitVisuals); this.branchCollection.add(branch, {silent: true}); }, this); _.each(tree.tags, function(tagJSON) { var tag = this.getOrMakeRecursive(tree, createdSoFar, tagJSON.id, this.gitVisuals); this.tagCollection.add(tag, {silent: true}); }, this); var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id, this.gitVisuals); this.HEAD = HEAD; this.rootCommit = createdSoFar['C0']; if (!this.rootCommit) { throw new Error('Need root commit of C0 for calculations'); } this.refs = createdSoFar; this.gitVisuals.gitReady = false; this.branchCollection.each(function(branch) { this.gitVisuals.addBranch(branch); }, this); this.tagCollection.each(function(tag) { this.gitVisuals.addTag(tag); }, this); if (tree.originTree) { var treeString = JSON.stringify(tree.originTree); // if we don't have an animation queue (like when loading // right away), just go ahead and make an empty one this.animationQueue = this.animationQueue || new AnimationQueue({ callback: function() {} }); this.makeOrigin(treeString); } }; GitEngine.prototype.makeOrigin = function(treeString) { if (this.hasOrigin()) { throw new GitError({ msg: intl.str('git-error-origin-exists') }); } treeString = treeString || this.printTree(this.exportTreeForBranch('master')); // this is super super ugly but a necessary hack because of the way LGB was // originally designed. We need to get to the top level visualization from // the git engine -- aka we need to access our own visuals, then the // visualization and ask the main vis to create a new vis/git pair. Then // we grab the gitengine out of that and assign that as our origin repo // which connects the two. epic var masterVis = this.gitVisuals.getVisualization(); var originVis = masterVis.makeOrigin({ localRepo: this, treeString: treeString }); // defer the starting of our animation until origin has been created this.animationQueue.set('promiseBased', true); originVis.customEvents.on('gitEngineReady', function() { this.origin = originVis.gitEngine; originVis.gitEngine.assignLocalRepo(this); this.syncRemoteBranchFills(); // and then here is the crazy part -- we need the ORIGIN to refresh // itself in a separate animation. @_____@ this.origin.externalRefresh(); this.animationFactory.playRefreshAnimationAndFinish(this.gitVisuals, this.animationQueue); }, this); var originTree = JSON.parse(unescape(treeString)); // make an origin branch for each branch mentioned in the tree if its // not made already... _.each(originTree.branches, function(branchJSON, branchName) { if (this.refs[ORIGIN_PREFIX + branchName]) { // we already have this branch return; } var originTarget = this.findCommonAncestorWithRemote( branchJSON.target ); // now we have something in common, lets make the tracking branch var remoteBranch = this.makeBranch( ORIGIN_PREFIX + branchName, this.getCommitFromRef(originTarget) ); this.setLocalToTrackRemote(this.refs[branchJSON.id], remoteBranch); }, this); }; GitEngine.prototype.makeRemoteBranchIfNeeded = function(branchName) { if (this.refs[ORIGIN_PREFIX + branchName]) { return; } // if its not a branch on origin then bounce var source = this.origin.resolveID(branchName); if (source.get('type') !== 'branch') { return; } return this.makeRemoteBranchForRemote(branchName); }; GitEngine.prototype.makeBranchIfNeeded = function(branchName) { if (this.refs[branchName]) { return; } var where = this.findCommonAncestorForRemote( this.getCommitFromRef('HEAD').get('id') ); return this.validateAndMakeBranch(branchName, this.getCommitFromRef(where)); }; GitEngine.prototype.makeRemoteBranchForRemote = function(branchName) { var target = this.origin.refs[branchName].get('target'); var originTarget = this.findCommonAncestorWithRemote( target.get('id') ); return this.makeBranch( ORIGIN_PREFIX + branchName, this.getCommitFromRef(originTarget) ); }; GitEngine.prototype.findCommonAncestorForRemote = function(myTarget) { if (this.origin.refs[myTarget]) { return myTarget; } var parents = this.refs[myTarget].get('parents'); if (parents.length === 1) { // Easy, we only have one parent. lets just go upwards myTarget = parents[0].get('id'); // Recurse upwards to find where our remote has a commit. return this.findCommonAncestorForRemote(myTarget); } // We have multiple parents so find out where these two meet. var leftTarget = this.findCommonAncestorForRemote(parents[0].get('id')); var rightTarget = this.findCommonAncestorForRemote(parents[1].get('id')); return this.getCommonAncestor( leftTarget, rightTarget, true // don't throw since we don't know the order here. ).get('id'); }; GitEngine.prototype.findCommonAncestorWithRemote = function(originTarget) { if (this.refs[originTarget]) { return originTarget; } // now this is tricky -- our remote could have commits that we do // not have. so lets go upwards until we find one that we have var parents = this.origin.refs[originTarget].get('parents'); if (parents.length === 1) { return this.findCommonAncestorWithRemote(parents[0].get('id')); } // Like above, could have two parents var leftTarget = this.findCommonAncestorWithRemote(parents[0].get('id')); var rightTarget = this.findCommonAncestorWithRemote(parents[1].get('id')); return this.getCommonAncestor(leftTarget, rightTarget, true /* don't throw */).get('id'); }; GitEngine.prototype.makeBranchOnOriginAndTrack = function(branchName, target) { var remoteBranch = this.makeBranch( ORIGIN_PREFIX + branchName, this.getCommitFromRef(target) ); if (this.refs[branchName]) { // not all remote branches have tracking ones this.setLocalToTrackRemote(this.refs[branchName], remoteBranch); } var originTarget = this.findCommonAncestorForRemote( this.getCommitFromRef(target).get('id') ); this.origin.makeBranch( branchName, this.origin.getCommitFromRef(originTarget) ); }; GitEngine.prototype.setLocalToTrackRemote = function(localBranch, remoteBranch) { localBranch.setRemoteTrackingBranchID(remoteBranch.get('id')); if (!this.command) { // during init we have no command return; } var msg = 'local branch "' + localBranch.get('id') + '" set to track remote branch "' + remoteBranch.get('id') + '"'; this.command.addWarning(intl.todo(msg)); }; GitEngine.prototype.getOrMakeRecursive = function( tree, createdSoFar, objID, gitVisuals ) { 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'; } else if (tree.tags[id]) { return 'tag'; } 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(Object.assign( 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(Object.assign( tree.branches[objID], { target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target) } )); createdSoFar[objID] = branch; return branch; } if (type == 'tag') { var tagJSON = tree.tags[objID]; var tag = new Tag(Object.assign( tree.tags[objID], { target: this.getOrMakeRecursive(tree, createdSoFar, tagJSON.target) } )); createdSoFar[objID] = tag; return tag; } 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(Object.assign( commitJSON, { parents: parentObjs, gitVisuals: this.gitVisuals } )); createdSoFar[objID] = commit; return commit; } throw new Error('ruh rho!! unsupported type for ' + objID); }; GitEngine.prototype.tearDown = function() { if (this.tornDown) { return; } this.eventBaton.releaseBaton('processGitCommand', this.dispatch, this); this.removeAll(); this.tornDown = true; }; GitEngine.prototype.reloadGraphics = function() { // get the root commit this.gitVisuals.rootCommit = this.refs['C0']; // 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.removeAll = function() { this.branchCollection.reset(); this.tagCollection.reset(); this.commitCollection.reset(); this.refs = {}; this.HEAD = null; this.rootCommit = null; if (this.origin) { // we will restart all this jazz during init from tree this.origin.gitVisuals.getVisualization().tearDown(); delete this.origin; this.gitVisuals.getVisualization().clearOrigin(); } 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) { // Lets escape some of the nasty characters name = name.replace(///g,"\/"); name = name.replace(/\s/g, ''); // And then just make sure it starts with alpha-numeric, // can contain a slash or dash, and then ends with alpha if ( !/^(\w+[.\/\-]?)+\w+$/.test(name) || name.search('o/') === 0 ) { throw new GitError({ msg: intl.str( 'bad-branch-name', { branch: name } ) }); } if (/^[cC]\d+$/.test(name)) { throw new GitError({ msg: intl.str( 'bad-branch-name', { branch: name } ) }); } if (/[hH][eE][aA][dD]/.test(name)) { throw new GitError({ msg: intl.str( 'bad-branch-name', { branch: name } ) }); } if (name.length > 9) { name = name.slice(0, 9); this.command.addWarning( intl.str( 'branch-name-short', { branch: name } ) ); } return name; }; GitEngine.prototype.validateAndMakeBranch = function(id, target) { id = this.validateBranchName(id); if (this.refs[id]) { throw new GitError({ msg: intl.str( 'bad-branch-name', { branch: id } ) }); } return this.makeBranch(id, target); }; GitEngine.prototype.validateAndMakeTag = function(id, target) { id = this.validateBranchName(id); if (this.refs[id]) { throw new GitError({ msg: intl.str( 'bad-tag-name', { tag: name } ) }); } this.makeTag(id, target); }; GitEngine.prototype.makeBranch = function(id, target) { if (this.refs[id]) { throw new Error('woah already have that'); } var branch = new Branch({ target: target, id: id }); this.branchCollection.add(branch); this.refs[branch.get('id')] = branch; return branch; }; GitEngine.prototype.makeTag = function(id, target) { if (this.refs[id]) { throw new Error('woah already have that'); } var tag = new Tag({ target: target, id: id }); this.tagCollection.add(tag); this.refs[tag.get('id')] = tag; return tag; }; GitEngine.prototype.getHead = function() { return Object.assign({}, this.HEAD); }; GitEngine.prototype.getTags = function() { var toReturn = []; this.tagCollection.each(function(tag) { toReturn.push({ id: tag.get('id'), target: tag.get('target'), remote: tag.getIsRemote(), obj: tag }); }, this); return toReturn; }; 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'), remote: branch.getIsRemote(), obj: branch }); }, this); return toReturn; }; GitEngine.prototype.getRemoteBranches = function() { var all = this.getBranches(); return _.filter(all, function(branchJSON) { return branchJSON.remote === true; }); }; GitEngine.prototype.getLocalBranches = function() { var all = this.getBranches(); return _.filter(all, function(branchJSON) { return branchJSON.remote === false; }); }; 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.printTags = function(tags) { var result = ''; _.each(tags, function(tag) { result += tag.id + '\n'; }); throw new CommandResult({ msg: result }); }; GitEngine.prototype.printRemotes = function(options) { var result = ''; if (options.verbose) { result += 'origin (fetch)\n'; result += TAB + 'git@github.com:pcottle/foo.git' + '\n\n'; result += 'origin (push)\n'; result += TAB + 'git@github.com:pcottle/foo.git'; } else { result += 'origin'; } throw new CommandResult({ msg: result }); }; GitEngine.prototype.getUniqueID = function() { var id = this.uniqueId('C'); var hasID = function(idToCheck) { // loop through and see if we have it locally or // remotely if (this.refs[idToCheck]) { return true; } if (this.origin && this.origin.refs[idToCheck]) { return true; } return false; }.bind(this); while (hasID(id)) { id = this.uniqueId('C'); } return id; }; 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 = this.getUniqueID(); } var commit = new Commit(Object.assign({ parents: parents, id: id, gitVisuals: this.gitVisuals }, options || {} )); this.refs[commit.get('id')] = commit; this.commitCollection.add(commit); return commit; }; GitEngine.prototype.revert = function(whichCommits) { // resolve the commits we will rebase var toRevert = _.map(whichCommits, function(stringRef) { return this.getCommitFromRef(stringRef); }, this); var deferred = Q.defer(); var chain = deferred.promise; var destBranch = this.resolveID('HEAD'); chain = this.animationFactory.highlightEachWithPromise( chain, toRevert, destBranch ); var base = this.getCommitFromRef('HEAD'); // each step makes a new commit var chainStep = function(oldCommit) { var newId = this.rebaseAltID(oldCommit.get('id')); var commitMessage = intl.str('git-revert-msg', { oldCommit: this.resolveName(oldCommit), oldMsg: oldCommit.get('commitMessage') }); var newCommit = this.makeCommit([base], newId, { commitMessage: commitMessage }); base = newCommit; return this.animationFactory.playCommitBirthPromiseAnimation( newCommit, this.gitVisuals ); }.bind(this); // set up the promise chain _.each(toRevert, function(commit) { chain = chain.then(function() { return chainStep(commit); }); }, this); // done! update our location chain = chain.then(function() { this.setTargetLocation('HEAD', base); return this.animationFactory.playRefreshAnimation(this.gitVisuals); }.bind(this)); this.animationQueue.thenFinish(chain, deferred); }; GitEngine.prototype.reset = function(target) { this.setTargetLocation('HEAD', this.getCommitFromRef(target)); }; GitEngine.prototype.setupCherrypickChain = function(toCherrypick) { // error checks are all good, lets go! var deferred = Q.defer(); var chain = deferred.promise; var destinationBranch = this.resolveID('HEAD'); chain = this.animationFactory.highlightEachWithPromise( chain, toCherrypick, destinationBranch ); var chainStep = function(commit) { var newCommit = this.cherrypick(commit); return this.animationFactory.playCommitBirthPromiseAnimation( newCommit, this.gitVisuals ); }.bind(this); _.each(toCherrypick, function(arg) { chain = chain.then(function() { return chainStep(arg); }); }, this); this.animationQueue.thenFinish(chain, deferred); }; /************************************* * Origin stuff! ************************************/ GitEngine.prototype.checkUpstreamOfSource = function( target, source, targetBranch, sourceBranch, errorMsg ) { // here we are downloading some X number of commits from source onto // target. Hence target should be strictly upstream of source // lets first get the upstream set from source's dest branch var upstream = Graph.getUpstreamSet(source, sourceBranch); var targetLocationID = target.getCommitFromRef(targetBranch).get('id'); if (!upstream[targetLocationID]) { throw new GitError({ msg: errorMsg || intl.str('git-error-origin-fetch-no-ff') }); } }; GitEngine.prototype.getTargetGraphDifference = function( target, source, targetBranch, sourceBranch, options ) { options = options || {}; sourceBranch = source.resolveID(sourceBranch); var targetSet = Graph.getUpstreamSet(target, targetBranch); var sourceStartCommit = source.getCommitFromRef(sourceBranch); var sourceTree = source.exportTree(); var sourceStartCommitJSON = sourceTree.commits[sourceStartCommit.get('id')]; if (targetSet[sourceStartCommitJSON.id]) { // either we throw since theres no work to be done, or we return an empty array if (options.dontThrowOnNoFetch) { return []; } else { throw new GitError({ msg: intl.str('git-error-origin-fetch-uptodate') }); } } // ok great, we have our starting point and our stopping set. lets go ahead // and traverse upwards and keep track of depth manually sourceStartCommitJSON.depth = 0; var difference = []; var toExplore = [sourceStartCommitJSON]; var pushParent = function(parentID) { if (targetSet[parentID]) { // we already have that commit, lets bounce return; } var parentJSON = sourceTree.commits[parentID]; parentJSON.depth = here.depth + 1; toExplore.push(parentJSON); }; while (toExplore.length) { var here = toExplore.pop(); difference.push(here); _.each(here.parents, pushParent); } // filter because we weren't doing graph search var differenceUnique = Graph.getUniqueObjects(difference); /** * Ok now we have to determine the order in which to make these commits. * We used to just sort by depth because we were lazy but that is incorrect * since it doesn't represent the actual dependency tree of the commits. * * So here is what we are going to do -- loop through the differenceUnique * set and find a commit that has _all_ its parents in the targetSet. Then * decide to make that commit first, expand targetSet, and then rinse & repeat */ var inOrder = []; var allParentsMade = function(node) { var allParents = true; node.parents.forEach(function(parent) { allParents = allParents && targetSet[parent]; }); return allParents; }; while (differenceUnique.length) { for (var i = 0; i < differenceUnique.length; i++) { if (!allParentsMade(differenceUnique[i])) { // This commit cannot be made since not all of its dependencies are // satisfied. continue; } var makeThis = differenceUnique[i]; inOrder.push(makeThis); // remove the commit differenceUnique.splice(i, 1); // expand target set targetSet[makeThis.id] = true; } } return inOrder; }; GitEngine.prototype.push = function(options) { options = options || {}; if (options.source === "") { // delete case this.pushDeleteRemoteBranch( this.refs[ORIGIN_PREFIX + options.destination], this.origin.refs[options.destination] ); return; } var sourceBranch = this.refs[options.source]; if (sourceBranch && sourceBranch.attributes.type === 'tag') { throw new GitError({ msg: intl.todo('Tags are not allowed as sources for pushing'), }); } if (!this.origin.refs[options.destination]) { this.makeBranchOnOriginAndTrack( options.destination, this.getCommitFromRef(sourceBranch) ); // play an animation now since we might not have to fast forward // anything... this is weird because we are punting an animation // and not resolving the promise but whatever this.animationFactory.playRefreshAnimation(this.origin.gitVisuals); this.animationFactory.playRefreshAnimation(this.gitVisuals); } var branchOnRemote = this.origin.refs[options.destination]; var sourceLocation = this.resolveID(options.source || 'HEAD'); // first check if this is even allowed by checking the sync between if (!options.force) { this.checkUpstreamOfSource( this, this.origin, branchOnRemote, sourceLocation, intl.str('git-error-origin-push-no-ff') ); } var commitsToMake = this.getTargetGraphDifference( this.origin, this, branchOnRemote, sourceLocation, /* options */ { dontThrowOnNoFetch: true, } ); if (!commitsToMake.length) { if (!options.force) { // We are already up to date, and we can't be deleting // either since we don't have --force throw new GitError({ msg: intl.str('git-error-origin-fetch-uptodate') }); } else { var sourceCommit = this.getCommitFromRef(sourceBranch); var originCommit = this.getCommitFromRef(branchOnRemote); if (sourceCommit.id === originCommit.id) { // This is essentially also being up to date throw new GitError({ msg: intl.str('git-error-origin-fetch-uptodate') }); } // Otherwise fall through! We will update origin // and essentially delete the commit } } // now here is the tricky part -- the difference between local master // and remote master might be commits C2, C3, and C4, but the remote // might already have those commits. In this case, we don't need to // make them, so filter these out commitsToMake = _.filter(commitsToMake, function(commitJSON) { return !this.origin.refs[commitJSON.id]; }, this); var makeCommit = function(id, parentIDs) { // need to get the parents first. since we order by depth, we know // the dependencies are there already var parents = _.map(parentIDs, function(parentID) { return this.origin.refs[parentID]; }, this); return this.origin.makeCommit(parents, id); }.bind(this); // now make the promise chain to make each commit var chainStep = function(id, parents) { var newCommit = makeCommit(id, parents); return this.animationFactory.playCommitBirthPromiseAnimation( newCommit, this.origin.gitVisuals ); }.bind(this); var deferred = Q.defer(); var chain = deferred.promise; _.each(commitsToMake, function(commitJSON) { chain = chain.then(function() { return this.animationFactory.playHighlightPromiseAnimation( this.refs[commitJSON.id], branchOnRemote ); }.bind(this)); chain = chain.then(function() { return chainStep( commitJSON.id, commitJSON.parents ); }); }, this); chain = chain.then(function() { var localLocationID = this.getCommitFromRef(sourceLocation).get('id'); var remoteCommit = this.origin.refs[localLocationID]; this.origin.setTargetLocation(branchOnRemote, remoteCommit); // unhighlight local this.animationFactory.playRefreshAnimation(this.gitVisuals); return this.animationFactory.playRefreshAnimation(this.origin.gitVisuals); }.bind(this)); // HAX HAX update master and remote tracking for master chain = chain.then(function() { var localCommit = this.getCommitFromRef(sourceLocation); this.setTargetLocation(this.refs[ORIGIN_PREFIX + options.destination], localCommit); return this.animationFactory.playRefreshAnimation(this.gitVisuals); }.bind(this)); if (!options.dontResolvePromise) { this.animationQueue.thenFinish(chain, deferred); } }; GitEngine.prototype.pushDeleteRemoteBranch = function( remoteBranch, branchOnRemote ) { if (branchOnRemote.get('id') === 'master') { throw new GitError({ msg: intl.todo('You cannot delete master branch on remote!') }); } // ok so this isn't too bad -- we basically just: // 1) instruct the remote to delete the branch // 2) kill off the remote branch locally // 3) find any branches tracking this remote branch and set them to not track var id = remoteBranch.get('id'); this.origin.deleteBranch(branchOnRemote); this.deleteBranch(remoteBranch); this.branchCollection.each(function(branch) { if (branch.getRemoteTrackingBranchID() === id) { branch.setRemoteTrackingBranchID(null); } }, this); // animation needs to be triggered on origin directly this.origin.pruneTree(); this.origin.externalRefresh(); }; GitEngine.prototype.fetch = function(options) { options = options || {}; var didMakeBranch; // first check for super stupid case where we are just making // a branch with fetch... if (options.destination && options.source === '') { this.validateAndMakeBranch( options.destination, this.getCommitFromRef('HEAD') ); return; } else if (options.destination && options.source) { didMakeBranch = didMakeBranch || this.makeRemoteBranchIfNeeded(options.source); didMakeBranch = didMakeBranch || this.makeBranchIfNeeded(options.destination); options.didMakeBranch = didMakeBranch; return this.fetchCore([{ destination: options.destination, source: options.source }], options ); } // get all remote branches and specify the dest / source pairs var allBranchesOnRemote = this.origin.branchCollection.toArray(); var sourceDestPairs = _.map(allBranchesOnRemote, function(branch) { var branchName = branch.get('id'); didMakeBranch = didMakeBranch || this.makeRemoteBranchIfNeeded(branchName); return { destination: branch.getPrefixedID(), source: branchName }; }, this); options.didMakeBranch = didMakeBranch; return this.fetchCore(sourceDestPairs, options); }; GitEngine.prototype.fetchCore = function(sourceDestPairs, options) { // first check if our local remote branch is upstream of the origin branch set. // this check essentially pretends the local remote branch is in origin and // could be fast forwarded (basic sanity check) _.each(sourceDestPairs, function(pair) { this.checkUpstreamOfSource( this, this.origin, pair.destination, pair.source ); }, this); // then we get the difference in commits between these two graphs var commitsToMake = []; _.each(sourceDestPairs, function(pair) { commitsToMake = commitsToMake.concat(this.getTargetGraphDifference( this, this.origin, pair.destination, pair.source, Object.assign( {}, options, {dontThrowOnNoFetch: true} ) )); }, this); if (!commitsToMake.length && !options.dontThrowOnNoFetch) { throw new GitError({ msg: intl.str('git-error-origin-fetch-uptodate') }); } // we did this for each remote branch, but we still need to reduce to unique // and sort. in this particular app we can never have unfected remote // commits that are upstream of multiple branches (since the fakeTeamwork // command simply commits), but we are doing it anyways for correctness commitsToMake = Graph.getUniqueObjects(commitsToMake); commitsToMake = Graph.descendSortDepth(commitsToMake); // now here is the tricky part -- the difference between local master // and remote master might be commits C2, C3, and C4, but we // might already have those commits. In this case, we don't need to // make them, so filter these out commitsToMake = _.filter(commitsToMake, function(commitJSON) { return !this.refs[commitJSON.id]; }, this); var makeCommit = function(id, parentIDs) { // need to get the parents first. since we order by depth, we know // the dependencies are there already var parents = _.map(parentIDs, function(parentID) { return this.refs[parentID]; }, this); return this.makeCommit(parents, id); }.bind(this); // now make the promise chain to make each commit var chainStep = function(id, parents) { var newCommit = makeCommit(id, parents); return this.animationFactory.playCommitBirthPromiseAnimation( newCommit, this.gitVisuals ); }.bind(this); var deferred = Q.defer(); var chain = deferred.promise; if (options.didMakeBranch) { chain = chain.then(function() { this.animationFactory.playRefreshAnimation(this.origin.gitVisuals); return this.animationFactory.playRefreshAnimation(this.gitVisuals); }.bind(this)); } var originBranchSet = this.origin.getUpstreamBranchSet(); _.each(commitsToMake, function(commitJSON) { // technically we could grab the wrong one here // but this works for now var originBranch = originBranchSet[commitJSON.id][0].obj; var localBranch = this.refs[originBranch.getPrefixedID()]; chain = chain.then(function() { return this.animationFactory.playHighlightPromiseAnimation( this.origin.refs[commitJSON.id], localBranch ); }.bind(this)); chain = chain.then(function() { return chainStep( commitJSON.id, commitJSON.parents ); }); }, this); chain = chain.then(function() { // update all the destinations _.each(sourceDestPairs, function(pair) { var ours = this.refs[pair.destination]; var theirCommitID = this.origin.getCommitFromRef(pair.source).get('id'); // by definition we just made the commit with this id, // so we can grab it now var localCommit = this.refs[theirCommitID]; this.setTargetLocation(ours, localCommit); }, this); // unhighlight origin by refreshing this.animationFactory.playRefreshAnimation(this.origin.gitVisuals); return this.animationFactory.playRefreshAnimation(this.gitVisuals); }.bind(this)); if (!options.dontResolvePromise) { this.animationQueue.thenFinish(chain, deferred); } return { chain: chain, deferred: deferred }; }; GitEngine.prototype.pull = function(options) { options = options || {}; var localBranch = this.getOneBeforeCommit('HEAD'); // no matter what fetch var pendingFetch = this.fetch({ dontResolvePromise: true, dontThrowOnNoFetch: true, source: options.source, destination: options.destination }); if (!pendingFetch) { // short circuited for some reason return; } var destBranch = this.refs[options.destination]; // then either rebase or merge if (options.isRebase) { this.pullFinishWithRebase(pendingFetch, localBranch, destBranch); } else { this.pullFinishWithMerge(pendingFetch, localBranch, destBranch); } }; GitEngine.prototype.pullFinishWithRebase = function( pendingFetch, localBranch, remoteBranch ) { var chain = pendingFetch.chain; var deferred = pendingFetch.deferred; chain = chain.then(function() { if (this.isUpstreamOf(remoteBranch, localBranch)) { this.command.set('error', new CommandResult({ msg: intl.str('git-result-uptodate') })); throw SHORT_CIRCUIT_CHAIN; } }.bind(this)); // delay a bit after the intense refresh animation from // fetch chain = chain.then(function() { return this.animationFactory.getDelayedPromise(300); }.bind(this)); chain = chain.then(function() { // highlight last commit on o/master to color of // local branch return this.animationFactory.playHighlightPromiseAnimation( this.getCommitFromRef(remoteBranch), localBranch ); }.bind(this)); chain = chain.then(function() { pendingFetch.dontResolvePromise = true; // Lets move the git pull --rebase check up here. if (this.isUpstreamOf(localBranch, remoteBranch)) { this.setTargetLocation( localBranch, this.getCommitFromRef(remoteBranch) ); this.checkout(localBranch); return this.animationFactory.playRefreshAnimation(this.gitVisuals); } try { return this.rebase(remoteBranch, localBranch, pendingFetch); } catch (err) { this.filterError(err); if (err.getMsg() !== intl.str('git-error-rebase-none')) { throw err; } this.setTargetLocation( localBranch, this.getCommitFromRef(remoteBranch) ); this.checkout(localBranch); return this.animationFactory.playRefreshAnimation(this.gitVisuals); } }.bind(this)); chain = chain.fail(catchShortCircuit); this.animationQueue.thenFinish(chain, deferred); }; GitEngine.prototype.pullFinishWithMerge = function( pendingFetch, localBranch, remoteBranch ) { var chain = pendingFetch.chain; var deferred = pendingFetch.deferred; chain = chain.then(function() { if (this.mergeCheck(remoteBranch, localBranch)) { this.command.set('error', new CommandResult({ msg: intl.str('git-result-uptodate') })); throw SHORT_CIRCUIT_CHAIN; } }.bind(this)); // delay a bit after the intense refresh animation from // fetch chain = chain.then(function() { return this.animationFactory.getDelayedPromise(300); }.bind(this)); chain = chain.then(function() { // highlight last commit on o/master to color of // local branch return this.animationFactory.playHighlightPromiseAnimation( this.getCommitFromRef(remoteBranch), localBranch ); }.bind(this)); chain = chain.then(function() { // highlight commit on master to color of remote return this.animationFactory.playHighlightPromiseAnimation( this.getCommitFromRef(localBranch), remoteBranch ); }.bind(this)); // delay and merge chain = chain.then(function() { return this.animationFactory.getDelayedPromise(700); }.bind(this)); chain = chain.then(function() { var newCommit = this.merge(remoteBranch); if (!newCommit) { // it is a fast forward return this.animationFactory.playRefreshAnimation(this.gitVisuals); } return this.animationFactory.playCommitBirthPromiseAnimation( newCommit, this.gitVisuals ); }.bind(this)); chain = chain.fail(catchShortCircuit); this.animationQueue.thenFinish(chain, deferred); }; GitEngine.prototype.fakeTeamwork = function(numToMake, branch) { var makeOriginCommit = function() { var id = this.getUniqueID(); return this.origin.receiveTeamwork(id, branch, this.animationQueue); }.bind(this); var chainStep = function() { var newCommit = makeOriginCommit(); return this.animationFactory.playCommitBirthPromiseAnimation( newCommit, this.origin.gitVisuals ); }.bind(this); var deferred = Q.defer(); var chain = deferred.promise; _.each(_.range(numToMake), function(i) { chain = chain.then(function() { return chainStep(); }); }); this.animationQueue.thenFinish(chain, deferred); }; GitEngine.prototype.receiveTeamwork = function(id, branch, animationQueue) { this.checkout(this.resolveID(branch)); var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id); this.setTargetLocation(this.HEAD, newCommit); return newCommit; }; GitEngine.prototype.cherrypick = function(commit) { // 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.commit = function(options) { options = options || {}; var targetCommit = this.getCommitFromRef(this.HEAD); var id = null; // if we want to amend, go one above if (options.isAmend) { targetCommit = this.resolveID('HEAD~1'); id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id')); } var newCommit = this.makeCommit([targetCommit], id); if (this.getDetachedHead() && this.mode === 'git') { this.command.addWarning(intl.str('git-warning-detached')); } 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('Don\'t call this with null / undefined'); } if (typeof idOrTarget !== 'string') { return idOrTarget; } return this.resolveStringRef(idOrTarget); }; GitEngine.prototype.resolveRelativeRef = function(commit, relative) { var regex = /([~\^])(\d*)/g; var matches; while (matches = regex.exec(relative)) { var next = commit; var num = matches[2] ? parseInt(matches[2], 10) : 1; if (matches[1] == '^') { next = commit.getParent(num-1); } else { while (next && num--) { next = next.getParent(0); } } if (!next) { var msg = intl.str('git-error-relative-ref', { commit: commit.id, match: matches[0] }); throw new GitError({ msg: msg }); } commit = next; } return commit; }; GitEngine.prototype.resolveStringRef = function(ref) { ref = this.crappyUnescape(ref); if (this.refs[ref]) { return this.refs[ref]; } // Commit hashes like C4 are case insensitive if (ref.match(/^c\d+'*/) && this.refs[ref.toUpperCase()]) { return this.refs[ref.toUpperCase()]; } // Attempt to split ref string into a reference and a string of ~ and ^ modifiers. var startRef = null; var relative = null; var regex = /^([a-zA-Z0-9]+)(([~\^]\d*)*)/; var matches = regex.exec(ref); if (matches) { startRef = matches[1]; relative = matches[2]; } else { throw new GitError({ msg: intl.str('git-error-exist', {ref: ref}) }); } if (!this.refs[startRef]) { throw new GitError({ msg: intl.str('git-error-exist', {ref: ref}) }); } var commit = this.getCommitFromRef(startRef); if (relative) { commit = this.resolveRelativeRef( commit, relative ); } return commit; }; 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.updateBranchesFromSet = function(commitSet) { if (!commitSet) { throw new Error('need commit set here'); } // commitSet is the set of commits that are stale or moved or whatever. // any branches POINTING to these commits need to be moved! // first get a list of what branches influence what commits var upstreamSet = this.getUpstreamBranchSet(); var branchesToUpdate = {}; // now loop over the set we got passed in and find which branches // that means (aka intersection) _.each(commitSet, function(val, id) { _.each(upstreamSet[id], function(branchJSON) { branchesToUpdate[branchJSON.id] = true; }); }, this); var branchList = _.map(branchesToUpdate, function(val, id) { return id; }); return this.updateBranchesForHg(branchList); }; GitEngine.prototype.updateAllBranchesForHgAndPlay = function(branchList) { return this.updateBranchesForHg(branchList) && this.animationFactory.playRefreshAnimationSlow(this.gitVisuals); }; GitEngine.prototype.updateAllBranchesForHg = function() { var branchList = this.branchCollection.map(function(branch) { return branch.get('id'); }); return this.updateBranchesForHg(branchList); }; GitEngine.prototype.syncRemoteBranchFills = function() { this.branchCollection.each(function(branch) { if (!branch.getIsRemote()) { return; } var originBranch = this.origin.refs[branch.getBaseID()]; if (!originBranch.get('visBranch')) { // testing mode doesn't get this return; } var originFill = originBranch.get('visBranch').get('fill'); branch.get('visBranch').set('fill', originFill); }, this); }; GitEngine.prototype.updateBranchesForHg = function(branchList) { var hasUpdated = false; _.each(branchList, function(branchID) { // ok now just check if this branch has a more recent commit available. // that mapping is easy because we always do rebase alt id -- // theres no way to have C3' and C3''' but no C3''. so just // bump the ID once -- if thats not filled in we are updated, // otherwise loop until you find undefined var commitID = this.getCommitFromRef(branchID).get('id'); var altID = this.getBumpedID(commitID); if (!this.refs[altID]) { return; } hasUpdated = true; var lastID; while (this.refs[altID]) { lastID = altID; altID = this.rebaseAltID(altID); } // last ID is the one we want to update to this.setTargetLocation(this.refs[branchID], this.refs[lastID]); }, this); if (!hasUpdated) { return false; } return true; }; GitEngine.prototype.updateCommitParentsForHgRebase = function(commitSet) { var anyChange = false; _.each(commitSet, function(val, commitID) { var commit = this.refs[commitID]; var thisUpdated = commit.checkForUpdatedParent(this); anyChange = anyChange || thisUpdated; }, this); return anyChange; }; GitEngine.prototype.pruneTreeAndPlay = function() { return this.pruneTree() && this.animationFactory.playRefreshAnimationSlow(this.gitVisuals); }; GitEngine.prototype.pruneTree = function() { var set = this.getUpstreamBranchSet(); // don't prune commits that HEAD depends on var headSet = Graph.getUpstreamSet(this, 'HEAD'); _.each(headSet, function(val, commitID) { set[commitID] = true; }); var toDelete = []; this.commitCollection.each(function(commit) { // nothing cares about this commit :( if (!set[commit.get('id')]) { toDelete.push(commit); } }, this); if (!toDelete.length) { // returning nothing will perform // the switch sync return; } if (this.command) { this.command.addWarning(intl.str('hg-prune-tree')); } _.each(toDelete, function(commit) { commit.removeFromParents(); this.commitCollection.remove(commit); var ID = commit.get('id'); this.refs[ID] = undefined; delete this.refs[ID]; var visNode = commit.get('visNode'); if (visNode) { visNode.removeAll(); } }, this); return true; }; GitEngine.prototype.getUpstreamBranchSet = function() { return this.getUpstreamCollectionSet(this.branchCollection); }; GitEngine.prototype.getUpstreamTagSet = function() { return this.getUpstreamCollectionSet(this.tagCollection); }; GitEngine.prototype.getUpstreamCollectionSet = function(collection) { // 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; }; collection.each(function(ref) { var set = bfsSearch(ref.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], ref.get('id'))) { commitToSet[id].push({ obj: ref, id: ref.get('id') }); } }); }); return commitToSet; }; GitEngine.prototype.getUpstreamHeadSet = function() { var set = Graph.getUpstreamSet(this, '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.scrapeBaseID = function(id) { var results = /^C(\d+)/.exec(id); if (!results) { throw new Error('regex failed on ' + id); } return 'C' + results[1]; }; /* * grabs a bumped ID that is NOT currently reserved */ GitEngine.prototype.rebaseAltID = function(id) { var newID = this.getBumpedID(id); while (this.refs[newID]) { newID = this.getBumpedID(newID); } return newID; }; GitEngine.prototype.getMostRecentBumpedID = function(id) { var newID = id; var lastID; while (this.refs[newID]) { lastID = newID; newID = this.getBumpedID(newID); } return lastID; }; GitEngine.prototype.getBumpedID = function(id) { // this function alters an ID to add a quote to the end, // indicating that it was rebased. 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 loop for early return (instead of _.each) 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 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.dateSortFunc = function(cA, cB) { var dateA = new Date(cA.get('createTime')); var dateB = new Date(cB.get('createTime')); if (dateA - dateB === 0) { // hmmmmm this still needs fixing. we need to know basically just WHEN a commit was created, but since // we strip off the date creation field, when loading a tree from string this fails :-/ // there's actually no way to determine it... //c.warn('WUT it is equal'); //c.log(cA, cB); return GitEngine.prototype.idSortFunc(cA, cB); } return dateA - dateB; }; GitEngine.prototype.hgRebase = function(destination, base) { var deferred = Q.defer(); var chain = this.rebase(destination, base, { dontResolvePromise: true, deferred: deferred }); // was upstream or something if (!chain) { return; } // ok lets grab the merge base first var commonAncestor = this.getCommonAncestor(destination, base); var baseCommit = this.getCommitFromRef(base); // we need everything BELOW ourselves... var downstream = this.getDownstreamSet(base); // and we need to go upwards to the stop set var stopSet = Graph.getUpstreamSet(this, destination); var upstream = this.getUpstreamDiffSetFromSet(stopSet, base); // and NOWWWwwww get all the descendants of this set var moreSets = []; _.each(upstream, function(val, id) { moreSets.push(this.getDownstreamSet(id)); }, this); var masterSet = {}; masterSet[baseCommit.get('id')] = true; _.each([upstream, downstream].concat(moreSets), function(set) { _.each(set, function(val, id) { masterSet[id] = true; }); }); // we also need the branches POINTING to master set var branchMap = {}; var upstreamSet = this.getUpstreamBranchSet(); _.each(masterSet, function(val, commitID) { // now loop over that commits branches _.each(upstreamSet[commitID], function(branchJSON) { branchMap[branchJSON.id] = true; }); }); var branchList = _.map(branchMap, function(val, id) { return id; }); chain = chain.then(function() { // now we just moved a bunch of commits, but we haven't updated the // dangling guys. lets do that and then prune var anyChange = this.updateCommitParentsForHgRebase(masterSet); if (!anyChange) { return; } return this.animationFactory.playRefreshAnimationSlow(this.gitVisuals); }.bind(this)); chain = chain.then(function() { return this.updateAllBranchesForHgAndPlay(branchList); }.bind(this)); chain = chain.then(function() { // now that we have moved branches, lets prune return this.pruneTreeAndPlay(); }.bind(this)); this.animationQueue.thenFinish(chain, deferred); }; GitEngine.prototype.rebase = function(targetSource, currentLocation, options) { // first some conditions if (this.isUpstreamOf(targetSource, currentLocation)) { this.command.setResult(intl.str('git-result-uptodate')); // 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(intl.str('git-result-fastforward')); 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 = Graph.getUpstreamSet(this, targetSource); var toRebaseRough = this.getUpstreamDiffFromSet(stopSet, currentLocation); return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation, options); }; GitEngine.prototype.getUpstreamDiffSetFromSet = function(stopSet, location) { var set = {}; _.each(this.getUpstreamDiffFromSet(stopSet, location), function(commit) { set[commit.get('id')] = true; }); return set; }; GitEngine.prototype.getUpstreamDiffFromSet = function(stopSet, location) { var result = Graph.bfsFromLocationWithSet(this, location, stopSet); result.sort(this.dateSortFunc); return result; }; GitEngine.prototype.getInteractiveRebaseCommits = function(targetSource, currentLocation) { var stopSet = Graph.getUpstreamSet(this, 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.dateSortFunc); } // throw out 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: intl.str('git-error-rebase-none') }); } return toRebase; }; GitEngine.prototype.rebaseInteractiveTest = function(targetSource, currentLocation, options) { options = options || {}; // Get the list of commits that would be displayed to the user var toRebase = this.getInteractiveRebaseCommits(targetSource, currentLocation); var rebaseMap = {}; _.each(toRebase, function(commit) { var id = commit.get('id'); rebaseMap[id] = commit; }); var rebaseOrder; if (options['interactiveTest'].length === 0) { // If no commits were explicitly specified for the rebase, act like the user didn't change anything // in the rebase dialog and hit confirm rebaseOrder = toRebase; } else { // Get the list and order of commits specified var idsToRebase = options['interactiveTest'][0].split(','); // Verify each chosen commit exists in the list of commits given to the user var extraCommits = []; rebaseOrder = []; _.each(idsToRebase, function(id) { if (id in rebaseMap) { rebaseOrder.push(rebaseMap[id]); } else { extraCommits.push(id); } }); if (extraCommits.length > 0) { throw new GitError({ msg: intl.todo('Hey those commits don\'t exist in the set!') }); } } this.rebaseFinish(rebaseOrder, {}, targetSource, currentLocation); }; GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation, options) { options = options || {}; // there are a reduced set of checks now, so we can't exactly use parts of the rebase function // but it will look similar. var toRebase = this.getInteractiveRebaseCommits(targetSource, currentLocation); // 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 deferred = Q.defer(); deferred.promise .then(function(userSpecifiedRebase) { // first, they might have dropped everything (annoying) if (!userSpecifiedRebase.length) { throw new CommandResult({ msg: intl.str('git-result-nothing') }); } // finish the rebase crap and animate! this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation); }.bind(this)) .fail(function(err) { this.filterError(err); this.command.set('error', err); this.animationQueue.start(); }.bind(this)) .done(); // If we have a solution provided, set up the GUI to display it by default var initialCommitOrdering; if (options.initialCommitOrdering && options.initialCommitOrdering.length > 0) { var rebaseMap = {}; _.each(toRebase, function(commit) { rebaseMap[commit.get('id')] = true; }); // Verify each chosen commit exists in the list of commits given to the user initialCommitOrdering = []; _.each(options.initialCommitOrdering[0].split(','), function(id) { if (!rebaseMap[id]) { throw new GitError({ msg: intl.todo('Hey those commits don\'t exist in the set!') }); } initialCommitOrdering.push(id); }); } var InteractiveRebaseView = require('../views/rebaseView').InteractiveRebaseView; // interactive rebase view will reject or resolve our promise new InteractiveRebaseView({ deferred: deferred, toRebase: toRebase, initialCommitOrdering: initialCommitOrdering, aboveAll: options.aboveAll }); }; GitEngine.prototype.filterRebaseCommits = function( toRebaseRough, stopSet, options ) { var changesAlreadyMade = {}; _.each(stopSet, function(val, key) { changesAlreadyMade[this.scrapeBaseID(key)] = true; }, this); var uniqueIDs = {}; // resolve the commits we will rebase return _.filter(toRebaseRough, function(commit) { // no merge commits, unless we preserve if (commit.get('parents').length !== 1 && !options.preserveMerges) { return false; } // 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 don't rebase the C4' again. var baseID = this.scrapeBaseID(commit.get('id')); if (changesAlreadyMade[baseID]) { return false; } // make unique if (uniqueIDs[commit.get('id')]) { return false; } uniqueIDs[commit.get('id')] = true; return true; }, this); }; GitEngine.prototype.getRebasePreserveMergesParents = function(oldCommit) { var oldParents = oldCommit.get('parents'); return _.map(oldParents, function(parent) { var oldID = parent.get('id'); var newID = this.getMostRecentBumpedID(oldID); return this.refs[newID]; }, this); }; GitEngine.prototype.rebaseFinish = function( toRebaseRough, stopSet, targetSource, currentLocation, options ) { options = options || {}; // now we have the all the commits between currentLocation and the set of target to rebase. var destinationBranch = this.resolveID(targetSource); var deferred = options.deferred || Q.defer(); var chain = options.chain || deferred.promise; var toRebase = this.filterRebaseCommits(toRebaseRough, stopSet, options); if (!toRebase.length) { throw new GitError({ msg: intl.str('git-error-rebase-none') }); } chain = this.animationFactory.highlightEachWithPromise( chain, toRebase, destinationBranch ); // now pop all of these commits onto targetLocation var base = this.getCommitFromRef(targetSource); var hasStartedChain = false; // each step makes a new commit var chainStep = function(oldCommit) { var newId = this.rebaseAltID(oldCommit.get('id')); var parents; if (!options.preserveMerges || !hasStartedChain) { // easy logic since we just have a straight line parents = [base]; } else { // preserving merges // we always define the parent for the first commit to plop, // otherwise search for most recent parents parents = (hasStartedChain) ? this.getRebasePreserveMergesParents(oldCommit) : [base]; } var newCommit = this.makeCommit(parents, newId); base = newCommit; hasStartedChain = true; return this.animationFactory.playCommitBirthPromiseAnimation( newCommit, this.gitVisuals ); }.bind(this); // set up the promise chain _.each(toRebase, function(commit) { chain = chain.then(function() { return chainStep(commit); }); }, this); chain = chain.then(function() { if (this.resolveID(currentLocation).get('type') == 'commit') { // we referenced a commit like git rebase C2 C1, so we have // to manually check out C1' this.checkout(base); } else { // now we just need to update the rebased branch is this.setTargetLocation(currentLocation, base); this.checkout(currentLocation); } return this.animationFactory.playRefreshAnimation(this.gitVisuals); }.bind(this)); if (!options.dontResolvePromise) { this.animationQueue.thenFinish(chain, deferred); } return chain; }; GitEngine.prototype.mergeCheck = function(targetSource, currentLocation) { var sameCommit = this.getCommitFromRef(targetSource) === this.getCommitFromRef(currentLocation); return this.isUpstreamOf(targetSource, currentLocation) || sameCommit; }; GitEngine.prototype.merge = function(targetSource, options) { options = options || {}; var currentLocation = 'HEAD'; // first some conditions if (this.mergeCheck(targetSource, currentLocation)) { throw new CommandResult({ msg: intl.str('git-result-uptodate') }); } if (this.isUpstreamOf(currentLocation, targetSource) && !options.noFF) { // 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(intl.str('git-result-fastforward')); 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 = intl.str( 'git-merge-msg', { target: this.resolveName(targetSource), current: 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.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'); // check if this is an origin branch, and if so go to the commit referenced if (type === 'branch' && target.getIsRemote()) { target = this.getCommitFromRef(target.get('id')); } if (type !== 'branch' && type !== 'tag' && type !== 'commit') { throw new GitError({ msg: intl.str('git-error-options') }); } if (type === 'tag') { target = target.get('target'); } this.HEAD.set('target', target); }; GitEngine.prototype.forceBranch = function(branchName, where) { branchName = this.crappyUnescape(branchName); // 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: intl.str('git-error-options') }); } if (branch.getIsRemote()) { throw new GitError({ msg: intl.str('git-error-remote-branch') }); } var whereCommit = this.getCommitFromRef(where); this.setTargetLocation(branch, whereCommit); }; GitEngine.prototype.branch = function(name, ref) { var target = this.getCommitFromRef(ref); var newBranch = this.validateAndMakeBranch(name, target); ref = this.resolveID(ref); if (this.isRemoteBranchRef(ref)) { this.setLocalToTrackRemote(newBranch, ref); } }; GitEngine.prototype.isRemoteBranchRef = function(ref) { var resolved = this.resolveID(ref); if (resolved.get('type') !== 'branch') { return false; } return resolved.getIsRemote(); }; GitEngine.prototype.tag = function(name, ref) { var target = this.getCommitFromRef(ref); this.validateAndMakeTag(name, target); }; GitEngine.prototype.describe = function(ref) { var startCommit = this.getCommitFromRef(ref); // ok we need to BFS from start upwards until we hit a tag. but // first we need to get a reverse mapping from tag to commit var tagMap = {}; _.each(this.tagCollection.toJSON(), function(tag) { tagMap[tag.target.get('id')] = tag.id; }); var pQueue = [startCommit]; var foundTag; var numAway = []; while (pQueue.length) { var popped = pQueue.pop(); var thisID = popped.get('id'); if (tagMap[thisID]) { foundTag = tagMap[thisID]; break; } // ok keep going numAway.push(popped.get('id')); var parents = popped.get('parents'); if (parents && parents.length) { pQueue = pQueue.concat(parents); pQueue.sort(this.dateSortFunc); } } if (!foundTag) { throw new GitError({ msg: intl.todo('Fatal: no tags found upstream') }); } if (numAway.length === 0) { throw new CommandResult({ msg: foundTag }); } // then join throw new CommandResult({ msg: foundTag + '_' + numAway.length + '_g' + startCommit.get('id') }); }; GitEngine.prototype.validateAndDeleteBranch = function(name) { // trying to delete, lets check our refs var target = this.resolveID(name); if (target.get('type') !== 'branch' || target.get('id') == 'master' || this.HEAD.get('target') === target) { throw new GitError({ msg: intl.str('git-error-branch') }); } // now we know it's a branch var branch = target; // if its remote if (target.getIsRemote()) { throw new GitError({ msg: intl.str('git-error-remote-branch') }); } this.deleteBranch(branch); }; GitEngine.prototype.deleteBranch = function(branch) { this.branchCollection.remove(branch); this.refs[branch.get('id')] = undefined; delete this.refs[branch.get('id')]; // also in some cases external engines call our delete, so // verify integrity of HEAD here if (this.HEAD.get('target') === branch) { this.HEAD.set('target', this.refs['master']); } if (branch.get('visBranch')) { branch.get('visBranch').remove(); } }; GitEngine.prototype.crappyUnescape = function(str) { return str.replace(/'/g, "'").replace(///g, "/"); }; GitEngine.prototype.filterError = function(err) { if (!(err instanceof GitError || err instanceof CommandResult)) { throw err; } }; // called on a origin repo from a local -- simply refresh immediately with // an animation GitEngine.prototype.externalRefresh = function() { this.animationQueue = new AnimationQueue({ callback: function() {} }); this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals); this.animationQueue.start(); }; GitEngine.prototype.dispatch = function(command, deferred) { this.command = command; var vcs = command.get('vcs'); var executeCommand = function() { this.dispatchProcess(command, deferred); }.bind(this); // handle mode change will either execute sync or // animate during tree pruning / etc this.handleModeChange(vcs, executeCommand); }; GitEngine.prototype.dispatchProcess = function(command, deferred) { // set up the animation queue var whenDone = function() { command.finishWith(deferred); }.bind(this); this.animationQueue = new AnimationQueue({ callback: whenDone }); var vcs = command.get('vcs'); var methodName = command.get('method').replace(/-/g, ''); try { Commands.commands.execute(vcs, methodName, this, this.command); } catch (err) { this.filterError(err); // short circuit animation by just setting error and returning command.set('error', err); deferred.resolve(); return; } var willStartAuto = this.animationQueue.get('defer') || this.animationQueue.get('promiseBased'); // only add the refresh if we didn't do manual animations if (!this.animationQueue.get('animations').length && !willStartAuto) { this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals); } // animation queue will call the callback when its done if (!willStartAuto) { this.animationQueue.start(); } }; GitEngine.prototype.show = function(ref) { var commit = this.getCommitFromRef(ref); throw new CommandResult({ msg: commit.getShowEntry() }); }; GitEngine.prototype.status = function() { // UGLY todo var lines = []; if (this.getDetachedHead()) { lines.push(intl.str('git-status-detached')); } else { var branchName = this.HEAD.get('target').get('id'); lines.push(intl.str('git-status-onbranch', {branch: branchName})); } lines.push('Changes to be committed:'); lines.push(''); lines.push(TAB + 'modified: cal/OskiCostume.stl'); lines.push(''); lines.push(intl.str('git-status-readytocommit')); var msg = ''; _.each(lines, function(line) { msg += '# ' + line + '\n'; }); throw new CommandResult({ msg: msg }); }; GitEngine.prototype.logWithout = function(ref, omitBranch) { // slice off the ^branch omitBranch = omitBranch.slice(1); this.log(ref, Graph.getUpstreamSet(this, 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.getCommonAncestor = function(ancestor, cousin, dontThrow) { if (this.isUpstreamOf(cousin, ancestor) && !dontThrow) { throw new Error('Don\'t use common ancestor if we are upstream!'); } var upstreamSet = Graph.getUpstreamSet(this, 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 aren\'t 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 = Graph.getUpstreamSet(this, ancestor); return upstream[child.get('id')] !== undefined; }; GitEngine.prototype.getDownstreamSet = function(ancestor) { var commit = this.getCommitFromRef(ancestor); var ancestorID = commit.get('id'); var queue = [commit]; var exploredSet = {}; exploredSet[ancestorID] = true; var addToExplored = function(child) { exploredSet[child.get('id')] = true; queue.push(child); }; while (queue.length) { var here = queue.pop(); var children = here.get('children'); _.each(children, addToExplored); } return exploredSet; }; var Ref = Backbone.Model.extend({ initialize: function() { if (!this.get('target')) { throw new Error('must be initialized with target'); } if (!this.get('id')) { throw new Error('must be given an id'); } this.set('type', 'general ref'); if (this.get('id') == 'HEAD') { this.set('lastLastTarget', null); this.set('lastTarget', this.get('target')); // have HEAD remember where it is for checkout - this.on('change:target', this.targetChanged, this); } }, getIsRemote: function() { return false; }, getName: function() { return this.get('id'); }, 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, remoteTrackingBranchID: null, remote: false }, initialize: function() { Ref.prototype.initialize.call(this); this.set('type', 'branch'); }, /** * Here is the deal -- there are essentially three types of branches * we deal with: * 1) Normal local branches (that may track a remote branch) * 2) Local remote branches (o/master) that track an origin branch * 3) Origin branches (master) that exist in origin * * With that in mind, we change our branch model to support the following */ setRemoteTrackingBranchID: function(id) { this.set('remoteTrackingBranchID', id); }, getRemoteTrackingBranchID: function() { return this.get('remoteTrackingBranchID'); }, getPrefixedID: function() { if (this.getIsRemote()) { throw new Error('im already remote'); } return ORIGIN_PREFIX + this.get('id'); }, getBaseID: function() { if (!this.getIsRemote()) { throw new Error('im not remote so can\'t get base'); } return this.get('id').replace(ORIGIN_PREFIX, ''); }, getIsRemote: function() { if (typeof this.get('id') !== 'string') { debugger; } return this.get('id').slice(0, 2) === ORIGIN_PREFIX; } }); 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', intl.str('git-dummy-msg')); } 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')); }, getParent: function(parentNum) { if (this && this.attributes && this.attributes.parents) { return this.attributes.parents[parentNum]; } else { return null; } }, removeFromParents: function() { _.each(this.get('parents'), function(parent) { parent.removeChild(this); }, this); }, checkForUpdatedParent: function(engine) { var parents = this.get('parents'); if (parents.length > 1) { return; } var parent = parents[0]; var parentID = parent.get('id'); var newestID = engine.getMostRecentBumpedID(parentID); if (parentID === newestID) { // BOOM done, its already updated return; } // crap we have to switch var newParent = engine.refs[newestID]; this.removeFromParents(); this.set('parents', [newParent]); newParent.get('children').push(this); // when we run in test mode, our visnode and // visuals will be undefined so we need to check for their existence var visNode = this.get('visNode'); if (visNode) { visNode.removeAllEdges(); } var gitVisuals = this.get('gitVisuals'); if (gitVisuals) { gitVisuals.addEdge(this.get('id'), newestID); } return true; }, removeChild: function(childToRemove) { var newChildren = []; _.each(this.get('children'), function(child) { if (child !== childToRemove) { newChildren.push(child); } }, this); this.set('children', newChildren); }, 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); } }); var Tag = Ref.extend({ defaults: { visTag: null }, initialize: function() { Ref.prototype.initialize.call(this); this.set('type', 'tag'); } }); exports.GitEngine = GitEngine; exports.Commit = Commit; exports.Branch = Branch; exports.Tag = Tag; exports.Ref = Ref;