mirror of
https://github.com/pcottle/learnGitBranching.git
synced 2025-06-29 17:27:22 +02:00
with raphael
This commit is contained in:
parent
d0a4d1956c
commit
b21f2e536f
3 changed files with 452 additions and 0 deletions
10
lib/raphael-min.js
vendored
Normal file
10
lib/raphael-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
255
src/commandModel.js
Normal file
255
src/commandModel.js
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
var Command = Backbone.Model.extend({
|
||||||
|
defaults: {
|
||||||
|
status: 'inqueue',
|
||||||
|
result: '',
|
||||||
|
error: null,
|
||||||
|
generalArgs: [],
|
||||||
|
supportedMap: {},
|
||||||
|
options: null,
|
||||||
|
method: null,
|
||||||
|
createTime: null,
|
||||||
|
rawStr: null
|
||||||
|
},
|
||||||
|
|
||||||
|
validateAtInit: function() {
|
||||||
|
if (!this.get('rawStr')) {
|
||||||
|
throw new Error('Give me a string!');
|
||||||
|
}
|
||||||
|
if (!this.get('createTime')) {
|
||||||
|
this.set('createTime', new Date().toString());
|
||||||
|
}
|
||||||
|
this.on('change:error', this.errorChanged, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function() {
|
||||||
|
this.validateAtInit();
|
||||||
|
this.parseOrCatch();
|
||||||
|
},
|
||||||
|
|
||||||
|
parseOrCatch: function() {
|
||||||
|
try {
|
||||||
|
this.parse();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof CommandProcessError ||
|
||||||
|
err instanceof GitError) {
|
||||||
|
this.set('status', 'error');
|
||||||
|
this.set('error', err);
|
||||||
|
} else if (err instanceof CommandResult) {
|
||||||
|
this.set('status', 'finished');
|
||||||
|
this.set('error', err);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
errorChanged: function(model, err) {
|
||||||
|
this.set('err', err);
|
||||||
|
this.set('status', 'error');
|
||||||
|
this.formatError();
|
||||||
|
},
|
||||||
|
|
||||||
|
formatError: function() {
|
||||||
|
this.set('result', this.get('err').toResult());
|
||||||
|
},
|
||||||
|
|
||||||
|
getShortcutMap: function() {
|
||||||
|
return {
|
||||||
|
'git commit': /^gc($|\s)/,
|
||||||
|
'git add': /^ga($|\s)/,
|
||||||
|
'git checkout': /^gchk($|\s)/,
|
||||||
|
'git rebase': /^gr($|\s)/,
|
||||||
|
'git branch': /^gb($|\s)/
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getRegexMap: function() {
|
||||||
|
return {
|
||||||
|
// ($|\s) means that we either have to end the string
|
||||||
|
// after the command or there needs to be a space for options
|
||||||
|
commit: /^commit($|\s)/,
|
||||||
|
add: /^add($|\s)/,
|
||||||
|
checkout: /^checkout($|\s)/,
|
||||||
|
rebase: /^rebase($|\s)/,
|
||||||
|
reset: /^reset($|\s)/,
|
||||||
|
branch: /^branch($|\s)/,
|
||||||
|
revert: /^revert($|\s)/,
|
||||||
|
merge: /^merge($|\s)/
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getSandboxCommands: function() {
|
||||||
|
return [
|
||||||
|
[/^ls/, function() {
|
||||||
|
throw new CommandResult({
|
||||||
|
msg: "DontWorryAboutFilesInThisDemo.txt"
|
||||||
|
});
|
||||||
|
}],
|
||||||
|
[/^cd/, function() {
|
||||||
|
throw new CommandResult({
|
||||||
|
msg: "Directory Changed to '/directories/dont/matter/in/this/demo'"
|
||||||
|
});
|
||||||
|
}],
|
||||||
|
[/^git$/, function() {
|
||||||
|
// TODO better git description. also help, hint, etc
|
||||||
|
throw new CommandResult({
|
||||||
|
msg: _.escape("\
|
||||||
|
Git Version \n \
|
||||||
|
PCOTTLE.1.0 \
|
||||||
|
Usage: \n \
|
||||||
|
git <command> [<args>] \
|
||||||
|
")
|
||||||
|
});
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
parse: function() {
|
||||||
|
var str = this.get('rawStr');
|
||||||
|
// first if the string is empty, they just want a blank line
|
||||||
|
if (!str.length) {
|
||||||
|
throw new CommandResult({msg: ""});
|
||||||
|
}
|
||||||
|
|
||||||
|
// then check if it's one of our sandbox commands
|
||||||
|
_.each(this.getSandboxCommands(), function(tuple) {
|
||||||
|
var regex = tuple[0];
|
||||||
|
if (regex.exec(str)) {
|
||||||
|
tuple[1]();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// then check if shortcut exists, and replace, but
|
||||||
|
// preserve options if so
|
||||||
|
_.each(this.getShortcutMap(), function(regex, method) {
|
||||||
|
var results = regex.exec(str);
|
||||||
|
if (results) {
|
||||||
|
str = method + ' ' + str.slice(results[0].length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// see if begins with git
|
||||||
|
if (str.slice(0,3) !== 'git') {
|
||||||
|
throw new CommandProcessError({
|
||||||
|
msg: 'Git commands only, sorry!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ok, we have a (probably) valid command. actually parse it
|
||||||
|
this.gitParse(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
gitParse: function(str) {
|
||||||
|
// now slice off command part
|
||||||
|
var fullCommand = str.slice('git '.length);
|
||||||
|
|
||||||
|
// see if we support this particular command
|
||||||
|
_.each(this.getRegexMap(), function(regex, method) {
|
||||||
|
if (regex.exec(fullCommand)) {
|
||||||
|
this.set('options', fullCommand.slice(method.length + 1));
|
||||||
|
this.set('method', method);
|
||||||
|
// we should stop iterating, but the regex will only match
|
||||||
|
// one command in practice. we could stop iterating if we used
|
||||||
|
// jqeurys for each but im using underscore (for no real reason other
|
||||||
|
// than style)
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
if (!this.get('method')) {
|
||||||
|
throw new CommandProcessError({
|
||||||
|
msg: "Sorry, this demo does not support that git command: " + fullCommand
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse off the options and assemble the map / general args
|
||||||
|
var optionParser = new OptionParser(this.get('method'), this.get('options'));
|
||||||
|
|
||||||
|
// steal these away so we can be completely JSON
|
||||||
|
this.set('generalArgs', optionParser.generalArgs);
|
||||||
|
this.set('supportedMap', optionParser.supportedMap);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OptionParser
|
||||||
|
*/
|
||||||
|
function OptionParser(method, options) {
|
||||||
|
this.method = method;
|
||||||
|
this.rawOptions = options;
|
||||||
|
|
||||||
|
this.supportedMap = this.getMasterOptionMap()[method];
|
||||||
|
if (this.supportedMap === undefined) {
|
||||||
|
throw new Error('No option map for ' + method);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generalArgs = [];
|
||||||
|
this.explodeAndSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionParser.prototype.getMasterOptionMap = function() {
|
||||||
|
// here a value of false means that we support it, even if its just a
|
||||||
|
// pass-through option. If the value is not here (aka will be undefined
|
||||||
|
// when accessed), we do not support it.
|
||||||
|
return {
|
||||||
|
commit: {
|
||||||
|
'--amend': false,
|
||||||
|
'-a': false, // warning
|
||||||
|
'-am': false
|
||||||
|
},
|
||||||
|
add: {},
|
||||||
|
branch: {
|
||||||
|
'-d': false,
|
||||||
|
'-D': false
|
||||||
|
},
|
||||||
|
checkout: {
|
||||||
|
'-b': false
|
||||||
|
},
|
||||||
|
reset: {
|
||||||
|
'--hard': false,
|
||||||
|
'--soft': false, // this will raise an error but we catch it in gitEngine
|
||||||
|
},
|
||||||
|
merge: {},
|
||||||
|
rebase: {},
|
||||||
|
revert: {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
OptionParser.prototype.explodeAndSet = function() {
|
||||||
|
// split on spaces, except when inside quotes, and strip quotes after.
|
||||||
|
// for some reason the regex includes the quotes even if i move the parantheses
|
||||||
|
// inside
|
||||||
|
var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/g) || [];
|
||||||
|
_.each(exploded, function(part, i) {
|
||||||
|
exploded[i] = part.replace(/['"]/g, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var i = 0; i < exploded.length; i++) {
|
||||||
|
var part = exploded[i];
|
||||||
|
if (part.slice(0,1) == '-') {
|
||||||
|
// it's an option, check supportedMap
|
||||||
|
if (this.supportedMap[part] === undefined) {
|
||||||
|
throw new CommandProcessError({
|
||||||
|
msg: 'The option "' + part + '" is not supported'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through and include all the next args until we hit another option or the end
|
||||||
|
var optionArgs = [];
|
||||||
|
var next = i + 1;
|
||||||
|
while (next < exploded.length && exploded[next].slice(0,1) != '-') {
|
||||||
|
optionArgs.push(exploded[next]);
|
||||||
|
next += 1;
|
||||||
|
}
|
||||||
|
i = next - 1;
|
||||||
|
|
||||||
|
// **phew** we are done grabbing those. theseArgs is truthy even with an empty array
|
||||||
|
this.supportedMap[part] = optionArgs;
|
||||||
|
} else {
|
||||||
|
// must be a general arg
|
||||||
|
this.generalArgs.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// done!
|
||||||
|
};
|
||||||
|
|
187
src/commandViews.js
Normal file
187
src/commandViews.js
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
var CommandPromptView = Backbone.View.extend({
|
||||||
|
initialize: function(options) {
|
||||||
|
this.collection = options.collection;
|
||||||
|
this.commands = [];
|
||||||
|
this.index = -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'keyup #commandTextField': 'keyUp'
|
||||||
|
},
|
||||||
|
|
||||||
|
keyUp: function(e) {
|
||||||
|
// we need to capture some of these events.
|
||||||
|
// WARNING: this key map is not internationalized :(
|
||||||
|
var keyMap = {
|
||||||
|
// enter
|
||||||
|
13: _.bind(function() {
|
||||||
|
this.submit();
|
||||||
|
}, this),
|
||||||
|
// up
|
||||||
|
38: _.bind(function() {
|
||||||
|
this.commandSelectChange(1);
|
||||||
|
}, this),
|
||||||
|
// down
|
||||||
|
40: _.bind(function() {
|
||||||
|
this.commandSelectChange(-1);
|
||||||
|
}, this)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (keyMap[e.which] !== undefined) {
|
||||||
|
e.preventDefault();
|
||||||
|
keyMap[e.which]();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
commandSelectChange: function(delta) {
|
||||||
|
this.index += delta;
|
||||||
|
|
||||||
|
// if we are over / under, display blank line. yes this eliminates your
|
||||||
|
// partially written command, but i doubt that is much in this demo
|
||||||
|
if (this.index >= this.commands.length || this.index < 0) {
|
||||||
|
this.clear();
|
||||||
|
this.index = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// yay! we actually can display something
|
||||||
|
this.setTextField(this.commands[this.index]);
|
||||||
|
},
|
||||||
|
|
||||||
|
setTextField: function(value) {
|
||||||
|
this.$('#commandTextField').val(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function() {
|
||||||
|
this.setTextField('');
|
||||||
|
},
|
||||||
|
|
||||||
|
submit: function() {
|
||||||
|
var value = this.$('#commandTextField').val().replace('\n', '');
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
// if we are entering a real command, add it to our history
|
||||||
|
if (value.length) {
|
||||||
|
this.commands.unshift(value);
|
||||||
|
}
|
||||||
|
this.index = -1;
|
||||||
|
|
||||||
|
// split commands on semicolon
|
||||||
|
_.each(value.split(';'), _.bind(function(command) {
|
||||||
|
command = command.replace(/^(\s+)/, '');
|
||||||
|
command = command.replace(/(\s+)$/, '');
|
||||||
|
if (command.length) {
|
||||||
|
this.addToCollection(command);
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
},
|
||||||
|
|
||||||
|
addToCollection: function(value) {
|
||||||
|
var command = new Command({
|
||||||
|
rawStr: value
|
||||||
|
});
|
||||||
|
this.collection.add(command);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// This is the view for all commands -- it will represent
|
||||||
|
// their status (inqueue, processing, finished, error),
|
||||||
|
// their value ("git commit --amend"),
|
||||||
|
// and the result (either errors or warnings or whatever)
|
||||||
|
var CommandView = Backbone.View.extend({
|
||||||
|
tagName: 'div',
|
||||||
|
model: Command,
|
||||||
|
template: _.template($('#command-template').html()),
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'click': 'clicked'
|
||||||
|
},
|
||||||
|
|
||||||
|
clicked: function(e) {
|
||||||
|
console.log('was clicked');
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function() {
|
||||||
|
this.model.bind('change', this.wasChanged, this);
|
||||||
|
this.model.bind('destroy', this.remove, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
wasChanged: function(model, changeEvent) {
|
||||||
|
console.log('command changed', model, changeEvent);
|
||||||
|
// for changes that are just comestic, we actually only want to toggle classes
|
||||||
|
// with jquery rather than brutally delete a html of HTML
|
||||||
|
var changes = changeEvent.changes;
|
||||||
|
var changeKeys = _.keys(changes);
|
||||||
|
if (_.difference(changeKeys, ['status']) == 0) {
|
||||||
|
this.updateStatus();
|
||||||
|
} else if (_.difference(changeKeys, ['error']) == 0) {
|
||||||
|
// the above will
|
||||||
|
this.render();
|
||||||
|
} else {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStatus: function() {
|
||||||
|
var statuses = ['inqueue', 'processing', 'finished'];
|
||||||
|
var toggleMap = {};
|
||||||
|
_.each(statuses, function(status) {
|
||||||
|
toggleMap[status] = false;
|
||||||
|
});
|
||||||
|
toggleMap[this.model.get('status')] = true;
|
||||||
|
|
||||||
|
var query = this.$('p.commandLine');
|
||||||
|
|
||||||
|
_.each(toggleMap, function(value, key) {
|
||||||
|
query.toggleClass(key, value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var json = _.extend(
|
||||||
|
{
|
||||||
|
resultType: '',
|
||||||
|
result: ''
|
||||||
|
},
|
||||||
|
this.model.toJSON()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$el.html(this.template(json));
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: function() {
|
||||||
|
$(this.el).hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var CommandLineHistoryView = Backbone.View.extend({
|
||||||
|
initialize: function(options) {
|
||||||
|
this.collection = options.collection;
|
||||||
|
|
||||||
|
this.collection.on('add', this.addOne, this);
|
||||||
|
this.collection.on('reset', this.addAll, this);
|
||||||
|
this.collection.on('all', this.render, this);
|
||||||
|
|
||||||
|
this.collection.on('change', this.scrollDown, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollDown: function() {
|
||||||
|
var el = $('#commandLineHistory')[0];
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
|
addOne: function(command) {
|
||||||
|
var view = new CommandView({
|
||||||
|
model: command
|
||||||
|
});
|
||||||
|
this.$('#commandDisplay').append(view.render().el);
|
||||||
|
this.scrollDown();
|
||||||
|
},
|
||||||
|
|
||||||
|
addAll: function() {
|
||||||
|
this.collection.each(this.addOne);
|
||||||
|
}
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue