pcottle.learnGitBranching/src/git.js
2012-10-12 13:47:16 -07:00

1073 lines
28 KiB
JavaScript

// 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.id_gen = 0;
this.branches = options.branches;
this.collection = options.collection;
// global variable to keep track of the options given
// along with the command call.
this.commandOptions = {};
this.generalArgs = [];
events.on('processCommand', _.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.collection.add(this.rootCommit);
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();
// now we are ready
events.trigger('gitEngineReady', this);
};
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 already exists!'
});
}
var branch = new Branch({
target: target,
id: id
});
this.branches.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.branches.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.printBranches = function() {
var branches = this.getBranches();
var result = '';
_.each(branches, function(branch) {
result += (branch.selected ? '* ' : '') + branch.id + '\n';
});
throw new CommandResult({
msg: result
});
};
GitEngine.prototype.logBranches = function() {
var branches = this.getBranches();
_.each(branches, function(branch) {
console.log((branch.selected ? '* ' : '') + branch.id);
});
};
GitEngine.prototype.makeCommit = function(parents, id) {
var commit = new Commit({
parents: parents,
id: id
});
this.refs[commit.get('id')] = commit;
this.collection.add(commit);
return commit;
};
GitEngine.prototype.acceptNoGeneralArgs = function() {
if (this.generalArgs.length) {
throw new GitError({
msg: "That command accepts no general arguments"
});
}
};
GitEngine.prototype.revertStarter = function() {
if (this.generalArgs.length !== 1) {
throw new GitError({
msg: "Specify the commit to revert to"
});
}
if (this.generalArgs[0] == 'HEAD') {
throw new GitError({
msg: "Can't revert to HEAD! That's where you are now"
});
return;
}
this.revert(this.generalArgs[0]);
};
GitEngine.prototype.revert = function(whichCommit) {
// first get that commit
var targetLocation = this.getCommitFromRef(whichCommit);
var here = this.getCommitFromRef('HEAD');
// then BFS from here to that commit
var toUndo = [];
var queue = [here];
while (queue.length) {
var popped = queue.pop();
if (popped.get('id') == targetLocation.get('id')) {
// we got all that we needed to get
break;
}
toUndo.push(popped);
queue = queue.concat(popped.get('parents'));
queue.sort(this.idSortFunc);
}
// now make a bunch of commits on top of where we are
var base = here;
toUndo.sort(this.idSortFunc);
for (var i = 0; i < toUndo.length; i++) {
var newId = this.rebaseAltId(toUndo[i].get('id'));
var newCommit = this.makeCommit([base], newId);
base = newCommit;
}
// done! update our location
this.setLocationTarget('HEAD', base);
};
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']);
}
if (this.generalArgs.length !== 1) {
throw new GitError({
msg: "Specify the commit to reset to (1 argument)"
});
}
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.setLocationTarget('HEAD', this.getCommitFromRef(target));
};
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;
if (this.commandOptions['-a']) {
this.command.addWarning('No need to add files in this demo');
}
if (this.commandOptions['-am']) {
var args = this.commandOptions['-am'];
if (args.length > 1) {
throw new GitError({
msg: "Commit -am doesn't make sense with more than one arg..."
});
}
this.command.addWarning("Don't worry about adding files in this demo. I'll take " +
"down your commit message but it's not important");
msg = args[0];
}
if (this.commandOptions['-m']) {
var args = this.commandOptions['-m'];
if (args.length > 1) {
throw new GitError({
msg: "Specifying a commit message with more than 1 arg doesnt make sense!"
});
}
msg = args[0];
}
var newCommit = this.commit();
if (msg) {
msg = msg
.replace(/&quot;/g, '"')
.replace(/^"/g, '')
.replace(/"$/g, '');
newCommit.set('commitMessage', msg);
}
animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit);
};
GitEngine.prototype.commit = function() {
var targetCommit = this.getCommitFromRef(this.HEAD);
// if we want to ammend, go one above
if (this.commandOptions['--amend']) {
targetCommit = this.resolveId('HEAD~1');
}
var newCommit = this.makeCommit([targetCommit]);
if (this.getDetachedHead()) {
this.command.addWarning('Warning!! Detached HEAD state');
this.HEAD.set('target', newCommit);
} else {
var targetBranch = this.HEAD.get('target');
targetBranch.set('target', newCommit);
}
return 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 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.setLocationTarget = function(ref, target) {
var 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 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.branches.each(function(branch) {
var set = bfsSearch(branch.get('target'));
_.each(set, function(id) {
commitToSet[id] = commitToSet[id] || [];
commitToSet[id].push(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;
}
var pQueue = [].concat(commit.get('parents') || []);
pQueue.sort(this.idSortFunc);
numBack--;
while (pQueue.length && numBack !== 0) {
var popped = pQueue.shift(0);
pQueue = pQueue.concat(popped.get('parents'));
pQueue.sort(this.idSortFunc);
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.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(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.rebaseStarter = function() {
if (this.generalArgs.length > 2) {
throw new GitError({
msg: 'rebase with more than 2 arguments doesnt make sense!'
});
}
if (!this.generalArgs.length) {
throw new GitError({
msg: 'Give me a branch to rebase onto!'
});
}
if (this.generalArgs.length == 1) {
this.generalArgs.push('HEAD');
}
var response = this.rebase(this.generalArgs[0], this.generalArgs[1]);
if (response === undefined) {
// was a fastforward or already up to date
return;
}
this.rebaseAnimation(response);
};
GitEngine.prototype.rebaseAnimation = function(response) {
// TODO: move to animation factory
var start = function() {
// maybe search stuff??
};
this.animationQueue.add(new Animation({
closure: start
}));
// first set all birth positions...
_.each(response, function(step) {
step.newCommit.get('visNode').setBirth();
}, this);
var fixedOpacity = 0.8;
// then fix all opacities... ugh
_.each(response, function(step) {
_.each(step.snapshot, function(obj) {
_.each(obj, function(attr) {
if (attr.opacity !== undefined) {
attr.opacity = fixedOpacity;
}
});
});
});
var time = GRAPHICS.defaultAnimationTime;
var bounceTime = time * 2.0;
_.each(response, function(step) {
this.animationQueue.add(new Animation({
closure: function() {
var id = step.newCommit.get('id');
var vNode = step.newCommit.get('visNode');
vNode.setBirth();
vNode.setOutgoingEdgesBirthPosition();
vNode.animateOutgoingEdgesFromSnapshot(step.snapshot, bounceTime, 'bounce');
vNode.animateFromAttr(step.snapshot[id], bounceTime, 'bounce');
},
duration: Math.max(bounceTime, time)
}));
}, this);
animationFactory.refreshTree(this.animationQueue);
};
GitEngine.prototype.rebase = function(targetSource, currentLocation) {
// first some conditions
if (this.isUpstreamOf(targetSource, 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.setLocationTarget(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...');
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);
// keep searching
pQueue = pQueue.concat(popped.get('parents'));
pQueue.sort(this.idSortFunc);
}
// now we have the all the commits between currentLocation and the set of target.
// we need to throw out merge commits
var toRebase = [];
_.each(toRebaseRough, function(commit) {
if (commit.get('parents').length == 1) {
toRebase.push(commit);
}
});
// now sort
toRebase.sort(this.idSortFunc);
// now pop all of these commits onto targetLocation
var base = this.getCommitFromRef(targetSource);
var animationInfo = [];
for (var i = 0; i < toRebase.length; i++) {
var old = toRebase[i];
var newId = this.rebaseAltId(old.get('id'));
var newCommit = this.makeCommit([base], newId);
base = newCommit;
animationInfo.push({
oldCommit: old,
newCommit: newCommit,
snapshot: gitVisuals.genSnapshot()
});
}
// now we just need to update where we are
this.setLocationTarget(currentLocation, base);
// for animation
return animationInfo;
};
GitEngine.prototype.mergeStarter = function() {
if (this.generalArgs.length > 2) {
throw new GitError({
msg: 'merge with more than 2 arguments doesnt make sense!'
});
}
if (!this.generalArgs.length) {
throw new GitError({
msg: 'Give me a branch to merge into!'
});
}
if (this.generalArgs.length == 1) {
this.generalArgs.push('HEAD');
}
if (_.include(this.generalArgs, 'HEAD') && this.getDetachedHead()) {
throw new GitError({
msg: 'Cant merge things referencing HEAD when you are in detached head!'
});
}
this.merge(this.generalArgs[0], this.generalArgs[1]);
};
GitEngine.prototype.merge = function(targetSource, currentLocation) {
// first some conditions
if (this.isUpstreamOf(targetSource, 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.setLocationTarget(currentLocation, this.getCommitFromRef(targetSource));
throw new CommandResult({
msg: 'Fast-forwarding...'
});
}
// now the part of making a merge commit
var parent1 = this.getCommitFromRef(currentLocation);
var parent2 = this.getCommitFromRef(targetSource);
var commit = this.makeCommit([parent1, parent2]);
this.setLocationTarget(currentLocation, commit)
};
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'];
if (!args.length) {
throw new GitError({
msg: 'I expect a branch name with "checkout -b"!!'
});
}
if (args.length > 2) {
throw new GitError({
msg: 'Only two args max with checkout -b please (the new name and target branch)'
});
}
// we are good!
if (args.length == 1) {
args.push('HEAD');
}
var validId = this.validateBranchName(args[0]);
this.branch(validId, args[1]);
this.checkout(validId);
return;
}
if (this.generalArgs.length != 1) {
throw new GitError({
msg: 'I expect one argument along with git checkout (dont reference files)'
});
}
this.checkout(this.generalArgs[0]);
events.trigger('treeRefresh');
};
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'];
names = names.concat(this.commandOptions['-D']);
if (!names.length) {
throw new GitError({
msg: 'I expect branch names when deleting'
});
}
_.each(names, _.bind(function(name) {
this.deleteBranch(name);
}, this));
return;
}
var len = this.generalArgs.length;
if (len > 2) {
throw new GitError({
msg: 'git branch with more than two general args does not make sense!'
});
}
if (len == 0) {
this.printBranches();
return;
}
if (len == 1) {
// making a branch from where we are now
this.generalArgs.push('HEAD');
}
this.branch(this.generalArgs[0], this.generalArgs[1]);
};
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"
});
}
var id = target.get('id');
target.delete();
delete this.refs[id];
// delete from array
// TODO
var toDelete = -1;
_.each(this.branches, function(branch, index) {
console.log(branch);
console.log(id);
if (branch.get('id') == id) {
toDelete = index;
}
});
this.branches.splice(toDelete, 1);
};
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 {
this[command.get('method') + 'Starter']();
} 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.add(new Animation({
closure: function() {
gitVisuals.refreshTree();
}
}));
}
// animation queue will call the callback when its done
this.animationQueue.start();
};
GitEngine.prototype.logStarter = function() {
if (this.generalArgs.length > 1) {
throw new GitError({
msg: "git log with more than 1 argument doesn't make sense"
});
}
if (this.generalArgs.length == 0) {
this.generalArgs.push('HEAD');
}
this.log(this.generalArgs[0]);
};
GitEngine.prototype.log = function(ref) {
// 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];
while (pQueue.length) {
var popped = pQueue.shift(0);
toDump.push(popped);
if (popped.get('parents') && popped.get('parents').length) {
pQueue = pQueue.concat(popped.get('parents'));
}
pQueue.sort(this.idSortFunc);
}
// 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 queue = [commit];
var exploredSet = {};
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');
},
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({
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
},
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'),
'<br/>',
this.get('commitMessage'),
'<br/>',
'Commit: ' + this.get('id')
].join('\n' ) + '\n';
},
validateAtInit: function() {
if (!this.get('id')) {
this.set('id', uniqueId('C'));
}
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 = gitVisuals.addNode(this.get('id'), this);
this.set('visNode', visNode);
},
addEdgeToVisuals: function(parent) {
gitVisuals.addEdge(this.get('id'), parent.get('id'));
},
initialize: function() {
this.validateAtInit();
this.addNodeToVisuals();
_.each(this.get('parents'), function(parent) {
parent.get('children').push(this);
this.addEdgeToVisuals(parent);
}, this);
}
});