├── .gitignore ├── LICENSE ├── README.md ├── assets └── learnGitBranching.png ├── build ├── bundle.js ├── bundle.min.662ccc1a.js ├── bundle.min.js └── main.ddc18834.css ├── grunt.js ├── index.html ├── lib ├── backbone-min.js ├── backbone.localStorage-min.js ├── jquery-1.8.0.min.js ├── jquery-ui-1.9.0.custom.min.js ├── raphael-min.js └── underscore-min.js ├── package.json ├── spec ├── first.spec.js └── git.spec.js ├── src ├── js │ ├── app │ │ └── index.js │ ├── dialogs │ │ ├── levelBuilder.js │ │ └── sandbox.js │ ├── git │ │ ├── commands.js │ │ ├── gitShim.js │ │ ├── headless.js │ │ ├── index.js │ │ └── treeCompare.js │ ├── level │ │ ├── arbiter.js │ │ ├── builder.js │ │ ├── disabledMap.js │ │ ├── index.js │ │ ├── parseWaterfall.js │ │ ├── sandbox.js │ │ └── sandboxCommands.js │ ├── models │ │ ├── collections.js │ │ └── commandModel.js │ ├── util │ │ ├── constants.js │ │ ├── debug.js │ │ ├── errors.js │ │ ├── eventBaton.js │ │ ├── index.js │ │ ├── keyboard.js │ │ ├── mock.js │ │ └── zoomLevel.js │ ├── views │ │ ├── builderViews.js │ │ ├── commandViews.js │ │ ├── gitDemonstrationView.js │ │ ├── index.js │ │ ├── levelDropdownView.js │ │ ├── multiView.js │ │ └── rebaseView.js │ └── visuals │ │ ├── animation │ │ ├── animationFactory.js │ │ └── index.js │ │ ├── index.js │ │ ├── tree.js │ │ ├── visBase.js │ │ ├── visBranch.js │ │ ├── visEdge.js │ │ ├── visNode.js │ │ └── visualization.js ├── levels │ ├── index.js │ ├── intro │ │ ├── 1.js │ │ ├── 2.js │ │ ├── 3.js │ │ ├── 4.js │ │ └── 5.js │ ├── mixed │ │ ├── 1.js │ │ ├── 2.js │ │ └── 3.js │ └── rebase │ │ ├── 1.js │ │ └── 2.js ├── style │ ├── font-awesome.css │ ├── font │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ └── main.css └── template.index.html └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | bundle 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012 Peter Cottle 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LearnGitBranching 2 | 3 | LearnGitBranching is a pseudo-git sandbox and interactive series of tutorials / challenges to accelerate the understanding of how git commit trees work. The ideal audience is a complete newcomer to git, but a wide range of experience levels should be able to benefit from these tutorials. 4 | 5 | It supports a fairly wide range of commands and dynamically visualizes the effects each change has on a commit tree visualization next to the command box: 6 | 7 | 8 | 9 | You can see the demo here: 10 | http://pcottle.github.com/learnGitBranching/?demo 11 | 12 | or use the vanilla app here: 13 | http://pcottle.github.com/learnGitBranching/ 14 | 15 | ### Sandbox Mode 16 | 17 | Sandbox mode is where you can mess around and just see what certain git commands do. It is moderately helpful, but the real magic lies in levels... 18 | 19 | ## Levels 20 | 21 | Type `levels` to see the available levels. These are a mix of tutorials and challenges to introduce git concepts and get newcomers familiar with certain workflows. There is also a "git golf" concept that tracks how many commands you used to solve the level :P 22 | 23 | ### Level Builder 24 | 25 | You can build levels with `build level`. The dialog should walk you through the majority of the commands -- at the end you will get a JSON blob that you can share with friends or paste into a Github issue. 26 | 27 | ### Contributing Levels 28 | 29 | I would love for more levels to be added! I think there is a ton to learn and cover. Hopefully the community together can build a great tool for all git newcomers. 30 | 31 | ## Contributing 32 | 33 | I am really loose about contributing levels. For contributing functionality, you will need to install the `grunt` build tool. The general steps: 34 | 35 | ``` 36 | git clone 37 | cd learnGitBranching 38 | npm install 39 | git checkout -b newAwesomeFeature 40 | # some changes 41 | grunt build 42 | git commit -am "My new sweet feature!" 43 | git push 44 | # go online and request a pull 45 | ``` 46 | 47 | ## Helpful Folks 48 | A big shoutout to these brave souls for extensively testing our sandbox and finding bugs and/or inconsistencies: 49 | 50 | * Nikita Kouevda 51 | * Maksim Ioffe 52 | * Dan Miller 53 | 54 | Also huge shoutout for everyone who has put up a pull request that was pulled: 55 | 56 | * Stephen Cavaliere 57 | * Andrew Ardill 58 | * Shao-Chung Chen 59 | * Tobias Pfeiffer 60 | * Luke Kysow - 2 61 | * Adam Brodzinski 62 | * Hamish Macpherson 63 | * Cameron Wills 64 | * Johan ("josso") 65 | 66 | Or reported an issue that was successfully closed! 67 | 68 | * Caspar Krieger 69 | * Stuart Knightley 70 | * John Gietzen 71 | * Chris Greene 72 | * "datton" 73 | * Jaymes Bearden 74 | * Jan-Erik Rediger 75 | * Scott Bigelow 76 | * "ortin" 77 | * Dave Myron 78 | * "chosenken" 79 | 80 | -------------------------------------------------------------------------------- /assets/learnGitBranching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urigit/learnGitBranching/659e23fec29a84e043e3cfc9ccdfe5c2b879ddab/assets/learnGitBranching.png -------------------------------------------------------------------------------- /grunt.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var fs = require('fs'); 3 | 4 | // Haha, this is so tricky. so we have a template for index.html to stick 5 | // in the hashed JS and style files -- that template also contains 6 | // templates used in the app. in order to avoid evaluating those 7 | // templates, we change the regexes so we can effectively nest templates 8 | _.templateSettings.interpolate = /\{\{(.+?)\}\}/g; 9 | _.templateSettings.escape = /\{\{\{(.*?)\}\}\}/g; 10 | _.templateSettings.evaluate = /\{\{-(.*?)\}\}/g; 11 | 12 | // precompile for speed 13 | var indexFile = fs.readFileSync('src/template.index.html').toString(); 14 | var indexTemplate = _.template(indexFile); 15 | 16 | /*global module:false*/ 17 | module.exports = function(grunt) { 18 | // eventually have sound...? 19 | grunt.registerTask('compliment', 'Stay motivated!', function() { 20 | var defaults = ['Awesome!!']; 21 | 22 | var compliments = grunt.config('compliment.compliments') || defaults; 23 | var index = Math.floor(Math.random() * compliments.length); 24 | 25 | grunt.log.writeln(compliments[index]); 26 | }); 27 | 28 | grunt.registerTask('buildIndex', 'stick in hashed resources', function() { 29 | grunt.log.writeln('Building index...'); 30 | 31 | // first find the one in here that we want 32 | var buildFiles = fs.readdirSync('build'); 33 | 34 | var hashedMinFile; 35 | if (buildFiles.length == 2) { 36 | grunt.log.writeln('Assuming debug mode wanted'); 37 | hashedMinFile = 'bundle.js'; 38 | } 39 | var jsRegex = /bundle\.min\.\w+\.js/; 40 | _.each(buildFiles, function(jsFile) { 41 | if (jsRegex.test(jsFile)) { 42 | if (hashedMinFile) { 43 | throw new Error('more than one hashed file: ' + jsFile + hashedMinFile); 44 | } 45 | hashedMinFile = jsFile; 46 | } 47 | }); 48 | if (!hashedMinFile) { throw new Error('no hashed min file found!'); } 49 | 50 | grunt.log.writeln('Found hashed js file: ' + hashedMinFile); 51 | 52 | var styleRegex = /main\.\w+\.css/; 53 | var hashedStyleFile; 54 | _.each(buildFiles, function(styleFile) { 55 | if (styleRegex.test(styleFile)) { 56 | if (hashedStyleFile) { 57 | throw new Error('more than one hashed style: ' + styleFile + hashedStyleFile); 58 | } 59 | hashedStyleFile = styleFile; 60 | } 61 | }); 62 | if (!hashedStyleFile) { throw new Error('no style found'); } 63 | 64 | grunt.log.writeln('Found hashed style file: ' + hashedStyleFile); 65 | 66 | // output these filenames to our index template 67 | var outputIndex = indexTemplate({ 68 | jsFile: hashedMinFile, 69 | styleFile: hashedStyleFile 70 | }); 71 | fs.writeFileSync('index.html', outputIndex); 72 | }); 73 | 74 | grunt.initConfig({ 75 | lint: { 76 | files: ['grunt.js', 'src/**/*.js', 'spec/*.js'] 77 | }, 78 | compliment: { 79 | compliments: [ 80 | "Wow peter great work!", 81 | "Such a professional dev environment", 82 | "Can't stop the TRAIN", 83 | "git raging" 84 | ] 85 | }, 86 | hash: { 87 | src: ['build/bundle.min.js', 'src/style/main.css'], 88 | dest: 'build/' 89 | }, 90 | jasmine_node: { 91 | specNameMatcher: 'spec', // load only specs containing specNameMatcher 92 | projectRoot: '.', 93 | forceExit: true, 94 | verbose: true 95 | }, 96 | watch: { 97 | files: '', 98 | tasks: 'watching' 99 | }, 100 | min: { 101 | dist: { 102 | src: ['build/bundle.js'], 103 | dest: 'build/bundle.min.js' 104 | } 105 | }, 106 | rm: { 107 | build: 'build/*' 108 | }, 109 | // shell: { 110 | // gitAdd: { 111 | // command: {command: 'git add build/', options: ''} 112 | // } 113 | // }, 114 | jshint: { 115 | options: { 116 | curly: true, 117 | // sometimes triple equality is just redundant and unnecessary 118 | eqeqeq: false, 119 | // i know my regular expressions 120 | regexp: false, 121 | // i think it's super weird to not use new on a constructor 122 | nonew: false, 123 | // these latedefs are just annoying -- no pollution of global scope 124 | latedef: false, 125 | // use this in mocks 126 | forin: false, 127 | /////////////////////////////// 128 | // All others are true 129 | ////////////////////////////// 130 | immed: true, 131 | newcap: true, 132 | noarg: true, 133 | bitwise: true, 134 | sub: true, 135 | undef: true, 136 | unused: false, 137 | trailing: true, 138 | devel: true, 139 | jquery: true, 140 | nonstandard: true, 141 | boss: true, 142 | eqnull: true, 143 | browser: true, 144 | debug: true 145 | }, 146 | globals: { 147 | Raphael: true, 148 | require: true, 149 | console: true, 150 | describe: true, 151 | expect: true, 152 | it: true, 153 | exports: true, 154 | process: true 155 | } 156 | }, 157 | browserify: { 158 | 'build/bundle.js': { 159 | entries: ['src/**/*.js', 'src/js/**/*.js'] 160 | //prepend: [''], 161 | } 162 | } 163 | }); 164 | 165 | // all my npm helpers 166 | grunt.loadNpmTasks('grunt-jslint'); 167 | grunt.loadNpmTasks('grunt-browserify'); 168 | grunt.loadNpmTasks('grunt-jasmine-node'); 169 | grunt.loadNpmTasks('grunt-hash'); 170 | grunt.loadNpmTasks('grunt-rm'); 171 | grunt.loadNpmTasks('grunt-shell'); 172 | 173 | grunt.registerTask('build', 'rm browserify min hash buildIndex'); 174 | grunt.registerTask('fastBuild', 'rm browserify hash buildIndex'); 175 | 176 | grunt.registerTask('default', 'lint jasmine_node build compliment'); 177 | 178 | grunt.registerTask('watching', 'fastBuild lint'); 179 | grunt.registerTask('test', 'jasmine_node'); 180 | }; 181 | 182 | -------------------------------------------------------------------------------- /lib/backbone.localStorage-min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Backbone localStorage Adapter 3 | * https://github.com/jeromegn/Backbone.localStorage 4 | */(function(){function c(){return((1+Math.random())*65536|0).toString(16).substring(1)}function d(){return c()+c()+"-"+c()+"-"+c()+"-"+c()+"-"+c()+c()+c()}var a=this._,b=this.Backbone;b.LocalStorage=window.Store=function(a){this.name=a;var b=this.localStorage().getItem(this.name);this.records=b&&b.split(",")||[]},a.extend(b.LocalStorage.prototype,{save:function(){this.localStorage().setItem(this.name,this.records.join(","))},create:function(a){return a.id||(a.id=d(),a.set(a.idAttribute,a.id)),this.localStorage().setItem(this.name+"-"+a.id,JSON.stringify(a)),this.records.push(a.id.toString()),this.save(),a.toJSON()},update:function(b){return this.localStorage().setItem(this.name+"-"+b.id,JSON.stringify(b)),a.include(this.records,b.id.toString())||this.records.push(b.id.toString()),this.save(),b.toJSON()},find:function(a){return JSON.parse(this.localStorage().getItem(this.name+"-"+a.id))},findAll:function(){return a(this.records).chain().map(function(a){return JSON.parse(this.localStorage().getItem(this.name+"-"+a))},this).compact().value()},destroy:function(b){return this.localStorage().removeItem(this.name+"-"+b.id),this.records=a.reject(this.records,function(a){return a==b.id.toString()}),this.save(),b},localStorage:function(){return localStorage}}),b.LocalStorage.sync=window.Store.sync=b.localSync=function(a,b,c,d){var e=b.localStorage||b.collection.localStorage;typeof c=="function"&&(c={success:c,error:d});var f,g=$.Deferred&&$.Deferred();switch(a){case"read":f=b.id!=undefined?e.find(b):e.findAll();break;case"create":f=e.create(b);break;case"update":f=e.update(b);break;case"delete":f=e.destroy(b)}return f?(c.success(f),g&&g.resolve()):(c.error("Record not found"),g&&g.reject()),g&&g.promise()},b.ajaxSync=b.sync,b.getSyncMethod=function(a){return a.localStorage||a.collection&&a.collection.localStorage?b.localSync:b.ajaxSync},b.sync=function(a,c,d,e){return b.getSyncMethod(c).apply(this,[a,c,d,e])}})(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LearnGitBranching", 3 | "version": "0.5.0", 4 | "devDependencies": { 5 | "grunt": "~0.3.17", 6 | "jasmine-node": "~1.0.28", 7 | "grunt-browserify": "~0.1.2", 8 | "grunt-jslint": "~0.2.2-1", 9 | "grunt-jasmine-node": "latest", 10 | "grunt-hash": "latest", 11 | "grunt-rm": "~0.0.3", 12 | "grunt-shell": "latest" 13 | }, 14 | "dependencies": { 15 | "backbone": "~0.9.9", 16 | "underscore": "~1.4.3", 17 | "jquery": "~1.8.3", 18 | "q": "~0.8.11", 19 | "markdown": "~0.4.0", 20 | "grunt-shell": "~0.1.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/first.spec.js: -------------------------------------------------------------------------------- 1 | describe('Tests', function() { 2 | it('should work', function() { 3 | expect(1).toBeTruthy(); 4 | }); 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /spec/git.spec.js: -------------------------------------------------------------------------------- 1 | var HeadlessGit = require('../src/js/git/headless').HeadlessGit; 2 | var TreeCompare = require('../src/js/git/treeCompare').TreeCompare; 3 | var treeCompare = new TreeCompare(); 4 | 5 | var loadTree = function(json) { 6 | return JSON.parse(unescape(json)); 7 | }; 8 | 9 | var compareAnswer = function(headless, expectedJSON) { 10 | var expectedTree = loadTree(expectedJSON); 11 | var actualTree = headless.gitEngine.exportTree(); 12 | 13 | var equal = treeCompare.compareTrees(expectedTree, actualTree); 14 | expect(equal).toBe(true); 15 | }; 16 | 17 | var expectTree = function(command, expectedJSON) { 18 | var headless = new HeadlessGit(); 19 | headless.sendCommand(command); 20 | compareAnswer(headless, expectedJSON); 21 | }; 22 | 23 | describe('GitEngine', function() { 24 | it('Commits', function() { 25 | expectTree( 26 | 'git commit', 27 | '{"branches":{"master":{"target":"C2","id":"master","type":"branch"}},"commits":{"C0":{"type":"commit","parents":[],"id":"C0","rootCommit":true},"C1":{"type":"commit","parents":["C0"],"id":"C1"},"C2":{"type":"commit","parents":["C1"],"id":"C2"}},"HEAD":{"id":"HEAD","target":"master","type":"general ref"}}' 28 | ); 29 | }); 30 | 31 | it('Checkouts', function() { 32 | expectTree( 33 | 'git checkout -b side', 34 | '{"branches":{"master":{"target":"C1","id":"master"},"side":{"target":"C1","id":"side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"side","id":"HEAD"}}' 35 | ); 36 | }); 37 | 38 | it('Rebases', function() { 39 | expectTree( 40 | 'gc; git checkout -b side C1; gc; git rebase master', 41 | '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22master%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C3%27%22%2C%22id%22%3A%22side%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22side%22%2C%22id%22%3A%22HEAD%22%7D%7D' 42 | ); 43 | }); 44 | 45 | it('Reverts', function() { 46 | expectTree( 47 | 'git revert HEAD', 48 | '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%27%22%2C%22id%22%3A%22master%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C1%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C1%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D' 49 | ); 50 | }); 51 | 52 | it('Merges', function() { 53 | expectTree( 54 | 'gc; git checkout -b side C1; gc; git merge master', 55 | '{"branches":{"master":{"target":"C2","id":"master"},"side":{"target":"C4","id":"side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"},"C4":{"parents":["C2","C3"],"id":"C4"}},"HEAD":{"target":"side","id":"HEAD"}}' 56 | ); 57 | }); 58 | 59 | it('Resets', function() { 60 | expectTree( 61 | 'git commit; git reset HEAD~1', 62 | '{"branches":{"master":{"target":"C1","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}' 63 | ); 64 | }); 65 | 66 | it('Branches', function() { 67 | expectTree( 68 | 'git branch side C0', 69 | '{"branches":{"master":{"target":"C1","id":"master"},"side":{"target":"C0","id":"side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"master","id":"HEAD"}}' 70 | ); 71 | }); 72 | 73 | it('Deletes branches', function() { 74 | expectTree( 75 | 'git branch side; git branch -d side', 76 | '{"branches":{"master":{"target":"C1","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"master","id":"HEAD"}}' 77 | ); 78 | }); 79 | 80 | it('Ammends commits', function() { 81 | expectTree( 82 | 'git commit --amend', 83 | '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%27%22%2C%22id%22%3A%22master%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C1%27%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D' 84 | ); 85 | }); 86 | 87 | it('Cherry picks', function() { 88 | expectTree( 89 | 'git checkout -b side C0; gc; git cherry-pick C1', 90 | '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C1%27%22%2C%22id%22%3A%22side%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C1%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C1%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22side%22%2C%22id%22%3A%22HEAD%22%7D%7D' 91 | ); 92 | }); 93 | 94 | it('Forces branches', function() { 95 | expectTree( 96 | 'git checkout -b side; git branch -f side C0', 97 | '{"branches":{"master":{"target":"C1","id":"master"},"side":{"target":"C0","id":"side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"side","id":"HEAD"}}' 98 | ); 99 | }); 100 | 101 | it('Rebases only new commits to destination', function() { 102 | expectTree( 103 | 'git checkout -b side C0; gc; gc;git cherry-pick C1;git rebase master', 104 | '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C3%27%22%2C%22id%22%3A%22side%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C1%27%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C1%27%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%27%22%5D%2C%22id%22%3A%22C3%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22side%22%2C%22id%22%3A%22HEAD%22%7D%7D' 105 | ); 106 | }); 107 | 108 | }); 109 | 110 | -------------------------------------------------------------------------------- /src/js/app/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | 4 | var Constants = require('../util/constants'); 5 | var util = require('../util'); 6 | 7 | /** 8 | * Globals 9 | */ 10 | var events = _.clone(Backbone.Events); 11 | var commandUI; 12 | var sandbox; 13 | var eventBaton; 14 | var levelArbiter; 15 | var levelDropdown; 16 | 17 | /////////////////////////////////////////////////////////////////////// 18 | 19 | var init = function() { 20 | /** 21 | * There is a decent amount of bootstrapping we need just to hook 22 | * everything up. The init() method takes on these responsibilities, 23 | * including but not limited to: 24 | * - setting up Events and EventBaton 25 | * - calling the constructor for the main visualization 26 | * - initializing the command input bar 27 | * - handling window.focus and zoom events 28 | **/ 29 | var Sandbox = require('../level/sandbox').Sandbox; 30 | var Level = require('../level').Level; 31 | var EventBaton = require('../util/eventBaton').EventBaton; 32 | var LevelArbiter = require('../level/arbiter').LevelArbiter; 33 | var LevelDropdownView = require('../views/levelDropdownView').LevelDropdownView; 34 | 35 | eventBaton = new EventBaton(); 36 | commandUI = new CommandUI(); 37 | sandbox = new Sandbox(); 38 | levelArbiter = new LevelArbiter(); 39 | levelDropdown = new LevelDropdownView({ 40 | wait: true 41 | }); 42 | 43 | // we always want to focus the text area to collect input 44 | var focusTextArea = function() { 45 | $('#commandTextField').focus(); 46 | }; 47 | focusTextArea(); 48 | 49 | $(window).focus(function(e) { 50 | eventBaton.trigger('windowFocus', e); 51 | }); 52 | $(document).click(function(e) { 53 | eventBaton.trigger('documentClick', e); 54 | }); 55 | $(document).bind('keydown', function(e) { 56 | eventBaton.trigger('docKeydown', e); 57 | }); 58 | $(document).bind('keyup', function(e) { 59 | eventBaton.trigger('docKeyup', e); 60 | }); 61 | 62 | $(window).on('resize', function(e) { 63 | events.trigger('resize', e); 64 | }); 65 | 66 | /* 67 | $(window).on('resize', _.throttle(function(e) { 68 | var width = $(window).width(); 69 | var height = $(window).height(); 70 | eventBaton.trigger('windowSizeCheck', {w: width, h: height}); 71 | }, 500)); 72 | */ 73 | 74 | eventBaton.stealBaton('docKeydown', function() { }); 75 | eventBaton.stealBaton('docKeyup', function() { }); 76 | 77 | /** 78 | * I am disabling this for now, it works on desktop but is 79 | hacky on iOS mobile and god knows the behavior on android... 80 | // zoom level measure, I wish there was a jquery event for this :/ 81 | require('../util/zoomLevel').setupZoomPoll(function(level) { 82 | eventBaton.trigger('zoomChange', level); 83 | }, this); 84 | 85 | eventBaton.stealBaton('zoomChange', function(level) { 86 | if (level > Constants.VIEWPORT.maxZoom || 87 | level < Constants.VIEWPORT.minZoom) { 88 | var Views = require('../views'); 89 | var view = new Views.ZoomAlertWindow({level: level}); 90 | } 91 | }); 92 | */ 93 | 94 | /* people were pissed about this apparently 95 | eventBaton.stealBaton('windowSizeCheck', function(size) { 96 | if (size.w < Constants.VIEWPORT.minWidth || 97 | size.h < Constants.VIEWPORT.minHeight) { 98 | var Views = require('../views'); 99 | var view = new Views.WindowSizeAlertWindow(); 100 | } 101 | });*/ 102 | 103 | // the default action on window focus and document click is to just focus the text area 104 | eventBaton.stealBaton('windowFocus', focusTextArea); 105 | eventBaton.stealBaton('documentClick', focusTextArea); 106 | 107 | // but when the input is fired in the text area, we pipe that to whoever is 108 | // listenining 109 | var makeKeyListener = function(name) { 110 | return function() { 111 | var args = [name]; 112 | _.each(arguments, function(arg) { 113 | args.push(arg); 114 | }); 115 | eventBaton.trigger.apply(eventBaton, args); 116 | }; 117 | }; 118 | 119 | $('#commandTextField').on('keydown', makeKeyListener('keydown')); 120 | $('#commandTextField').on('keyup', makeKeyListener('keyup')); 121 | $(window).trigger('resize'); 122 | 123 | // demo functionality 124 | if (/\?demo/.test(window.location.href)) { 125 | sandbox.mainVis.customEvents.on('gitEngineReady', function() { 126 | eventBaton.trigger( 127 | 'commandSubmitted', 128 | [ 129 | "git commit; git checkout -b bugFix C1; git commit; git merge master; git checkout master; git commit; git rebase bugFix;", 130 | "delay 1000; reset;", 131 | "level rebase1 --noFinishDialog --noStartCommand --noIntroDialog;", 132 | "delay 2000; show goal; delay 1000; hide goal;", 133 | "git checkout bugFix; git rebase master; git checkout side; git rebase bugFix;", 134 | "git checkout another; git rebase side; git rebase another master;", 135 | "help; levels" 136 | ].join('')); 137 | }); 138 | } else if (!(/\?NODEMO/.test(window.location.href))) { 139 | sandbox.mainVis.customEvents.on('gitEngineReady', function() { 140 | eventBaton.trigger( 141 | 'commandSubmitted', 142 | [ 143 | "git help;", 144 | "delay 1000;", 145 | "help;", 146 | "levels" 147 | ].join('')); 148 | }); 149 | } 150 | if (/command=/.test(window.location.href)) { 151 | var commandRaw = window.location.href.split('command=')[1].split('&')[0]; 152 | var command = unescape(commandRaw); 153 | sandbox.mainVis.customEvents.on('gitEngineReady', function() { 154 | eventBaton.trigger('commandSubmitted', command); 155 | }); 156 | } 157 | 158 | if (/(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent) || /android/i.test(navigator.userAgent)) { 159 | sandbox.mainVis.customEvents.on('gitEngineReady', function() { 160 | eventBaton.trigger('commandSubmitted', 'mobile alert'); 161 | }); 162 | } 163 | }; 164 | 165 | if (require('../util').isBrowser()) { 166 | // this file gets included via node sometimes as well 167 | $(document).ready(init); 168 | } 169 | 170 | /** 171 | * the UI method simply bootstraps the command buffer and 172 | * command prompt views. It only interacts with user input 173 | * and simply pipes commands to the main events system 174 | **/ 175 | function CommandUI() { 176 | var Collections = require('../models/collections'); 177 | var CommandViews = require('../views/commandViews'); 178 | 179 | this.commandCollection = new Collections.CommandCollection(); 180 | this.commandBuffer = new Collections.CommandBuffer({ 181 | collection: this.commandCollection 182 | }); 183 | 184 | this.commandPromptView = new CommandViews.CommandPromptView({ 185 | el: $('#commandLineBar') 186 | }); 187 | 188 | this.commandLineHistoryView = new CommandViews.CommandLineHistoryView({ 189 | el: $('#commandLineHistory'), 190 | collection: this.commandCollection 191 | }); 192 | } 193 | 194 | exports.getEvents = function() { 195 | return events; 196 | }; 197 | 198 | exports.getSandbox = function() { 199 | return sandbox; 200 | }; 201 | 202 | exports.getEventBaton = function() { 203 | return eventBaton; 204 | }; 205 | 206 | exports.getCommandUI = function() { 207 | return commandUI; 208 | }; 209 | 210 | exports.getLevelArbiter = function() { 211 | return levelArbiter; 212 | }; 213 | 214 | exports.getLevelDropdown = function() { 215 | return levelDropdown; 216 | }; 217 | 218 | exports.init = init; 219 | 220 | -------------------------------------------------------------------------------- /src/js/dialogs/levelBuilder.js: -------------------------------------------------------------------------------- 1 | exports.dialog = [{ 2 | type: 'ModalAlert', 3 | options: { 4 | markdowns: [ 5 | '## Welcome to the level builder!', 6 | '', 7 | 'Here are the main steps:', 8 | '', 9 | ' * Set up the initial environment with git commands', 10 | ' * Define the starting tree with ```define start```', 11 | ' * Enter the series of git commands that compose the (optimal) solution', 12 | ' * Define the goal tree with ```define goal```. Defining the goal also defines the solution', 13 | ' * Optionally define a hint with ```define hint```', 14 | ' * Edit the name with ```define name```', 15 | ' * Optionally define a nice start dialog with ```edit dialog```', 16 | ' * Enter the command ```finish``` to output your level JSON!' 17 | ] 18 | } 19 | }]; 20 | -------------------------------------------------------------------------------- /src/js/dialogs/sandbox.js: -------------------------------------------------------------------------------- 1 | exports.dialog = [{ 2 | type: 'ModalAlert', 3 | options: { 4 | markdowns: [ 5 | //'## Welcome to LearnGitBranching!', 6 | '## Git 브랜치 배우기를 시작합니다!', 7 | '', 8 | // 'This application is designed to help beginners grasp ', 9 | // 'the powerful concepts behind branching when working ', 10 | // 'with git. We hope you enjoy this application and maybe ', 11 | // 'even learn something!', 12 | '이 애플리케이션은 git을 쓸 때 필요한 브랜치에 대한 개념을', 13 | '탄탄히 잡게끔 도와드리기 위해 만들었습니다. 재밌게 사용해주시기를', 14 | '바라며, 무언가를 배워가신다면 더 기쁘겠습니다!', 15 | // '', 16 | // '# Attention HN!!', 17 | // '', 18 | // 'Unfortunately this was submitted before I finished all the help ', 19 | // 'and tutorial sections, so forgive the scarcity. See the demo here:', 20 | '', 21 | '이 애플리케이션은 [Peter Cottle](https://github.com/pcottle)님의 [LearnGitBranching](http://pcottle.github.com/learnGitBranching/)를 번역한 것입니다.', 22 | '아래 데모를 먼저 보셔도 좋습니다.', 23 | '', 24 | '' 25 | ] 26 | } 27 | }, { 28 | type: 'ModalAlert', 29 | options: { 30 | markdowns: [ 31 | // '## Git commands', 32 | '## Git 명령어', 33 | '', 34 | // 'You have a large variety of git commands available in sandbox mode. These include', 35 | '연습 모드에서 쓸 수 있는 다양한 git명령어는 다음과 같습니다', 36 | '', 37 | ' * commit', 38 | ' * branch', 39 | ' * checkout', 40 | ' * cherry-pick', 41 | ' * reset', 42 | ' * revert', 43 | ' * rebase', 44 | ' * merge' 45 | ] 46 | } 47 | }, { 48 | type: 'ModalAlert', 49 | options: { 50 | markdowns: [ 51 | // '## Sharing is caring!', 52 | // '', 53 | // 'Share trees with your friends via `export tree` and `import tree`', 54 | // '', 55 | // 'Have a great lesson to share? Try building a level with `build level` or try out a friend\'s level with `import level`', 56 | // '', 57 | // 'For now let\'s get you started on the `levels`...' 58 | '## 공유해주세요!', 59 | '', 60 | '`export tree` 와 `import tree`로 여러분의 친구들에게 트리를 공유해주세요', 61 | '', 62 | '훌륭한 학습 자료가 있으신가요? `build level`로 레벨을 만들어 보시거나, 친구의 레벨을 `import level`로 가져와서 실험해보세요', 63 | '', 64 | '이제 레슨을 시작해봅시다...' 65 | ] 66 | } 67 | }]; 68 | 69 | -------------------------------------------------------------------------------- /src/js/git/commands.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | var Errors = require('../util/errors'); 4 | var CommandProcessError = Errors.CommandProcessError; 5 | var GitError = Errors.GitError; 6 | var Warning = Errors.Warning; 7 | var CommandResult = Errors.CommandResult; 8 | 9 | var shortcutMap = { 10 | 'git commit': /^(gc|git ci)($|\s)/, 11 | 'git add': /^ga($|\s)/, 12 | 'git checkout': /^(go|git co)($|\s)/, 13 | 'git rebase': /^gr($|\s)/, 14 | 'git branch': /^(gb|git br)($|\s)/, 15 | 'git status': /^(gst|gs|git st)($|\s)/, 16 | 'git help': /^git$/ 17 | }; 18 | 19 | var instantCommands = [ 20 | [/^git help($|\s)/, function() { 21 | var lines = [ 22 | 'Git Version PCOTTLE.1.0', 23 | '
', 24 | // 'Usage:', 25 | '사용법:', 26 | _.escape('\t git []'), 27 | '
', 28 | // 'Supported commands:', 29 | '지원하는 명령어:', 30 | '
' 31 | ]; 32 | var commands = GitOptionParser.prototype.getMasterOptionMap(); 33 | 34 | // build up a nice display of what we support 35 | _.each(commands, function(commandOptions, command) { 36 | lines.push('git ' + command); 37 | _.each(commandOptions, function(vals, optionName) { 38 | lines.push('\t ' + optionName); 39 | }, this); 40 | }, this); 41 | 42 | // format and throw 43 | var msg = lines.join('\n'); 44 | msg = msg.replace(/\t/g, '   '); 45 | throw new CommandResult({ 46 | msg: msg 47 | }); 48 | }] 49 | ]; 50 | 51 | var regexMap = { 52 | // ($|\s) means that we either have to end the string 53 | // after the command or there needs to be a space for options 54 | 'git commit': /^git commit($|\s)/, 55 | 'git add': /^git add($|\s)/, 56 | 'git checkout': /^git checkout($|\s)/, 57 | 'git rebase': /^git rebase($|\s)/, 58 | 'git reset': /^git reset($|\s)/, 59 | 'git branch': /^git branch($|\s)/, 60 | 'git revert': /^git revert($|\s)/, 61 | 'git log': /^git log($|\s)/, 62 | 'git merge': /^git merge($|\s)/, 63 | 'git show': /^git show($|\s)/, 64 | 'git status': /^git status($|\s)/, 65 | 'git cherry-pick': /^git cherry-pick($|\s)/ 66 | }; 67 | 68 | var parse = function(str) { 69 | var method; 70 | var options; 71 | 72 | // see if we support this particular command 73 | _.each(regexMap, function(regex, thisMethod) { 74 | if (regex.exec(str)) { 75 | options = str.slice(thisMethod.length + 1); 76 | method = thisMethod.slice('git '.length); 77 | } 78 | }); 79 | 80 | if (!method) { 81 | return false; 82 | } 83 | 84 | // we support this command! 85 | // parse off the options and assemble the map / general args 86 | var parsedOptions = new GitOptionParser(method, options); 87 | return { 88 | toSet: { 89 | generalArgs: parsedOptions.generalArgs, 90 | supportedMap: parsedOptions.supportedMap, 91 | method: method, 92 | options: options, 93 | eventName: 'processGitCommand' 94 | } 95 | }; 96 | }; 97 | 98 | /** 99 | * GitOptionParser 100 | */ 101 | function GitOptionParser(method, options) { 102 | this.method = method; 103 | this.rawOptions = options; 104 | 105 | this.supportedMap = this.getMasterOptionMap()[method]; 106 | if (this.supportedMap === undefined) { 107 | throw new Error('No option map for ' + method); 108 | } 109 | 110 | this.generalArgs = []; 111 | this.explodeAndSet(); 112 | } 113 | 114 | GitOptionParser.prototype.getMasterOptionMap = function() { 115 | // here a value of false means that we support it, even if its just a 116 | // pass-through option. If the value is not here (aka will be undefined 117 | // when accessed), we do not support it. 118 | return { 119 | commit: { 120 | '--amend': false, 121 | '-a': false, // warning 122 | '-am': false, // warning 123 | '-m': false 124 | }, 125 | status: {}, 126 | log: {}, 127 | add: {}, 128 | 'cherry-pick': {}, 129 | branch: { 130 | '-d': false, 131 | '-D': false, 132 | '-f': false, 133 | '--contains': false 134 | }, 135 | checkout: { 136 | '-b': false, 137 | '-B': false, 138 | '-': false 139 | }, 140 | reset: { 141 | '--hard': false, 142 | '--soft': false // this will raise an error but we catch it in gitEngine 143 | }, 144 | merge: {}, 145 | rebase: { 146 | '-i': false // the mother of all options 147 | }, 148 | revert: {}, 149 | show: {} 150 | }; 151 | }; 152 | 153 | GitOptionParser.prototype.explodeAndSet = function() { 154 | // split on spaces, except when inside quotes 155 | 156 | var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/g) || []; 157 | 158 | for (var i = 0; i < exploded.length; i++) { 159 | var part = exploded[i]; 160 | if (part.slice(0,1) == '-') { 161 | // it's an option, check supportedMap 162 | if (this.supportedMap[part] === undefined) { 163 | throw new CommandProcessError({ 164 | // msg: 'The option "' + part + '" is not supported' 165 | msg: '옵션 "' + part + '"는 지원하지 않습니다' 166 | }); 167 | } 168 | 169 | // go through and include all the next args until we hit another option or the end 170 | var optionArgs = []; 171 | var next = i + 1; 172 | while (next < exploded.length && exploded[next].slice(0,1) != '-') { 173 | optionArgs.push(exploded[next]); 174 | next += 1; 175 | } 176 | i = next - 1; 177 | 178 | // **phew** we are done grabbing those. theseArgs is truthy even with an empty array 179 | this.supportedMap[part] = optionArgs; 180 | } else { 181 | // must be a general arg 182 | this.generalArgs.push(part); 183 | } 184 | } 185 | }; 186 | 187 | exports.shortcutMap = shortcutMap; 188 | exports.instantCommands = instantCommands; 189 | exports.parse = parse; 190 | exports.regexMap = regexMap; 191 | 192 | -------------------------------------------------------------------------------- /src/js/git/gitShim.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Q = require('q'); 3 | 4 | var Main = require('../app'); 5 | var MultiView = require('../views/multiView').MultiView; 6 | 7 | function GitShim(options) { 8 | options = options || {}; 9 | 10 | // these variables are just functions called before / after for 11 | // simple things (like incrementing a counter) 12 | this.beforeCB = options.beforeCB || function() {}; 13 | this.afterCB = options.afterCB || function() {}; 14 | 15 | // these guys handle an optional async process before the git 16 | // command executes or afterwards. If there is none, 17 | // it just resolves the deferred immediately 18 | var resolveImmediately = function(deferred) { 19 | deferred.resolve(); 20 | }; 21 | this.beforeDeferHandler = options.beforeDeferHandler || resolveImmediately; 22 | this.afterDeferHandler = options.afterDeferHandler || resolveImmediately; 23 | this.eventBaton = options.eventBaton || Main.getEventBaton(); 24 | } 25 | 26 | GitShim.prototype.insertShim = function() { 27 | this.eventBaton.stealBaton('processGitCommand', this.processGitCommand, this); 28 | }; 29 | 30 | GitShim.prototype.removeShim = function() { 31 | this.eventBaton.releaseBaton('processGitCommand', this.processGitCommand, this); 32 | }; 33 | 34 | GitShim.prototype.processGitCommand = function(command, deferred) { 35 | this.beforeCB(command); 36 | 37 | // ok we make a NEW deferred that will, upon resolution, 38 | // call our afterGitCommandProcessed. This inserts the 'after' shim 39 | // functionality. we give this new deferred to the eventBaton handler 40 | var newDeferred = Q.defer(); 41 | newDeferred.promise 42 | .then(_.bind(function() { 43 | // give this method the original defer so it can resolve it 44 | this.afterGitCommandProcessed(command, deferred); 45 | }, this)) 46 | .done(); 47 | 48 | // now our shim owner might want to launch some kind of deferred beforehand, like 49 | // a modal or something. in order to do this, we need to defer the passing 50 | // of the event baton backwards, and either resolve that promise immediately or 51 | // give it to our shim owner. 52 | var passBaton = _.bind(function() { 53 | // punt to the previous listener 54 | this.eventBaton.passBatonBack('processGitCommand', this.processGitCommand, this, [command, newDeferred]); 55 | }, this); 56 | 57 | var beforeDefer = Q.defer(); 58 | beforeDefer.promise 59 | .then(passBaton) 60 | .done(); 61 | 62 | // if we didnt receive a defer handler in the options, this just 63 | // resolves immediately 64 | this.beforeDeferHandler(beforeDefer, command); 65 | }; 66 | 67 | GitShim.prototype.afterGitCommandProcessed = function(command, deferred) { 68 | this.afterCB(command); 69 | 70 | // again we can't just resolve this deferred right away... our shim owner might 71 | // want to insert some promise functionality before that happens. so again 72 | // we make a defer 73 | var afterDefer = Q.defer(); 74 | afterDefer.promise 75 | .then(function() { 76 | deferred.resolve(); 77 | }) 78 | .done(); 79 | 80 | this.afterDeferHandler(afterDefer, command); 81 | }; 82 | 83 | exports.GitShim = GitShim; 84 | 85 | -------------------------------------------------------------------------------- /src/js/git/headless.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | var Q = require('q'); 4 | 5 | var GitEngine = require('../git').GitEngine; 6 | var AnimationFactory = require('../visuals/animation/animationFactory').AnimationFactory; 7 | var GitVisuals = require('../visuals').GitVisuals; 8 | var TreeCompare = require('../git/treeCompare').TreeCompare; 9 | var EventBaton = require('../util/eventBaton').EventBaton; 10 | 11 | var Collections = require('../models/collections'); 12 | var CommitCollection = Collections.CommitCollection; 13 | var BranchCollection = Collections.BranchCollection; 14 | var Command = require('../models/commandModel').Command; 15 | 16 | var mock = require('../util/mock').mock; 17 | var util = require('../util'); 18 | 19 | var HeadlessGit = function() { 20 | this.init(); 21 | }; 22 | 23 | HeadlessGit.prototype.init = function() { 24 | this.commitCollection = new CommitCollection(); 25 | this.branchCollection = new BranchCollection(); 26 | this.treeCompare = new TreeCompare(); 27 | 28 | // here we mock visuals and animation factory so the git engine 29 | // is headless 30 | var animationFactory = mock(AnimationFactory); 31 | var gitVisuals = mock(GitVisuals); 32 | 33 | this.gitEngine = new GitEngine({ 34 | collection: this.commitCollection, 35 | branches: this.branchCollection, 36 | gitVisuals: gitVisuals, 37 | animationFactory: animationFactory, 38 | eventBaton: new EventBaton() 39 | }); 40 | this.gitEngine.init(); 41 | }; 42 | 43 | HeadlessGit.prototype.sendCommand = function(value) { 44 | util.splitTextCommand(value, function(commandStr) { 45 | var commandObj = new Command({ 46 | rawStr: commandStr 47 | }); 48 | this.gitEngine.dispatch(commandObj, Q.defer()); 49 | }, this); 50 | }; 51 | 52 | exports.HeadlessGit = HeadlessGit; 53 | 54 | -------------------------------------------------------------------------------- /src/js/git/treeCompare.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | // static class... 4 | function TreeCompare() { 5 | 6 | } 7 | 8 | TreeCompare.prototype.compareAllBranchesWithinTreesAndHEAD = function(treeA, treeB) { 9 | treeA = this.convertTreeSafe(treeA); 10 | treeB = this.convertTreeSafe(treeB); 11 | 12 | return treeA.HEAD.target == treeB.HEAD.target && this.compareAllBranchesWithinTrees(treeA, treeB); 13 | }; 14 | 15 | TreeCompare.prototype.compareAllBranchesWithinTrees = function(treeA, treeB) { 16 | treeA = this.convertTreeSafe(treeA); 17 | treeB = this.convertTreeSafe(treeB); 18 | 19 | var allBranches = _.extend( 20 | {}, 21 | treeA.branches, 22 | treeB.branches 23 | ); 24 | 25 | var result = true; 26 | _.uniq(allBranches, function(info, branch) { 27 | result = result && this.compareBranchWithinTrees(treeA, treeB, branch); 28 | }, this); 29 | return result; 30 | }; 31 | 32 | TreeCompare.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) { 33 | var result = true; 34 | _.each(branches, function(branchName) { 35 | result = result && this.compareBranchWithinTrees(treeA, treeB, branchName); 36 | }, this); 37 | 38 | return result; 39 | }; 40 | 41 | TreeCompare.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) { 42 | treeA = this.convertTreeSafe(treeA); 43 | treeB = this.convertTreeSafe(treeB); 44 | this.reduceTreeFields([treeA, treeB]); 45 | 46 | var recurseCompare = this.getRecurseCompare(treeA, treeB); 47 | var branchA = treeA.branches[branchName]; 48 | var branchB = treeB.branches[branchName]; 49 | 50 | return _.isEqual(branchA, branchB) && 51 | recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); 52 | }; 53 | 54 | TreeCompare.prototype.compareAllBranchesWithinTreesHashAgnostic = function(treeA, treeB) { 55 | treeA = this.convertTreeSafe(treeA); 56 | treeB = this.convertTreeSafe(treeB); 57 | this.reduceTreeFields([treeA, treeB]); 58 | 59 | var allBranches = _.extend( 60 | {}, 61 | treeA.branches, 62 | treeB.branches 63 | ); 64 | var branchNames = []; 65 | _.each(allBranches, function(obj, name) { branchNames.push(name); }); 66 | 67 | return this.compareBranchesWithinTreesHashAgnostic(treeA, treeB, branchNames); 68 | }; 69 | 70 | TreeCompare.prototype.compareBranchesWithinTreesHashAgnostic = function(treeA, treeB, branches) { 71 | // we can't DRY unfortunately here because we need a special _.isEqual function 72 | // for both the recursive compare and the branch compare 73 | treeA = this.convertTreeSafe(treeA); 74 | treeB = this.convertTreeSafe(treeB); 75 | this.reduceTreeFields([treeA, treeB]); 76 | 77 | // get a function to compare branch objects without hashes 78 | var compareBranchObjs = _.bind(function(branchA, branchB) { 79 | if (!branchA || !branchB) { 80 | return false; 81 | } 82 | branchA.target = this.getBaseRef(branchA.target); 83 | branchB.target = this.getBaseRef(branchB.target); 84 | 85 | return _.isEqual(branchA, branchB); 86 | }, this); 87 | // and a function to compare recursively without worrying about hashes 88 | var recurseCompare = this.getRecurseCompareHashAgnostic(treeA, treeB); 89 | 90 | var result = true; 91 | _.each(branches, function(branchName) { 92 | var branchA = treeA.branches[branchName]; 93 | var branchB = treeB.branches[branchName]; 94 | 95 | result = result && compareBranchObjs(branchA, branchB) && 96 | recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]); 97 | }, this); 98 | return result; 99 | }; 100 | 101 | TreeCompare.prototype.getBaseRef = function(ref) { 102 | var idRegex = /^C(\d+)/; 103 | var bits = idRegex.exec(ref); 104 | if (!bits) { throw new Error('no regex matchy for ' + ref); } 105 | // no matter what hash this is (aka C1', C1'', C1'^3, etc) we 106 | // return C1 107 | return 'C' + bits[1]; 108 | }; 109 | 110 | TreeCompare.prototype.getRecurseCompareHashAgnostic = function(treeA, treeB) { 111 | // here we pass in a special comparison function to pass into the base 112 | // recursive compare. 113 | 114 | // some buildup functions 115 | var getStrippedCommitCopy = _.bind(function(commit) { 116 | return _.extend( 117 | {}, 118 | commit, 119 | {id: this.getBaseRef(commit.id) 120 | }); 121 | }, this); 122 | 123 | var isEqual = function(commitA, commitB) { 124 | return _.isEqual( 125 | getStrippedCommitCopy(commitA), 126 | getStrippedCommitCopy(commitB) 127 | ); 128 | }; 129 | return this.getRecurseCompare(treeA, treeB, {isEqual: isEqual}); 130 | }; 131 | 132 | TreeCompare.prototype.getRecurseCompare = function(treeA, treeB, options) { 133 | options = options || {}; 134 | 135 | // we need a recursive comparison function to bubble up the branch 136 | var recurseCompare = function(commitA, commitB) { 137 | // this is the short-circuit base case 138 | var result = options.isEqual ? 139 | options.isEqual(commitA, commitB) : _.isEqual(commitA, commitB); 140 | if (!result) { 141 | return false; 142 | } 143 | 144 | // we loop through each parent ID. we sort the parent ID's beforehand 145 | // so the index lookup is valid. for merge commits this will duplicate some of the 146 | // checking (because we aren't doing graph search) but it's not a huge deal 147 | var allParents = _.unique(commitA.parents.concat(commitB.parents)); 148 | _.each(allParents, function(pAid, index) { 149 | var pBid = commitB.parents[index]; 150 | 151 | // if treeA or treeB doesn't have this parent, 152 | // then we get an undefined child which is fine when we pass into _.isEqual 153 | var childA = treeA.commits[pAid]; 154 | var childB = treeB.commits[pBid]; 155 | 156 | result = result && recurseCompare(childA, childB); 157 | }, this); 158 | // if each of our children recursively are equal, we are good 159 | return result; 160 | }; 161 | return recurseCompare; 162 | }; 163 | 164 | TreeCompare.prototype.convertTreeSafe = function(tree) { 165 | if (typeof tree == 'string') { 166 | return JSON.parse(unescape(tree)); 167 | } 168 | return tree; 169 | }; 170 | 171 | TreeCompare.prototype.reduceTreeFields = function(trees) { 172 | var commitSaveFields = [ 173 | 'parents', 174 | 'id', 175 | 'rootCommit' 176 | ]; 177 | var commitSortFields = ['children', 'parents']; 178 | var branchSaveFields = [ 179 | 'target', 180 | 'id' 181 | ]; 182 | 183 | // this function saves only the specified fields of a tree 184 | var saveOnly = function(tree, treeKey, saveFields, sortFields) { 185 | var objects = tree[treeKey]; 186 | _.each(objects, function(obj, objKey) { 187 | // our blank slate to copy over 188 | var blank = {}; 189 | _.each(saveFields, function(field) { 190 | if (obj[field] !== undefined) { 191 | blank[field] = obj[field]; 192 | } 193 | }); 194 | 195 | _.each(sortFields, function(field) { 196 | // also sort some fields 197 | if (obj[field]) { 198 | obj[field].sort(); 199 | blank[field] = obj[field]; 200 | } 201 | }); 202 | tree[treeKey][objKey] = blank; 203 | }); 204 | }; 205 | 206 | _.each(trees, function(tree) { 207 | saveOnly(tree, 'commits', commitSaveFields, commitSortFields); 208 | saveOnly(tree, 'branches', branchSaveFields); 209 | 210 | tree.HEAD = { 211 | target: tree.HEAD.target, 212 | id: tree.HEAD.id 213 | }; 214 | }); 215 | }; 216 | 217 | TreeCompare.prototype.compareTrees = function(treeA, treeB) { 218 | treeA = this.convertTreeSafe(treeA); 219 | treeB = this.convertTreeSafe(treeB); 220 | 221 | // now we need to strip out the fields we don't care about, aka things 222 | // like createTime, message, author 223 | this.reduceTreeFields([treeA, treeB]); 224 | 225 | return _.isEqual(treeA, treeB); 226 | }; 227 | 228 | exports.TreeCompare = TreeCompare; 229 | 230 | -------------------------------------------------------------------------------- /src/js/level/arbiter.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | 4 | // Each level is part of a "sequence;" levels within 5 | // a sequence proceed in order. 6 | var levelSequences = require('../../levels').levelSequences; 7 | var sequenceInfo = require('../../levels').sequenceInfo; 8 | 9 | var Main = require('../app'); 10 | 11 | function LevelArbiter() { 12 | this.levelMap = {}; 13 | this.levelSequences = levelSequences; 14 | this.sequences = []; 15 | this.init(); 16 | 17 | var solvedMap; 18 | try { 19 | solvedMap = JSON.parse(localStorage.getItem('solvedMap') || '{}'); 20 | } catch (e) { 21 | console.warn('local storage failed', e); 22 | // throw e; 23 | } 24 | this.solvedMap = solvedMap || {}; 25 | 26 | Main.getEvents().on('levelSolved', this.levelSolved, this); 27 | } 28 | 29 | LevelArbiter.prototype.init = function() { 30 | var previousLevelID; 31 | _.each(this.levelSequences, function(levels, levelSequenceName) { 32 | this.sequences.push(levelSequenceName); 33 | if (!levels || !levels.length) { 34 | throw new Error('no empty sequences allowed'); 35 | } 36 | 37 | // for this particular sequence... 38 | _.each(levels, function(level, index) { 39 | this.validateLevel(level); 40 | 41 | var id = levelSequenceName + String(index + 1); 42 | var compiledLevel = _.extend( 43 | {}, 44 | level, 45 | { 46 | index: index, 47 | id: id, 48 | sequenceName: levelSequenceName 49 | } 50 | ); 51 | 52 | // update our internal data 53 | this.levelMap[id] = compiledLevel; 54 | this.levelSequences[levelSequenceName][index] = compiledLevel; 55 | }, this); 56 | }, this); 57 | }; 58 | 59 | LevelArbiter.prototype.isLevelSolved = function(id) { 60 | if (!this.levelMap[id]) { 61 | throw new Error('that level doesnt exist!'); 62 | } 63 | return Boolean(this.solvedMap[id]); 64 | }; 65 | 66 | LevelArbiter.prototype.levelSolved = function(id) { 67 | // called without an id when we reset solved status 68 | if (!id) { return; } 69 | 70 | this.solvedMap[id] = true; 71 | this.syncToStorage(); 72 | }; 73 | 74 | LevelArbiter.prototype.resetSolvedMap = function() { 75 | this.solvedMap = {}; 76 | this.syncToStorage(); 77 | Main.getEvents().trigger('levelSolved'); 78 | }; 79 | 80 | LevelArbiter.prototype.syncToStorage = function() { 81 | try { 82 | localStorage.setItem('solvedMap', JSON.stringify(this.solvedMap)); 83 | } catch (e) { 84 | console.warn('local storage fialed on set', e); 85 | } 86 | }; 87 | 88 | LevelArbiter.prototype.validateLevel = function(level) { 89 | level = level || {}; 90 | var requiredFields = [ 91 | 'name', 92 | 'goalTreeString', 93 | //'description', 94 | 'solutionCommand' 95 | ]; 96 | 97 | var optionalFields = [ 98 | 'hint', 99 | 'disabledMap', 100 | 'startTree' 101 | ]; 102 | 103 | _.each(requiredFields, function(field) { 104 | if (level[field] === undefined) { 105 | console.log(level); 106 | throw new Error('I need this field for a level: ' + field); 107 | } 108 | }); 109 | }; 110 | 111 | LevelArbiter.prototype.getSequenceToLevels = function() { 112 | return this.levelSequences; 113 | }; 114 | 115 | LevelArbiter.prototype.getSequences = function() { 116 | return _.keys(this.levelSequences); 117 | }; 118 | 119 | LevelArbiter.prototype.getLevelsInSequence = function(sequenceName) { 120 | if (!this.levelSequences[sequenceName]) { 121 | throw new Error('that sequecne name ' + sequenceName + 'does not exist'); 122 | } 123 | return this.levelSequences[sequenceName]; 124 | }; 125 | 126 | LevelArbiter.prototype.getSequenceInfo = function(sequenceName) { 127 | return sequenceInfo[sequenceName]; 128 | }; 129 | 130 | LevelArbiter.prototype.getLevel = function(id) { 131 | return this.levelMap[id]; 132 | }; 133 | 134 | LevelArbiter.prototype.getNextLevel = function(id) { 135 | if (!this.levelMap[id]) { 136 | console.warn('that level doesnt exist!!!'); 137 | return null; 138 | } 139 | 140 | // meh, this method could be better. It's a tradeoff between 141 | // having the sequence structure be really simple JSON 142 | // and having no connectivity information between levels, which means 143 | // you have to build that up yourself on every query 144 | var level = this.levelMap[id]; 145 | var sequenceName = level.sequenceName; 146 | var sequence = this.levelSequences[sequenceName]; 147 | 148 | var nextIndex = level.index + 1; 149 | if (nextIndex < sequence.length) { 150 | return sequence[nextIndex]; 151 | } 152 | 153 | var nextSequenceIndex = this.sequences.indexOf(sequenceName) + 1; 154 | if (nextSequenceIndex < this.sequences.length) { 155 | var nextSequenceName = this.sequences[nextSequenceIndex]; 156 | return this.levelSequences[nextSequenceName][0]; 157 | } 158 | 159 | // they finished the last level! 160 | return null; 161 | }; 162 | 163 | exports.LevelArbiter = LevelArbiter; 164 | 165 | -------------------------------------------------------------------------------- /src/js/level/builder.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | var Q = require('q'); 4 | 5 | var util = require('../util'); 6 | var Main = require('../app'); 7 | var Errors = require('../util/errors'); 8 | 9 | var Visualization = require('../visuals/visualization').Visualization; 10 | var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall; 11 | var Level = require('../level').Level; 12 | 13 | var Command = require('../models/commandModel').Command; 14 | var GitShim = require('../git/gitShim').GitShim; 15 | 16 | var MultiView = require('../views/multiView').MultiView; 17 | 18 | var CanvasTerminalHolder = require('../views').CanvasTerminalHolder; 19 | var ConfirmCancelTerminal = require('../views').ConfirmCancelTerminal; 20 | var NextLevelConfirm = require('../views').NextLevelConfirm; 21 | var LevelToolbar = require('../views').LevelToolbar; 22 | 23 | var MarkdownPresenter = require('../views/builderViews').MarkdownPresenter; 24 | var MultiViewBuilder = require('../views/builderViews').MultiViewBuilder; 25 | var MarkdownGrabber = require('../views/builderViews').MarkdownGrabber; 26 | 27 | var regexMap = { 28 | 'define goal': /^define goal$/, 29 | 'define name': /^define name$/, 30 | 'help builder': /^help builder$/, 31 | 'define start': /^define start$/, 32 | 'edit dialog': /^edit dialog$/, 33 | 'show start': /^show start$/, 34 | 'hide start': /^hide start$/, 35 | 'define hint': /^define hint$/, 36 | 'finish': /^finish$/ 37 | }; 38 | 39 | var parse = util.genParseCommand(regexMap, 'processLevelBuilderCommand'); 40 | 41 | var LevelBuilder = Level.extend({ 42 | initialize: function(options) { 43 | options = options || {}; 44 | options.level = options.level || {}; 45 | 46 | options.level.startDialog = { 47 | childViews: require('../dialogs/levelBuilder').dialog 48 | }; 49 | LevelBuilder.__super__.initialize.apply(this, [options]); 50 | 51 | this.startDialog = undefined; 52 | this.definedGoal = false; 53 | 54 | // we wont be using this stuff, and its to delete to ensure we overwrite all functions that 55 | // include that functionality 56 | delete this.treeCompare; 57 | delete this.solved; 58 | }, 59 | 60 | initName: function() { 61 | this.levelToolbar = new LevelToolbar({ 62 | name: 'Level Builder' 63 | }); 64 | }, 65 | 66 | initGoalData: function() { 67 | // add some default behavior in the beginning 68 | this.level.goalTreeString = '{"branches":{"master":{"target":"C1","id":"master"},"makeLevel":{"target":"C2","id":"makeLevel"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"makeLevel","id":"HEAD"}}'; 69 | this.level.solutionCommand = 'git checkout -b makeLevel; git commit'; 70 | LevelBuilder.__super__.initGoalData.apply(this, arguments); 71 | }, 72 | 73 | initStartVisualization: function() { 74 | this.startCanvasHolder = new CanvasTerminalHolder({ 75 | additionalClass: 'startTree', 76 | text: 'You can hide this window with "hide start"' 77 | }); 78 | 79 | this.startVis = new Visualization({ 80 | el: this.startCanvasHolder.getCanvasLocation(), 81 | containerElement: this.startCanvasHolder.getCanvasLocation(), 82 | treeString: this.level.startTree, 83 | noKeyboardInput: true, 84 | noClick: true 85 | }); 86 | return this.startCanvasHolder; 87 | }, 88 | 89 | startOffCommand: function() { 90 | Main.getEventBaton().trigger( 91 | 'commandSubmitted', 92 | 'echo "Get Building!!"' 93 | ); 94 | }, 95 | 96 | initParseWaterfall: function(options) { 97 | LevelBuilder.__super__.initParseWaterfall.apply(this, [options]); 98 | 99 | this.parseWaterfall.addFirst( 100 | 'parseWaterfall', 101 | parse 102 | ); 103 | this.parseWaterfall.addFirst( 104 | 'instantWaterfall', 105 | this.getInstantCommands() 106 | ); 107 | }, 108 | 109 | buildLevel: function(command, deferred) { 110 | this.exitLevel(); 111 | 112 | setTimeout(function() { 113 | Main.getSandbox().buildLevel(command, deferred); 114 | }, this.getAnimationTime() * 1.5); 115 | }, 116 | 117 | getInstantCommands: function() { 118 | return [ 119 | [/^help$|^\?$/, function() { 120 | throw new Errors.CommandResult({ 121 | msg: 'You are in a level builder, so multiple forms of ' + 122 | 'help are available. Please select either ' + 123 | '"help general" or "help builder"' 124 | }); 125 | }] 126 | ]; 127 | }, 128 | 129 | takeControl: function() { 130 | Main.getEventBaton().stealBaton('processLevelBuilderCommand', this.processLevelBuilderCommand, this); 131 | 132 | LevelBuilder.__super__.takeControl.apply(this); 133 | }, 134 | 135 | releaseControl: function() { 136 | Main.getEventBaton().releaseBaton('processLevelBuilderCommand', this.processLevelBuilderCommand, this); 137 | 138 | LevelBuilder.__super__.releaseControl.apply(this); 139 | }, 140 | 141 | showGoal: function() { 142 | this.hideStart(); 143 | LevelBuilder.__super__.showGoal.apply(this, arguments); 144 | }, 145 | 146 | showStart: function(command, deferred) { 147 | this.hideGoal(); 148 | this.showSideVis(command, deferred, this.startCanvasHolder, this.initStartVisualization); 149 | }, 150 | 151 | resetSolution: function() { 152 | this.gitCommandsIssued = []; 153 | this.level.solutionCommand = undefined; 154 | }, 155 | 156 | hideStart: function(command, deferred) { 157 | this.hideSideVis(command, deferred, this.startCanvasHolder); 158 | }, 159 | 160 | defineStart: function(command, deferred) { 161 | this.hideStart(); 162 | 163 | command.addWarning( 164 | 'Defining start point... solution and goal will be overwritten if they were defined earlier' 165 | ); 166 | this.resetSolution(); 167 | 168 | this.level.startTree = this.mainVis.gitEngine.printTree(); 169 | this.mainVis.resetFromThisTreeNow(this.level.startTree); 170 | 171 | this.showStart(command, deferred); 172 | }, 173 | 174 | defineGoal: function(command, deferred) { 175 | this.hideGoal(); 176 | 177 | if (!this.gitCommandsIssued.length) { 178 | command.set('error', new Errors.GitError({ 179 | msg: 'Your solution is empty!! something is amiss' 180 | })); 181 | deferred.resolve(); 182 | return; 183 | } 184 | 185 | this.definedGoal = true; 186 | this.level.solutionCommand = this.gitCommandsIssued.join(';'); 187 | this.level.goalTreeString = this.mainVis.gitEngine.printTree(); 188 | this.initGoalVisualization(); 189 | 190 | this.showGoal(command, deferred); 191 | }, 192 | 193 | defineName: function(command, deferred) { 194 | this.level.name = prompt('Enter the name for the level'); 195 | if (command) { command.finishWith(deferred); } 196 | }, 197 | 198 | defineHint: function(command, deferred) { 199 | this.level.hint = prompt('Enter a hint! Or blank if you dont want one'); 200 | if (command) { command.finishWith(deferred); } 201 | }, 202 | 203 | editDialog: function(command, deferred) { 204 | var whenDoneEditing = Q.defer(); 205 | this.currentBuilder = new MultiViewBuilder({ 206 | multiViewJSON: this.startDialog, 207 | deferred: whenDoneEditing 208 | }); 209 | whenDoneEditing.promise 210 | .then(_.bind(function(levelObj) { 211 | this.startDialog = levelObj; 212 | }, this)) 213 | .fail(function() { 214 | // nothing to do, they dont want to edit it apparently 215 | }) 216 | .done(function() { 217 | if (command) { 218 | command.finishWith(deferred); 219 | } else { 220 | deferred.resolve(); 221 | } 222 | }); 223 | }, 224 | 225 | finish: function(command, deferred) { 226 | if (!this.gitCommandsIssued.length || !this.definedGoal) { 227 | command.set('error', new Errors.GitError({ 228 | msg: 'Your solution is empty or goal is undefined!' 229 | })); 230 | deferred.resolve(); 231 | return; 232 | } 233 | 234 | while (!this.level.name) { 235 | this.defineName(); 236 | } 237 | 238 | var masterDeferred = Q.defer(); 239 | var chain = masterDeferred.promise; 240 | 241 | if (this.level.hint === undefined) { 242 | var askForHintDeferred = Q.defer(); 243 | chain = chain.then(function() { 244 | return askForHintDeferred.promise; 245 | }); 246 | 247 | // ask for a hint if there is none 248 | var askForHintView = new ConfirmCancelTerminal({ 249 | markdowns: [ 250 | 'You have not specified a hint, would you like to add one?' 251 | ] 252 | }); 253 | askForHintView.getPromise() 254 | .then(_.bind(this.defineHint, this)) 255 | .fail(_.bind(function() { 256 | this.level.hint = ''; 257 | }, this)) 258 | .done(function() { 259 | askForHintDeferred.resolve(); 260 | }); 261 | } 262 | 263 | if (this.startDialog === undefined) { 264 | var askForStartDeferred = Q.defer(); 265 | chain = chain.then(function() { 266 | return askForStartDeferred.promise; 267 | }); 268 | 269 | var askForStartView = new ConfirmCancelTerminal({ 270 | markdowns: [ 271 | 'You have not specified a start dialog, would you like to add one?' 272 | ] 273 | }); 274 | askForStartView.getPromise() 275 | .then(_.bind(function() { 276 | // oh boy this is complex 277 | var whenEditedDialog = Q.defer(); 278 | // the undefined here is the command that doesnt need resolving just yet... 279 | this.editDialog(undefined, whenEditedDialog); 280 | return whenEditedDialog.promise; 281 | }, this)) 282 | .fail(function() { 283 | // if they dont want to edit the start dialog, do nothing 284 | }) 285 | .done(function() { 286 | askForStartDeferred.resolve(); 287 | }); 288 | } 289 | 290 | chain = chain.done(_.bind(function() { 291 | // ok great! lets just give them the goods 292 | new MarkdownPresenter({ 293 | fillerText: JSON.stringify(this.getExportObj(), null, 2), 294 | previewText: 'Here is the JSON for this level! Share it with someone or send it to me on Github!' 295 | }); 296 | command.finishWith(deferred); 297 | }, this)); 298 | 299 | masterDeferred.resolve(); 300 | }, 301 | 302 | getExportObj: function() { 303 | var compiledLevel = _.extend( 304 | {}, 305 | this.level 306 | ); 307 | // the start dialog now is just our help intro thing 308 | delete compiledLevel.startDialog; 309 | if (this.startDialog) { 310 | compiledLevel.startDialog = this.startDialog; 311 | } 312 | return compiledLevel; 313 | }, 314 | 315 | processLevelBuilderCommand: function(command, deferred) { 316 | var methodMap = { 317 | 'define goal': this.defineGoal, 318 | 'define start': this.defineStart, 319 | 'show start': this.showStart, 320 | 'hide start': this.hideStart, 321 | 'finish': this.finish, 322 | 'define hint': this.defineHint, 323 | 'define name': this.defineName, 324 | 'edit dialog': this.editDialog, 325 | 'help builder': LevelBuilder.__super__.startDialog 326 | }; 327 | if (!methodMap[command.get('method')]) { 328 | throw new Error('woah we dont support that method yet'); 329 | } 330 | 331 | methodMap[command.get('method')].apply(this, arguments); 332 | }, 333 | 334 | afterCommandDefer: function(defer, command) { 335 | // we dont need to compare against the goal anymore 336 | defer.resolve(); 337 | }, 338 | 339 | die: function() { 340 | this.hideStart(); 341 | 342 | LevelBuilder.__super__.die.apply(this, arguments); 343 | 344 | delete this.startVis; 345 | delete this.startCanvasHolder; 346 | } 347 | }); 348 | 349 | exports.LevelBuilder = LevelBuilder; 350 | exports.regexMap = regexMap; 351 | -------------------------------------------------------------------------------- /src/js/level/disabledMap.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | var GitCommands = require('../git/commands'); 4 | 5 | var Errors = require('../util/errors'); 6 | var GitError = Errors.GitError; 7 | 8 | function DisabledMap(options) { 9 | options = options || {}; 10 | this.disabledMap = options.disabledMap || { 11 | 'git cherry-pick': true, 12 | 'git rebase': true 13 | }; 14 | } 15 | 16 | DisabledMap.prototype.getInstantCommands = function() { 17 | // this produces an array of regex / function pairs that can be 18 | // piped into a parse waterfall to disable certain git commmands 19 | // :D 20 | var instants = []; 21 | var onMatch = function() { 22 | throw new GitError({ 23 | msg: 'That git command is disabled for this level!' 24 | }); 25 | }; 26 | 27 | _.each(this.disabledMap, function(val, disabledCommand) { 28 | var gitRegex = GitCommands.regexMap[disabledCommand]; 29 | if (!gitRegex) { 30 | throw new Error('wuttttt this disbaled command' + disabledCommand + 31 | ' has no regex matching'); 32 | } 33 | instants.push([gitRegex, onMatch]); 34 | }); 35 | return instants; 36 | }; 37 | 38 | exports.DisabledMap = DisabledMap; 39 | 40 | -------------------------------------------------------------------------------- /src/js/level/parseWaterfall.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | var GitCommands = require('../git/commands'); 4 | var SandboxCommands = require('../level/SandboxCommands'); 5 | 6 | // more or less a static class 7 | var ParseWaterfall = function(options) { 8 | options = options || {}; 9 | this.options = options; 10 | this.shortcutWaterfall = options.shortcutWaterfall || [ 11 | GitCommands.shortcutMap 12 | ]; 13 | 14 | this.instantWaterfall = options.instantWaterfall || [ 15 | GitCommands.instantCommands, 16 | SandboxCommands.instantCommands 17 | ]; 18 | 19 | // defer the parse waterfall until later... 20 | }; 21 | 22 | ParseWaterfall.prototype.initParseWaterfall = function() { 23 | // check for node when testing 24 | if (!require('../util').isBrowser()) { 25 | this.parseWaterfall = [GitCommands.parse]; 26 | return; 27 | } 28 | 29 | // by deferring the initialization here, we dont require() 30 | // level too early (which barfs our init) 31 | this.parseWaterfall = this.options.parseWaterfall || [ 32 | GitCommands.parse, 33 | SandboxCommands.parse, 34 | SandboxCommands.getOptimisticLevelParse(), 35 | SandboxCommands.getOptimisticLevelBuilderParse() 36 | ]; 37 | }; 38 | 39 | ParseWaterfall.prototype.clone = function() { 40 | return new ParseWaterfall({ 41 | shortcutWaterfall: this.shortcutWaterfall.slice(), 42 | instantWaterfall: this.instantWaterfall.slice(), 43 | parseWaterfall: this.parseWaterfall.slice() 44 | }); 45 | }; 46 | 47 | ParseWaterfall.prototype.getWaterfallMap = function() { 48 | if (!this.parseWaterfall) { 49 | this.initParseWaterfall(); 50 | } 51 | return { 52 | shortcutWaterfall: this.shortcutWaterfall, 53 | instantWaterfall: this.instantWaterfall, 54 | parseWaterfall: this.parseWaterfall 55 | }; 56 | }; 57 | 58 | ParseWaterfall.prototype.addFirst = function(which, value) { 59 | if (!which || !value) { 60 | throw new Error('need to know which!!!'); 61 | } 62 | this.getWaterfallMap()[which].unshift(value); 63 | }; 64 | 65 | ParseWaterfall.prototype.addLast = function(which, value) { 66 | this.getWaterfallMap()[which].push(value); 67 | }; 68 | 69 | ParseWaterfall.prototype.expandAllShortcuts = function(commandStr) { 70 | _.each(this.shortcutWaterfall, function(shortcutMap) { 71 | commandStr = this.expandShortcut(commandStr, shortcutMap); 72 | }, this); 73 | return commandStr; 74 | }; 75 | 76 | ParseWaterfall.prototype.expandShortcut = function(commandStr, shortcutMap) { 77 | _.each(shortcutMap, function(regex, method) { 78 | var results = regex.exec(commandStr); 79 | if (results) { 80 | commandStr = method + ' ' + commandStr.slice(results[0].length); 81 | } 82 | }); 83 | return commandStr; 84 | }; 85 | 86 | ParseWaterfall.prototype.processAllInstants = function(commandStr) { 87 | _.each(this.instantWaterfall, function(instantCommands) { 88 | this.processInstant(commandStr, instantCommands); 89 | }, this); 90 | }; 91 | 92 | ParseWaterfall.prototype.processInstant = function(commandStr, instantCommands) { 93 | _.each(instantCommands, function(tuple) { 94 | var regex = tuple[0]; 95 | var results = regex.exec(commandStr); 96 | if (results) { 97 | // this will throw a result because it's an instant 98 | tuple[1](results); 99 | } 100 | }); 101 | }; 102 | 103 | ParseWaterfall.prototype.parseAll = function(commandStr) { 104 | if (!this.parseWaterfall) { 105 | this.initParseWaterfall(); 106 | } 107 | 108 | var toReturn = false; 109 | _.each(this.parseWaterfall, function(parseFunc) { 110 | var results = parseFunc(commandStr); 111 | if (results) { 112 | toReturn = results; 113 | } 114 | }, this); 115 | 116 | return toReturn; 117 | }; 118 | 119 | exports.ParseWaterfall = ParseWaterfall; 120 | 121 | -------------------------------------------------------------------------------- /src/js/level/sandbox.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Q = require('q'); 3 | // horrible hack to get localStorage Backbone plugin 4 | var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone; 5 | 6 | var util = require('../util'); 7 | var Main = require('../app'); 8 | 9 | var Visualization = require('../visuals/visualization').Visualization; 10 | var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall; 11 | var DisabledMap = require('../level/disabledMap').DisabledMap; 12 | var Command = require('../models/commandModel').Command; 13 | var GitShim = require('../git/gitShim').GitShim; 14 | 15 | var Views = require('../views'); 16 | var ModalTerminal = Views.ModalTerminal; 17 | var ModalAlert = Views.ModalAlert; 18 | var BuilderViews = require('../views/builderViews'); 19 | var MultiView = require('../views/multiView').MultiView; 20 | 21 | var Sandbox = Backbone.View.extend({ 22 | // tag name here is purely vestigial. I made this a view 23 | // simply to use inheritance and have a nice event system in place 24 | tagName: 'div', 25 | initialize: function(options) { 26 | options = options || {}; 27 | this.options = options; 28 | 29 | this.initVisualization(options); 30 | this.initCommandCollection(options); 31 | this.initParseWaterfall(options); 32 | this.initGitShim(options); 33 | 34 | if (!options.wait) { 35 | this.takeControl(); 36 | } 37 | }, 38 | 39 | getDefaultVisEl: function() { 40 | return $('#mainVisSpace')[0]; 41 | }, 42 | 43 | getAnimationTime: function() { return 700 * 1.5; }, 44 | 45 | initVisualization: function(options) { 46 | this.mainVis = new Visualization({ 47 | el: options.el || this.getDefaultVisEl() 48 | }); 49 | }, 50 | 51 | initCommandCollection: function(options) { 52 | // don't add it to just any collection -- adding to the 53 | // CommandUI collection will put in history 54 | this.commandCollection = Main.getCommandUI().commandCollection; 55 | }, 56 | 57 | initParseWaterfall: function(options) { 58 | this.parseWaterfall = new ParseWaterfall(); 59 | }, 60 | 61 | initGitShim: function(options) { 62 | }, 63 | 64 | takeControl: function() { 65 | // we will be handling commands that are submitted, mainly to add the sanadbox 66 | // functionality (which is included by default in ParseWaterfall()) 67 | Main.getEventBaton().stealBaton('commandSubmitted', this.commandSubmitted, this); 68 | // we obviously take care of sandbox commands 69 | Main.getEventBaton().stealBaton('processSandboxCommand', this.processSandboxCommand, this); 70 | 71 | // a few things to help transition between levels and sandbox 72 | Main.getEventBaton().stealBaton('levelExited', this.levelExited, this); 73 | 74 | this.insertGitShim(); 75 | }, 76 | 77 | releaseControl: function() { 78 | // we will be handling commands that are submitted, mainly to add the sanadbox 79 | // functionality (which is included by default in ParseWaterfall()) 80 | Main.getEventBaton().releaseBaton('commandSubmitted', this.commandSubmitted, this); 81 | // we obviously take care of sandbox commands 82 | Main.getEventBaton().releaseBaton('processSandboxCommand', this.processSandboxCommand, this); 83 | // a few things to help transition between levels and sandbox 84 | Main.getEventBaton().releaseBaton('levelExited', this.levelExited, this); 85 | 86 | this.releaseGitShim(); 87 | }, 88 | 89 | releaseGitShim: function() { 90 | if (this.gitShim) { 91 | this.gitShim.removeShim(); 92 | } 93 | }, 94 | 95 | insertGitShim: function() { 96 | // and our git shim goes in after the git engine is ready so it doesn't steal the baton 97 | // too early 98 | if (this.gitShim) { 99 | this.mainVis.customEvents.on('gitEngineReady', function() { 100 | this.gitShim.insertShim(); 101 | },this); 102 | } 103 | }, 104 | 105 | commandSubmitted: function(value) { 106 | // allow other things to see this command (aka command history on terminal) 107 | Main.getEvents().trigger('commandSubmittedPassive', value); 108 | 109 | util.splitTextCommand(value, function(command) { 110 | this.commandCollection.add(new Command({ 111 | rawStr: command, 112 | parseWaterfall: this.parseWaterfall 113 | })); 114 | }, this); 115 | }, 116 | 117 | startLevel: function(command, deferred) { 118 | var regexResults = command.get('regexResults') || []; 119 | var desiredID = regexResults[1] || ''; 120 | var levelJSON = Main.getLevelArbiter().getLevel(desiredID); 121 | 122 | // handle the case where that level is not found... 123 | if (!levelJSON) { 124 | command.addWarning( 125 | 'A level for that id "' + desiredID + '" was not found!! Opening up level selection view...' 126 | ); 127 | Main.getEventBaton().trigger('commandSubmitted', 'levels'); 128 | 129 | command.set('status', 'error'); 130 | deferred.resolve(); 131 | return; 132 | } 133 | 134 | // we are good to go!! lets prep a bit visually 135 | this.hide(); 136 | this.clear(); 137 | 138 | // we don't even need a reference to this, 139 | // everything will be handled via event baton :DDDDDDDDD 140 | var whenLevelOpen = Q.defer(); 141 | var Level = require('../level').Level; 142 | 143 | this.currentLevel = new Level({ 144 | level: levelJSON, 145 | deferred: whenLevelOpen, 146 | command: command 147 | }); 148 | 149 | whenLevelOpen.promise.then(function() { 150 | command.finishWith(deferred); 151 | }); 152 | }, 153 | 154 | buildLevel: function(command, deferred) { 155 | this.hide(); 156 | this.clear(); 157 | 158 | var whenBuilderOpen = Q.defer(); 159 | 160 | var LevelBuilder = require('../level/builder').LevelBuilder; 161 | this.levelBuilder = new LevelBuilder({ 162 | deferred: whenBuilderOpen 163 | }); 164 | 165 | whenBuilderOpen.promise.then(function() { 166 | command.finishWith(deferred); 167 | }); 168 | }, 169 | 170 | exitLevel: function(command, deferred) { 171 | command.addWarning( 172 | "You aren't in a level! You are in a sandbox, start a level with `level [id]`" 173 | ); 174 | command.set('status', 'error'); 175 | deferred.resolve(); 176 | }, 177 | 178 | showLevels: function(command, deferred) { 179 | var whenClosed = Q.defer(); 180 | Main.getLevelDropdown().show(whenClosed, command); 181 | whenClosed.promise.done(function() { 182 | command.finishWith(deferred); 183 | }); 184 | }, 185 | 186 | resetSolved: function(command, deferred) { 187 | Main.getLevelArbiter().resetSolvedMap(); 188 | command.addWarning( 189 | "Solved map was reset, you are starting from a clean slate!" 190 | ); 191 | command.finishWith(deferred); 192 | }, 193 | 194 | processSandboxCommand: function(command, deferred) { 195 | // I'm tempted to do camcel case conversion, but there are 196 | // some exceptions to the rule 197 | var commandMap = { 198 | 'reset solved': this.resetSolved, 199 | 'help general': this.helpDialog, 200 | 'help': this.helpDialog, 201 | 'reset': this.reset, 202 | 'delay': this.delay, 203 | 'clear': this.clear, 204 | 'exit level': this.exitLevel, 205 | 'level': this.startLevel, 206 | 'sandbox': this.exitLevel, 207 | 'levels': this.showLevels, 208 | 'mobileAlert': this.mobileAlert, 209 | 'build level': this.buildLevel, 210 | 'export tree': this.exportTree, 211 | 'import tree': this.importTree, 212 | 'import level': this.importLevel 213 | }; 214 | 215 | var method = commandMap[command.get('method')]; 216 | if (!method) { throw new Error('no method for that wut'); } 217 | 218 | method.apply(this, [command, deferred]); 219 | }, 220 | 221 | hide: function() { 222 | this.mainVis.hide(); 223 | }, 224 | 225 | levelExited: function() { 226 | this.show(); 227 | }, 228 | 229 | show: function() { 230 | this.mainVis.show(); 231 | }, 232 | 233 | importTree: function(command, deferred) { 234 | var jsonGrabber = new BuilderViews.MarkdownPresenter({ 235 | previewText: "Paste a tree JSON blob below!", 236 | fillerText: ' ' 237 | }); 238 | jsonGrabber.deferred.promise 239 | .then(_.bind(function(treeJSON) { 240 | try { 241 | this.mainVis.gitEngine.loadTree(JSON.parse(treeJSON)); 242 | } catch(e) { 243 | this.mainVis.reset(); 244 | new MultiView({ 245 | childViews: [{ 246 | type: 'ModalAlert', 247 | options: { 248 | markdowns: [ 249 | '## Error!', 250 | '', 251 | 'Something is wrong with that JSON! Here is the error:', 252 | '', 253 | String(e) 254 | ] 255 | } 256 | }] 257 | }); 258 | } 259 | }, this)) 260 | .fail(function() { }) 261 | .done(function() { 262 | command.finishWith(deferred); 263 | }); 264 | }, 265 | 266 | importLevel: function(command, deferred) { 267 | var jsonGrabber = new BuilderViews.MarkdownPresenter({ 268 | previewText: 'Paste a level JSON blob in here!', 269 | fillerText: ' ' 270 | }); 271 | 272 | jsonGrabber.deferred.promise 273 | .then(_.bind(function(inputText) { 274 | var Level = require('../level').Level; 275 | try { 276 | var levelJSON = JSON.parse(inputText); 277 | var whenLevelOpen = Q.defer(); 278 | this.currentLevel = new Level({ 279 | level: levelJSON, 280 | deferred: whenLevelOpen, 281 | command: command 282 | }); 283 | 284 | whenLevelOpen.promise.then(function() { 285 | command.finishWith(deferred); 286 | }); 287 | } catch(e) { 288 | new MultiView({ 289 | childViews: [{ 290 | type: 'ModalAlert', 291 | options: { 292 | markdowns: [ 293 | '## Error!', 294 | '', 295 | 'Something is wrong with that level JSON, this happened:', 296 | '', 297 | String(e) 298 | ] 299 | } 300 | }] 301 | }); 302 | command.finishWith(deferred); 303 | } 304 | }, this)) 305 | .fail(function() { 306 | command.finishWith(deferred); 307 | }) 308 | .done(); 309 | }, 310 | 311 | exportTree: function(command, deferred) { 312 | var treeJSON = JSON.stringify(this.mainVis.gitEngine.exportTree(), null, 2); 313 | 314 | var showJSON = new MultiView({ 315 | childViews: [{ 316 | type: 'MarkdownPresenter', 317 | options: { 318 | previewText: 'Share this tree with friends! They can load it with "import tree"', 319 | fillerText: treeJSON, 320 | noConfirmCancel: true 321 | } 322 | }] 323 | }); 324 | showJSON.getPromise() 325 | .then(function() { 326 | command.finishWith(deferred); 327 | }) 328 | .done(); 329 | }, 330 | 331 | clear: function(command, deferred) { 332 | Main.getEvents().trigger('clearOldCommands'); 333 | if (command && deferred) { 334 | command.finishWith(deferred); 335 | } 336 | }, 337 | 338 | mobileAlert: function(command, deferred) { 339 | alert("Can't bring up the keyboard on mobile / tablet :( try visiting on desktop! :D"); 340 | command.finishWith(deferred); 341 | }, 342 | 343 | delay: function(command, deferred) { 344 | var amount = parseInt(command.get('regexResults')[1], 10); 345 | setTimeout(function() { 346 | command.finishWith(deferred); 347 | }, amount); 348 | }, 349 | 350 | reset: function(command, deferred) { 351 | this.mainVis.reset(); 352 | 353 | setTimeout(function() { 354 | command.finishWith(deferred); 355 | }, this.mainVis.getAnimationTime()); 356 | }, 357 | 358 | helpDialog: function(command, deferred) { 359 | var helpDialog = new MultiView({ 360 | childViews: require('../dialogs/sandbox').dialog 361 | }); 362 | helpDialog.getPromise().then(_.bind(function() { 363 | // the view has been closed, lets go ahead and resolve our command 364 | command.finishWith(deferred); 365 | }, this)) 366 | .done(); 367 | } 368 | }); 369 | 370 | exports.Sandbox = Sandbox; 371 | 372 | -------------------------------------------------------------------------------- /src/js/level/sandboxCommands.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var util = require('../util'); 3 | 4 | var Errors = require('../util/errors'); 5 | var CommandProcessError = Errors.CommandProcessError; 6 | var GitError = Errors.GitError; 7 | var Warning = Errors.Warning; 8 | var CommandResult = Errors.CommandResult; 9 | 10 | var instantCommands = [ 11 | [/^ls/, function() { 12 | throw new CommandResult({ 13 | msg: "DontWorryAboutFilesInThisDemo.txt" 14 | }); 15 | }], 16 | [/^cd/, function() { 17 | throw new CommandResult({ 18 | msg: "Directory Changed to '/directories/dont/matter/in/this/demo'" 19 | }); 20 | }], 21 | [/^refresh$/, function() { 22 | var events = require('../app').getEvents(); 23 | 24 | events.trigger('refreshTree'); 25 | throw new CommandResult({ 26 | // msg: "Refreshing tree..." 27 | msg: "트리 초기화..." 28 | }); 29 | }], 30 | [/^rollup (\d+)$/, function(bits) { 31 | var events = require('../app').getEvents(); 32 | 33 | // go roll up these commands by joining them with semicolons 34 | events.trigger('rollupCommands', bits[1]); 35 | throw new CommandResult({ 36 | msg: 'Commands combined!' 37 | }); 38 | }], 39 | [/^echo "(.*?)"$|^echo (.*?)$/, function(bits) { 40 | var msg = bits[1] || bits[2]; 41 | throw new CommandResult({ 42 | msg: msg 43 | }); 44 | }] 45 | ]; 46 | 47 | var regexMap = { 48 | 'reset solved': /^reset solved($|\s)/, 49 | 'help': /^help( general)?$|^\?$/, 50 | 'reset': /^reset$/, 51 | 'delay': /^delay (\d+)$/, 52 | 'clear': /^clear($|\s)/, 53 | 'exit level': /^exit level($|\s)/, 54 | 'sandbox': /^sandbox($|\s)/, 55 | 'level': /^level\s?([a-zA-Z0-9]*)/, 56 | 'levels': /^levels($|\s)/, 57 | 'mobileAlert': /^mobile alert($|\s)/, 58 | 'build level': /^build level($|\s)/, 59 | 'export tree': /^export tree$/, 60 | 'import tree': /^import tree$/, 61 | 'import level': /^import level$/ 62 | }; 63 | 64 | exports.instantCommands = instantCommands; 65 | exports.parse = util.genParseCommand(regexMap, 'processSandboxCommand'); 66 | 67 | // optimistically parse some level and level builder commands; we do this 68 | // so you can enter things like "level intro1; show goal" and not 69 | // have it barf. when the 70 | // command fires the event, it will check if there is a listener and if not throw 71 | // an error 72 | 73 | // note: these are getters / setters because the require kills us 74 | exports.getOptimisticLevelParse = function() { 75 | return util.genParseCommand( 76 | require('../level').regexMap, 77 | 'processLevelCommand' 78 | ); 79 | }; 80 | 81 | exports.getOptimisticLevelBuilderParse = function() { 82 | return util.genParseCommand( 83 | require('../level/builder').regexMap, 84 | 'processLevelBuilderCommand' 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/js/models/collections.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Q = require('q'); 3 | // horrible hack to get localStorage Backbone plugin 4 | var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone; 5 | 6 | var Commit = require('../git').Commit; 7 | var Branch = require('../git').Branch; 8 | 9 | var Command = require('../models/commandModel').Command; 10 | var CommandEntry = require('../models/commandModel').CommandEntry; 11 | var TIME = require('../util/constants').TIME; 12 | 13 | var CommitCollection = Backbone.Collection.extend({ 14 | model: Commit 15 | }); 16 | 17 | var CommandCollection = Backbone.Collection.extend({ 18 | model: Command 19 | }); 20 | 21 | var BranchCollection = Backbone.Collection.extend({ 22 | model: Branch 23 | }); 24 | 25 | var CommandEntryCollection = Backbone.Collection.extend({ 26 | model: CommandEntry, 27 | localStorage: (Backbone.LocalStorage) ? new Backbone.LocalStorage('CommandEntries') : null 28 | }); 29 | 30 | var CommandBuffer = Backbone.Model.extend({ 31 | defaults: { 32 | collection: null 33 | }, 34 | 35 | initialize: function(options) { 36 | options.collection.bind('add', this.addCommand, this); 37 | 38 | this.buffer = []; 39 | this.timeout = null; 40 | }, 41 | 42 | addCommand: function(command) { 43 | this.buffer.push(command); 44 | this.touchBuffer(); 45 | }, 46 | 47 | touchBuffer: function() { 48 | // touch buffer just essentially means we just check if our buffer is being 49 | // processed. if it's not, we immediately process the first item 50 | // and then set the timeout. 51 | if (this.timeout) { 52 | // timeout existence implies its being processed 53 | return; 54 | } 55 | this.setTimeout(); 56 | }, 57 | 58 | 59 | setTimeout: function() { 60 | this.timeout = setTimeout(_.bind(function() { 61 | this.sipFromBuffer(); 62 | }, this), TIME.betweenCommandsDelay); 63 | }, 64 | 65 | popAndProcess: function() { 66 | var popped = this.buffer.shift(0); 67 | 68 | // find a command with no error (aka unprocessed) 69 | while (popped.get('error') && this.buffer.length) { 70 | popped = this.buffer.shift(0); 71 | } 72 | if (!popped.get('error')) { 73 | this.processCommand(popped); 74 | } else { 75 | // no more commands to process 76 | this.clear(); 77 | } 78 | }, 79 | 80 | processCommand: function(command) { 81 | command.set('status', 'processing'); 82 | 83 | var deferred = Q.defer(); 84 | deferred.promise.then(_.bind(function() { 85 | this.setTimeout(); 86 | }, this)); 87 | 88 | var eventName = command.get('eventName'); 89 | if (!eventName) { 90 | throw new Error('I need an event to trigger when this guy is parsed and ready'); 91 | } 92 | 93 | var Main = require('../app'); 94 | var eventBaton = Main.getEventBaton(); 95 | 96 | var numListeners = eventBaton.getNumListeners(eventName); 97 | if (!numListeners) { 98 | var Errors = require('../util/errors'); 99 | command.set('error', new Errors.GitError({ 100 | msg: 'That command is valid, but not supported in this current environment!' + 101 | ' Try entering a level or level builder to use that command' 102 | })); 103 | deferred.resolve(); 104 | return; 105 | } 106 | 107 | Main.getEventBaton().trigger(eventName, command, deferred); 108 | }, 109 | 110 | clear: function() { 111 | clearTimeout(this.timeout); 112 | this.timeout = null; 113 | }, 114 | 115 | sipFromBuffer: function() { 116 | if (!this.buffer.length) { 117 | this.clear(); 118 | return; 119 | } 120 | 121 | this.popAndProcess(); 122 | } 123 | }); 124 | 125 | exports.CommitCollection = CommitCollection; 126 | exports.CommandCollection = CommandCollection; 127 | exports.BranchCollection = BranchCollection; 128 | exports.CommandEntryCollection = CommandEntryCollection; 129 | exports.CommandBuffer = CommandBuffer; 130 | 131 | -------------------------------------------------------------------------------- /src/js/models/commandModel.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | // horrible hack to get localStorage Backbone plugin 3 | var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone; 4 | 5 | var Errors = require('../util/errors'); 6 | var GitCommands = require('../git/commands'); 7 | var GitOptionParser = GitCommands.GitOptionParser; 8 | 9 | var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall; 10 | 11 | var CommandProcessError = Errors.CommandProcessError; 12 | var GitError = Errors.GitError; 13 | var Warning = Errors.Warning; 14 | var CommandResult = Errors.CommandResult; 15 | 16 | var Command = Backbone.Model.extend({ 17 | defaults: { 18 | status: 'inqueue', 19 | rawStr: null, 20 | result: '', 21 | createTime: null, 22 | 23 | error: null, 24 | warnings: null, 25 | parseWaterfall: new ParseWaterfall(), 26 | 27 | generalArgs: null, 28 | supportedMap: null, 29 | options: null, 30 | method: null 31 | 32 | }, 33 | 34 | initialize: function(options) { 35 | this.initDefaults(); 36 | this.validateAtInit(); 37 | 38 | this.on('change:error', this.errorChanged, this); 39 | // catch errors on init 40 | if (this.get('error')) { 41 | this.errorChanged(); 42 | } 43 | 44 | this.parseOrCatch(); 45 | }, 46 | 47 | initDefaults: function() { 48 | // weird things happen with defaults if you dont 49 | // make new objects 50 | this.set('generalArgs', []); 51 | this.set('supportedMap', {}); 52 | this.set('warnings', []); 53 | }, 54 | 55 | validateAtInit: function() { 56 | if (this.get('rawStr') === null) { 57 | throw new Error('Give me a string!'); 58 | } 59 | if (!this.get('createTime')) { 60 | this.set('createTime', new Date().toString()); 61 | } 62 | }, 63 | 64 | setResult: function(msg) { 65 | this.set('result', msg); 66 | }, 67 | 68 | finishWith: function(deferred) { 69 | this.set('status', 'finished'); 70 | deferred.resolve(); 71 | }, 72 | 73 | addWarning: function(msg) { 74 | this.get('warnings').push(msg); 75 | // change numWarnings so the change event fires. This is bizarre -- Backbone can't 76 | // detect if an array changes, so adding an element does nothing 77 | this.set('numWarnings', this.get('numWarnings') ? this.get('numWarnings') + 1 : 1); 78 | }, 79 | 80 | getFormattedWarnings: function() { 81 | if (!this.get('warnings').length) { 82 | return ''; 83 | } 84 | var i = ''; 85 | return '

' + i + this.get('warnings').join('

' + i) + '

'; 86 | }, 87 | 88 | parseOrCatch: function() { 89 | this.expandShortcuts(this.get('rawStr')); 90 | try { 91 | this.processInstants(); 92 | } catch (err) { 93 | Errors.filterError(err); 94 | // errorChanged() will handle status and all of that 95 | this.set('error', err); 96 | return; 97 | } 98 | 99 | if (this.parseAll()) { 100 | // something in our parse waterfall succeeded 101 | return; 102 | } 103 | 104 | // if we reach here, this command is not supported :-/ 105 | this.set('error', new CommandProcessError({ 106 | // msg: 'The command "' + this.get('rawStr') + '" isn\'t supported, sorry!' 107 | msg: '명령어 "' + this.get('rawStr') + '"는 지원되지 않습니다. 죄송!' 108 | }) 109 | ); 110 | }, 111 | 112 | errorChanged: function() { 113 | var err = this.get('error'); 114 | if (err instanceof CommandProcessError || 115 | err instanceof GitError) { 116 | this.set('status', 'error'); 117 | } else if (err instanceof CommandResult) { 118 | this.set('status', 'finished'); 119 | } else if (err instanceof Warning) { 120 | this.set('status', 'warning'); 121 | } 122 | this.formatError(); 123 | }, 124 | 125 | formatError: function() { 126 | this.set('result', this.get('error').toResult()); 127 | }, 128 | 129 | expandShortcuts: function(str) { 130 | str = this.get('parseWaterfall').expandAllShortcuts(str); 131 | this.set('rawStr', str); 132 | }, 133 | 134 | processInstants: function() { 135 | var str = this.get('rawStr'); 136 | // first if the string is empty, they just want a blank line 137 | if (!str.length) { 138 | throw new CommandResult({msg: ""}); 139 | } 140 | 141 | // then instant commands that will throw 142 | this.get('parseWaterfall').processAllInstants(str); 143 | }, 144 | 145 | parseAll: function() { 146 | var str = this.get('rawStr'); 147 | var results = this.get('parseWaterfall').parseAll(str); 148 | 149 | if (!results) { 150 | // nothing parsed successfully 151 | return false; 152 | } 153 | 154 | _.each(results.toSet, function(obj, key) { 155 | // data comes back from the parsing functions like 156 | // options (etc) that need to be set 157 | this.set(key, obj); 158 | }, this); 159 | return true; 160 | } 161 | }); 162 | 163 | // command entry is for the commandview 164 | var CommandEntry = Backbone.Model.extend({ 165 | defaults: { 166 | text: '' 167 | } 168 | }); 169 | 170 | exports.CommandEntry = CommandEntry; 171 | exports.Command = Command; 172 | -------------------------------------------------------------------------------- /src/js/util/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants....!!! 3 | */ 4 | var TIME = { 5 | betweenCommandsDelay: 400 6 | }; 7 | 8 | // useful for locks, etc 9 | var GLOBAL = { 10 | isAnimating: false 11 | }; 12 | 13 | var VIEWPORT = { 14 | minZoom: 0.55, 15 | maxZoom: 1.25, 16 | minWidth: 600, 17 | minHeight: 600 18 | }; 19 | 20 | var GRAPHICS = { 21 | arrowHeadSize: 8, 22 | 23 | nodeRadius: 17, 24 | curveControlPointOffset: 50, 25 | defaultEasing: 'easeInOut', 26 | defaultAnimationTime: 400, 27 | 28 | //rectFill: '#FF3A3A', 29 | rectFill: 'hsb(0.8816909813322127,0.7,1)', 30 | headRectFill: '#2831FF', 31 | rectStroke: '#FFF', 32 | rectStrokeWidth: '3', 33 | 34 | multiBranchY: 20, 35 | upstreamHeadOpacity: 0.5, 36 | upstreamNoneOpacity: 0.2, 37 | edgeUpstreamHeadOpacity: 0.4, 38 | edgeUpstreamNoneOpacity: 0.15, 39 | 40 | visBranchStrokeWidth: 2, 41 | visBranchStrokeColorNone: '#333', 42 | 43 | defaultNodeFill: 'hsba(0.5,0.8,0.7,1)', 44 | defaultNodeStrokeWidth: 2, 45 | defaultNodeStroke: '#FFF', 46 | 47 | orphanNodeFill: 'hsb(0.5,0.8,0.7)' 48 | }; 49 | 50 | exports.GLOBAL = GLOBAL; 51 | exports.TIME = TIME; 52 | exports.GRAPHICS = GRAPHICS; 53 | exports.VIEWPORT = VIEWPORT; 54 | 55 | -------------------------------------------------------------------------------- /src/js/util/debug.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | var toGlobalize = { 4 | Tree: require('../visuals/tree'), 5 | Visuals: require('../visuals'), 6 | Git: require('../git'), 7 | CommandModel: require('../models/commandModel'), 8 | Levels: require('../git/treeCompare'), 9 | Constants: require('../util/constants'), 10 | Collections: require('../models/collections'), 11 | Async: require('../visuals/animation'), 12 | AnimationFactory: require('../visuals/animation/animationFactory'), 13 | Main: require('../app'), 14 | HeadLess: require('../git/headless'), 15 | Q: { Q: require('q') }, 16 | RebaseView: require('../views/rebaseView'), 17 | Views: require('../views'), 18 | MultiView: require('../views/multiView'), 19 | ZoomLevel: require('../util/zoomLevel'), 20 | VisBranch: require('../visuals/visBranch'), 21 | Level: require('../level'), 22 | Sandbox: require('../level/sandbox'), 23 | GitDemonstrationView: require('../views/gitDemonstrationView'), 24 | Markdown: require('markdown'), 25 | LevelDropdownView: require('../views/levelDropdownView'), 26 | BuilderViews: require('../views/builderViews') 27 | }; 28 | 29 | _.each(toGlobalize, function(module) { 30 | _.extend(window, module); 31 | }); 32 | 33 | $(document).ready(function() { 34 | window.events = toGlobalize.Main.getEvents(); 35 | window.eventBaton = toGlobalize.Main.getEventBaton(); 36 | window.sandbox = toGlobalize.Main.getSandbox(); 37 | window.modules = toGlobalize; 38 | window.levelDropdown = toGlobalize.Main.getLevelDropdown(); 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /src/js/util/errors.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | 4 | var MyError = Backbone.Model.extend({ 5 | defaults: { 6 | type: 'MyError', 7 | msg: 'Unknown Error' 8 | }, 9 | toString: function() { 10 | return this.get('type') + ': ' + this.get('msg'); 11 | }, 12 | 13 | getMsg: function() { 14 | return this.get('msg') || 'Unknown Error'; 15 | }, 16 | 17 | toResult: function() { 18 | if (!this.get('msg').length) { 19 | return ''; 20 | } 21 | return '

' + this.get('msg').replace(/\n/g, '

') + '

'; 22 | } 23 | }); 24 | 25 | var CommandProcessError = exports.CommandProcessError = MyError.extend({ 26 | defaults: { 27 | type: 'Command Process Error' 28 | } 29 | }); 30 | 31 | var CommandResult = exports.CommandResult = MyError.extend({ 32 | defaults: { 33 | type: 'Command Result' 34 | } 35 | }); 36 | 37 | var Warning = exports.Warning = MyError.extend({ 38 | defaults: { 39 | type: 'Warning' 40 | } 41 | }); 42 | 43 | var GitError = exports.GitError = MyError.extend({ 44 | defaults: { 45 | type: 'Git Error' 46 | } 47 | }); 48 | 49 | var filterError = function(err) { 50 | if (err instanceof CommandProcessError || 51 | err instanceof GitError || 52 | err instanceof CommandResult || 53 | err instanceof Warning) { 54 | // yay! one of ours 55 | return; 56 | } else { 57 | throw err; 58 | } 59 | }; 60 | 61 | exports.filterError = filterError; 62 | -------------------------------------------------------------------------------- /src/js/util/eventBaton.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | function EventBaton() { 4 | this.eventMap = {}; 5 | } 6 | 7 | // this method steals the "baton" -- aka, only this method will now 8 | // get called. analogous to events.on 9 | // EventBaton.prototype.on = function(name, func, context) { 10 | EventBaton.prototype.stealBaton = function(name, func, context) { 11 | if (!name) { throw new Error('need name'); } 12 | if (!func) { throw new Error('need func!'); } 13 | 14 | var listeners = this.eventMap[name] || []; 15 | listeners.push({ 16 | func: func, 17 | context: context 18 | }); 19 | this.eventMap[name] = listeners; 20 | }; 21 | 22 | EventBaton.prototype.sliceOffArgs = function(num, args) { 23 | var newArgs = []; 24 | for (var i = num; i < args.length; i++) { 25 | newArgs.push(args[i]); 26 | } 27 | return newArgs; 28 | }; 29 | 30 | EventBaton.prototype.trigger = function(name) { 31 | // arguments is weird and doesnt do slice right 32 | var argsToApply = this.sliceOffArgs(1, arguments); 33 | 34 | var listeners = this.eventMap[name]; 35 | if (!listeners || !listeners.length) { 36 | console.warn('no listeners for', name); 37 | return; 38 | } 39 | 40 | // call the top most listener with context and such 41 | var toCall = listeners.slice(-1)[0]; 42 | toCall.func.apply(toCall.context, argsToApply); 43 | }; 44 | 45 | EventBaton.prototype.getNumListeners = function(name) { 46 | var listeners = this.eventMap[name] || []; 47 | return listeners.length; 48 | }; 49 | 50 | EventBaton.prototype.getListenersThrow = function(name) { 51 | var listeners = this.eventMap[name]; 52 | if (!listeners || !listeners.length) { 53 | throw new Error('no one has that baton!' + name); 54 | } 55 | return listeners; 56 | }; 57 | 58 | EventBaton.prototype.passBatonBackSoft = function(name, func, context, args) { 59 | try { 60 | return this.passBatonBack(name, func, context, args); 61 | } catch (e) { 62 | } 63 | }; 64 | 65 | EventBaton.prototype.passBatonBack = function(name, func, context, args) { 66 | // this method will call the listener BEFORE the name/func pair. this 67 | // basically allows you to put in shims, where you steal batons but pass 68 | // them back if they don't meet certain conditions 69 | var listeners = this.getListenersThrow(name); 70 | 71 | var indexBefore; 72 | _.each(listeners, function(listenerObj, index) { 73 | // skip the first 74 | if (index === 0) { return; } 75 | if (listenerObj.func === func && listenerObj.context === context) { 76 | indexBefore = index - 1; 77 | } 78 | }, this); 79 | if (indexBefore === undefined) { 80 | throw new Error('you are the last baton holder! or i didnt find you'); 81 | } 82 | var toCallObj = listeners[indexBefore]; 83 | 84 | toCallObj.func.apply(toCallObj.context, args); 85 | }; 86 | 87 | EventBaton.prototype.releaseBaton = function(name, func, context) { 88 | // might be in the middle of the stack, so we have to loop instead of 89 | // just popping blindly 90 | var listeners = this.getListenersThrow(name); 91 | 92 | var newListeners = []; 93 | var found = false; 94 | _.each(listeners, function(listenerObj) { 95 | if (listenerObj.func === func && listenerObj.context === context) { 96 | if (found) { 97 | console.warn('woah duplicates!!!'); 98 | console.log(listeners); 99 | } 100 | found = true; 101 | } else { 102 | newListeners.push(listenerObj); 103 | } 104 | }, this); 105 | 106 | if (!found) { 107 | console.log('did not find that function', func, context, name, arguments); 108 | console.log(this.eventMap); 109 | throw new Error('cant releasebaton if yu dont have it'); 110 | } 111 | this.eventMap[name] = newListeners; 112 | }; 113 | 114 | exports.EventBaton = EventBaton; 115 | 116 | -------------------------------------------------------------------------------- /src/js/util/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | exports.isBrowser = function() { 4 | var inBrowser = String(typeof window) !== 'undefined'; 5 | return inBrowser; 6 | }; 7 | 8 | exports.splitTextCommand = function(value, func, context) { 9 | func = _.bind(func, context); 10 | _.each(value.split(';'), function(command, index) { 11 | command = _.escape(command); 12 | command = command 13 | .replace(/^(\s+)/, '') 14 | .replace(/(\s+)$/, '') 15 | .replace(/"/g, '"') 16 | .replace(/'/g, "'"); 17 | 18 | if (index > 0 && !command.length) { 19 | return; 20 | } 21 | func(command); 22 | }); 23 | }; 24 | 25 | exports.genParseCommand = function(regexMap, eventName) { 26 | return function(str) { 27 | var method; 28 | var regexResults; 29 | 30 | _.each(regexMap, function(regex, _method) { 31 | var results = regex.exec(str); 32 | if (results) { 33 | method = _method; 34 | regexResults = results; 35 | } 36 | }); 37 | 38 | return (!method) ? false : { 39 | toSet: { 40 | eventName: eventName, 41 | method: method, 42 | regexResults: regexResults 43 | } 44 | }; 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/js/util/keyboard.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | 4 | var Main = require('../app'); 5 | 6 | var mapKeycodeToKey = function(keycode) { 7 | // HELP WANTED -- internationalize? Dvorak? I have no idea 8 | var keyMap = { 9 | 37: 'left', 10 | 38: 'up', 11 | 39: 'right', 12 | 40: 'down', 13 | 27: 'esc', 14 | 13: 'enter' 15 | }; 16 | return keyMap[keycode]; 17 | }; 18 | 19 | function KeyboardListener(options) { 20 | this.events = options.events || _.clone(Backbone.Events); 21 | this.aliasMap = options.aliasMap || {}; 22 | 23 | if (!options.wait) { 24 | this.listen(); 25 | } 26 | } 27 | 28 | KeyboardListener.prototype.listen = function() { 29 | if (this.listening) { 30 | return; 31 | } 32 | this.listening = true; 33 | Main.getEventBaton().stealBaton('docKeydown', this.keydown, this); 34 | }; 35 | 36 | KeyboardListener.prototype.mute = function() { 37 | this.listening = false; 38 | Main.getEventBaton().releaseBaton('docKeydown', this.keydown, this); 39 | }; 40 | 41 | KeyboardListener.prototype.keydown = function(e) { 42 | var which = e.which || e.keyCode; 43 | 44 | var key = mapKeycodeToKey(which); 45 | if (key === undefined) { 46 | return; 47 | } 48 | 49 | this.fireEvent(key, e); 50 | }; 51 | 52 | KeyboardListener.prototype.fireEvent = function(eventName, e) { 53 | eventName = this.aliasMap[eventName] || eventName; 54 | this.events.trigger(eventName, e); 55 | }; 56 | 57 | KeyboardListener.prototype.passEventBack = function(e) { 58 | Main.getEventBaton().passBatonBackSoft('docKeydown', this.keydown, this, [e]); 59 | }; 60 | 61 | exports.KeyboardListener = KeyboardListener; 62 | exports.mapKeycodeToKey = mapKeycodeToKey; 63 | 64 | -------------------------------------------------------------------------------- /src/js/util/mock.js: -------------------------------------------------------------------------------- 1 | exports.mock = function(Constructor) { 2 | var dummy = {}; 3 | var stub = function() {}; 4 | 5 | for (var key in Constructor.prototype) { 6 | dummy[key] = stub; 7 | } 8 | return dummy; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/js/util/zoomLevel.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | var warnOnce = true; 4 | 5 | function detectZoom() { 6 | /** 7 | * Note: this method has only been tested on Chrome 8 | * but seems to work. A much more elaborate library is available here: 9 | * https://github.com/yonran/detect-zoom 10 | * but seems to return a "2" zoom level for my computer (who knows) 11 | * so I can't use it. The ecosystem for zoom level detection is a mess 12 | */ 13 | if (!window.outerWidth || !window.innerWidth) { 14 | if (warnOnce) { 15 | console.warn("Can't detect zoom level correctly :-/"); 16 | warnOnce = false; 17 | } 18 | return 1; 19 | } 20 | 21 | return window.outerWidth / window.innerWidth; 22 | } 23 | 24 | var locked = true; 25 | var setupZoomPoll = function(callback, context) { 26 | var currentZoom = 0; 27 | 28 | setInterval(function() { 29 | var newZoom = detectZoom(); 30 | 31 | if (newZoom !== currentZoom) { 32 | // we need to wait one more before issuing callback 33 | // to avoid window resize issues 34 | if (locked) { 35 | locked = false; 36 | return; 37 | } 38 | 39 | currentZoom = newZoom; 40 | callback.apply(context, [newZoom]); 41 | } else { 42 | locked = true; 43 | } 44 | }, 500); 45 | }; 46 | 47 | exports.setupZoomPoll = setupZoomPoll; 48 | exports.detectZoom = detectZoom; 49 | 50 | -------------------------------------------------------------------------------- /src/js/views/gitDemonstrationView.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Q = require('q'); 3 | // horrible hack to get localStorage Backbone plugin 4 | var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone; 5 | 6 | var util = require('../util'); 7 | var KeyboardListener = require('../util/keyboard').KeyboardListener; 8 | var Command = require('../models/commandModel').Command; 9 | 10 | var ModalTerminal = require('../views').ModalTerminal; 11 | var ContainedBase = require('../views').ContainedBase; 12 | 13 | var Visualization = require('../visuals/visualization').Visualization; 14 | 15 | var GitDemonstrationView = ContainedBase.extend({ 16 | tagName: 'div', 17 | className: 'gitDemonstrationView box horizontal', 18 | template: _.template($('#git-demonstration-view').html()), 19 | 20 | events: { 21 | 'click div.command > p.uiButton': 'positive' 22 | }, 23 | 24 | initialize: function(options) { 25 | options = options || {}; 26 | this.options = options; 27 | this.JSON = _.extend( 28 | { 29 | beforeMarkdowns: [ 30 | '## Git Commits', 31 | '', 32 | 'Awesome!' 33 | ], 34 | command: 'git commit', 35 | afterMarkdowns: [ 36 | 'Now you have seen it in action', 37 | '', 38 | 'Go ahead and try the level!' 39 | ] 40 | }, 41 | options 42 | ); 43 | 44 | var convert = function(markdowns) { 45 | return require('markdown').markdown.toHTML(markdowns.join('\n')); 46 | }; 47 | 48 | this.JSON.beforeHTML = convert(this.JSON.beforeMarkdowns); 49 | this.JSON.afterHTML = convert(this.JSON.afterMarkdowns); 50 | 51 | this.container = new ModalTerminal({ 52 | title: options.title || 'Git 데모'//'Git Demonstration' 53 | }); 54 | this.render(); 55 | this.checkScroll(); 56 | 57 | this.navEvents = _.clone(Backbone.Events); 58 | this.navEvents.on('positive', this.positive, this); 59 | this.navEvents.on('negative', this.negative, this); 60 | this.keyboardListener = new KeyboardListener({ 61 | events: this.navEvents, 62 | aliasMap: { 63 | enter: 'positive', 64 | right: 'positive', 65 | left: 'negative' 66 | }, 67 | wait: true 68 | }); 69 | 70 | this.visFinished = false; 71 | this.initVis(); 72 | 73 | if (!options.wait) { 74 | this.show(); 75 | } 76 | }, 77 | 78 | receiveMetaNav: function(navView, metaContainerView) { 79 | var _this = this; 80 | navView.navEvents.on('positive', this.positive, this); 81 | this.metaContainerView = metaContainerView; 82 | }, 83 | 84 | checkScroll: function() { 85 | var children = this.$('div.demonstrationText').children(); 86 | var heights = _.map(children, function(child) { return child.clientHeight; }); 87 | var totalHeight = _.reduce(heights, function(a, b) { return a + b; }); 88 | if (totalHeight < this.$('div.demonstrationText').height()) { 89 | this.$('div.demonstrationText').addClass('noLongText'); 90 | } 91 | }, 92 | 93 | dispatchBeforeCommand: function() { 94 | if (!this.options.beforeCommand) { 95 | return; 96 | } 97 | 98 | // here we just split the command and push them through to the git engine 99 | util.splitTextCommand(this.options.beforeCommand, function(commandStr) { 100 | this.mainVis.gitEngine.dispatch(new Command({ 101 | rawStr: commandStr 102 | }), Q.defer()); 103 | }, this); 104 | // then harsh refresh 105 | this.mainVis.gitVisuals.refreshTreeHarsh(); 106 | }, 107 | 108 | takeControl: function() { 109 | this.hasControl = true; 110 | this.keyboardListener.listen(); 111 | 112 | if (this.metaContainerView) { this.metaContainerView.lock(); } 113 | }, 114 | 115 | releaseControl: function() { 116 | if (!this.hasControl) { return; } 117 | this.hasControl = false; 118 | this.keyboardListener.mute(); 119 | 120 | if (this.metaContainerView) { this.metaContainerView.unlock(); } 121 | }, 122 | 123 | reset: function() { 124 | this.mainVis.reset(); 125 | this.dispatchBeforeCommand(); 126 | this.demonstrated = false; 127 | this.$el.toggleClass('demonstrated', false); 128 | this.$el.toggleClass('demonstrating', false); 129 | }, 130 | 131 | positive: function() { 132 | if (this.demonstrated || !this.hasControl) { 133 | // dont do anything if we are demonstrating, and if 134 | // we receive a meta nav event and we aren't listening, 135 | // then dont do anything either 136 | return; 137 | } 138 | this.demonstrated = true; 139 | this.demonstrate(); 140 | }, 141 | 142 | demonstrate: function() { 143 | this.$el.toggleClass('demonstrating', true); 144 | 145 | var whenDone = Q.defer(); 146 | this.dispatchCommand(this.JSON.command, whenDone); 147 | whenDone.promise.then(_.bind(function() { 148 | this.$el.toggleClass('demonstrating', false); 149 | this.$el.toggleClass('demonstrated', true); 150 | this.releaseControl(); 151 | }, this)); 152 | }, 153 | 154 | negative: function(e) { 155 | if (this.$el.hasClass('demonstrating')) { 156 | return; 157 | } 158 | this.keyboardListener.passEventBack(e); 159 | }, 160 | 161 | dispatchCommand: function(value, whenDone) { 162 | var commands = []; 163 | util.splitTextCommand(value, function(commandStr) { 164 | commands.push(new Command({ 165 | rawStr: commandStr 166 | })); 167 | }, this); 168 | 169 | var chainDeferred = Q.defer(); 170 | var chainPromise = chainDeferred.promise; 171 | 172 | _.each(commands, function(command, index) { 173 | chainPromise = chainPromise.then(_.bind(function() { 174 | var myDefer = Q.defer(); 175 | this.mainVis.gitEngine.dispatch(command, myDefer); 176 | return myDefer.promise; 177 | }, this)); 178 | chainPromise = chainPromise.then(function() { 179 | return Q.delay(300); 180 | }); 181 | }, this); 182 | 183 | chainPromise = chainPromise.then(function() { 184 | whenDone.resolve(); 185 | }); 186 | 187 | chainDeferred.resolve(); 188 | }, 189 | 190 | tearDown: function() { 191 | this.mainVis.tearDown(); 192 | GitDemonstrationView.__super__.tearDown.apply(this); 193 | }, 194 | 195 | hide: function() { 196 | this.releaseControl(); 197 | this.reset(); 198 | if (this.visFinished) { 199 | this.mainVis.setTreeIndex(-1); 200 | this.mainVis.setTreeOpacity(0); 201 | } 202 | 203 | this.shown = false; 204 | GitDemonstrationView.__super__.hide.apply(this); 205 | }, 206 | 207 | show: function() { 208 | this.takeControl(); 209 | if (this.visFinished) { 210 | setTimeout(_.bind(function() { 211 | if (this.shown) { 212 | this.mainVis.setTreeIndex(300); 213 | this.mainVis.showHarsh(); 214 | } 215 | }, this), this.getAnimationTime() * 1); 216 | } 217 | 218 | this.shown = true; 219 | GitDemonstrationView.__super__.show.apply(this); 220 | }, 221 | 222 | die: function() { 223 | if (!this.visFinished) { return; } 224 | 225 | GitDemonstrationView.__super__.die.apply(this); 226 | }, 227 | 228 | initVis: function() { 229 | this.mainVis = new Visualization({ 230 | el: this.$('div.visHolder')[0], 231 | noKeyboardInput: true, 232 | noClick: true, 233 | smallCanvas: true, 234 | zIndex: -1 235 | }); 236 | this.mainVis.customEvents.on('paperReady', _.bind(function() { 237 | this.visFinished = true; 238 | this.dispatchBeforeCommand(); 239 | if (this.shown) { 240 | // show the canvas once its done if we are shown 241 | this.show(); 242 | } 243 | }, this)); 244 | } 245 | }); 246 | 247 | exports.GitDemonstrationView = GitDemonstrationView; 248 | 249 | -------------------------------------------------------------------------------- /src/js/views/levelDropdownView.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Q = require('q'); 3 | // horrible hack to get localStorage Backbone plugin 4 | var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone; 5 | 6 | var util = require('../util'); 7 | var KeyboardListener = require('../util/keyboard').KeyboardListener; 8 | var Main = require('../app'); 9 | 10 | var ModalTerminal = require('../views').ModalTerminal; 11 | var ContainedBase = require('../views').ContainedBase; 12 | var BaseView = require('../views').BaseView; 13 | 14 | var LevelDropdownView = ContainedBase.extend({ 15 | tagName: 'div', 16 | className: 'levelDropdownView box vertical', 17 | template: _.template($('#level-dropdown-view').html()), 18 | 19 | initialize: function(options) { 20 | options = options || {}; 21 | this.JSON = {}; 22 | 23 | this.navEvents = _.clone(Backbone.Events); 24 | this.navEvents.on('clickedID', _.debounce( 25 | _.bind(this.loadLevelID, this), 26 | 300, 27 | true 28 | )); 29 | this.navEvents.on('negative', this.negative, this); 30 | this.navEvents.on('positive', this.positive, this); 31 | this.navEvents.on('left', this.left, this); 32 | this.navEvents.on('right', this.right, this); 33 | this.navEvents.on('up', this.up, this); 34 | this.navEvents.on('down', this.down, this); 35 | 36 | this.keyboardListener = new KeyboardListener({ 37 | events: this.navEvents, 38 | aliasMap: { 39 | esc: 'negative', 40 | enter: 'positive' 41 | }, 42 | wait: true 43 | }); 44 | 45 | this.sequences = Main.getLevelArbiter().getSequences(); 46 | this.sequenceToLevels = Main.getLevelArbiter().getSequenceToLevels(); 47 | 48 | this.container = new ModalTerminal({ 49 | title: '레벨 고르기'//'Select a Level' 50 | }); 51 | this.render(); 52 | this.buildSequences(); 53 | 54 | if (!options.wait) { 55 | this.show(); 56 | } 57 | }, 58 | 59 | positive: function() { 60 | if (!this.selectedID) { 61 | return; 62 | } 63 | this.loadLevelID(this.selectedID); 64 | }, 65 | 66 | left: function() { 67 | if (this.turnOnKeyboardSelection()) { 68 | return; 69 | } 70 | this.leftOrRight(-1); 71 | }, 72 | 73 | leftOrRight: function(delta) { 74 | this.deselectIconByID(this.selectedID); 75 | this.selectedIndex = this.wrapIndex(this.selectedIndex + delta, this.getCurrentSequence()); 76 | this.selectedID = this.getSelectedID(); 77 | this.selectIconByID(this.selectedID); 78 | }, 79 | 80 | right: function() { 81 | if (this.turnOnKeyboardSelection()) { 82 | return; 83 | } 84 | this.leftOrRight(1); 85 | }, 86 | 87 | up: function() { 88 | if (this.turnOnKeyboardSelection()) { 89 | return; 90 | } 91 | this.selectedSequence = this.getPreviousSequence(); 92 | this.downOrUp(); 93 | }, 94 | 95 | down: function() { 96 | if (this.turnOnKeyboardSelection()) { 97 | return; 98 | } 99 | this.selectedSequence = this.getNextSequence(); 100 | this.downOrUp(); 101 | }, 102 | 103 | downOrUp: function() { 104 | this.selectedIndex = this.boundIndex(this.selectedIndex, this.getCurrentSequence()); 105 | this.deselectIconByID(this.selectedID); 106 | this.selectedID = this.getSelectedID(); 107 | this.selectIconByID(this.selectedID); 108 | }, 109 | 110 | turnOnKeyboardSelection: function() { 111 | if (!this.selectedID) { 112 | this.selectFirst(); 113 | return true; 114 | } 115 | return false; 116 | }, 117 | 118 | turnOffKeyboardSelection: function() { 119 | if (!this.selectedID) { return; } 120 | this.deselectIconByID(this.selectedID); 121 | this.selectedID = undefined; 122 | this.selectedIndex = undefined; 123 | this.selectedSequence = undefined; 124 | }, 125 | 126 | wrapIndex: function(index, arr) { 127 | index = (index >= arr.length) ? 0 : index; 128 | index = (index < 0) ? arr.length - 1 : index; 129 | return index; 130 | }, 131 | 132 | boundIndex: function(index, arr) { 133 | index = (index >= arr.length) ? arr.length - 1 : index; 134 | index = (index < 0) ? 0 : index; 135 | return index; 136 | }, 137 | 138 | getNextSequence: function() { 139 | var current = this.getSequenceIndex(this.selectedSequence); 140 | var desired = this.wrapIndex(current + 1, this.sequences); 141 | return this.sequences[desired]; 142 | }, 143 | 144 | getPreviousSequence: function() { 145 | var current = this.getSequenceIndex(this.selectedSequence); 146 | var desired = this.wrapIndex(current - 1, this.sequences); 147 | return this.sequences[desired]; 148 | }, 149 | 150 | getSequenceIndex: function(name) { 151 | var index = this.sequences.indexOf(name); 152 | if (index < 0) { throw new Error('didnt find'); } 153 | return index; 154 | }, 155 | 156 | getIndexForID: function(id) { 157 | return Main.getLevelArbiter().getLevel(id).index; 158 | }, 159 | 160 | selectFirst: function() { 161 | var firstID = this.sequenceToLevels[this.sequences[0]][0].id; 162 | this.selectIconByID(firstID); 163 | this.selectedIndex = 0; 164 | this.selectedSequence = this.sequences[0]; 165 | }, 166 | 167 | getCurrentSequence: function() { 168 | return this.sequenceToLevels[this.selectedSequence]; 169 | }, 170 | 171 | getSelectedID: function() { 172 | return this.sequenceToLevels[this.selectedSequence][this.selectedIndex].id; 173 | }, 174 | 175 | selectIconByID: function(id) { 176 | this.toggleIconSelect(id, true); 177 | }, 178 | 179 | deselectIconByID: function(id) { 180 | this.toggleIconSelect(id, false); 181 | }, 182 | 183 | toggleIconSelect: function(id, value) { 184 | this.selectedID = id; 185 | var selector = '#levelIcon-' + id; 186 | $(selector).toggleClass('selected', value); 187 | }, 188 | 189 | negative: function() { 190 | this.hide(); 191 | }, 192 | 193 | testOption: function(str) { 194 | return this.currentCommand && new RegExp('--' + str).test(this.currentCommand.get('rawStr')); 195 | }, 196 | 197 | show: function(deferred, command) { 198 | this.currentCommand = command; 199 | // doing the update on show will allow us to fade which will be nice 200 | this.updateSolvedStatus(); 201 | 202 | this.showDeferred = deferred; 203 | this.keyboardListener.listen(); 204 | LevelDropdownView.__super__.show.apply(this); 205 | }, 206 | 207 | hide: function() { 208 | if (this.showDeferred) { 209 | this.showDeferred.resolve(); 210 | } 211 | this.showDeferred = undefined; 212 | this.keyboardListener.mute(); 213 | this.turnOffKeyboardSelection(); 214 | 215 | LevelDropdownView.__super__.hide.apply(this); 216 | }, 217 | 218 | loadLevelID: function(id) { 219 | if (!this.testOption('noOutput')) { 220 | Main.getEventBaton().trigger( 221 | 'commandSubmitted', 222 | 'level ' + id 223 | ); 224 | } 225 | this.hide(); 226 | }, 227 | 228 | updateSolvedStatus: function() { 229 | _.each(this.seriesViews, function(view) { 230 | view.updateSolvedStatus(); 231 | }, this); 232 | }, 233 | 234 | buildSequences: function() { 235 | this.seriesViews = []; 236 | _.each(this.sequences, function(sequenceName) { 237 | this.seriesViews.push(new SeriesView({ 238 | destination: this.$el, 239 | name: sequenceName, 240 | navEvents: this.navEvents 241 | })); 242 | }, this); 243 | } 244 | }); 245 | 246 | var SeriesView = BaseView.extend({ 247 | tagName: 'div', 248 | className: 'seriesView box flex1 vertical', 249 | template: _.template($('#series-view').html()), 250 | events: { 251 | 'click div.levelIcon': 'click' 252 | }, 253 | 254 | initialize: function(options) { 255 | this.name = options.name || 'intro'; 256 | this.navEvents = options.navEvents; 257 | this.info = Main.getLevelArbiter().getSequenceInfo(this.name); 258 | this.levels = Main.getLevelArbiter().getLevelsInSequence(this.name); 259 | 260 | this.levelIDs = []; 261 | _.each(this.levels, function(level) { 262 | this.levelIDs.push(level.id); 263 | }, this); 264 | 265 | this.destination = options.destination; 266 | this.JSON = { 267 | displayName: this.info.displayName, 268 | about: this.info.about, 269 | ids: this.levelIDs 270 | }; 271 | 272 | this.render(); 273 | this.updateSolvedStatus(); 274 | }, 275 | 276 | updateSolvedStatus: function() { 277 | // this is a bit hacky, it really should be some nice model 278 | // property changing but it's the 11th hour... 279 | var toLoop = this.$('div.levelIcon').each(function(index, el) { 280 | var id = $(el).attr('data-id'); 281 | $(el).toggleClass('solved', Main.getLevelArbiter().isLevelSolved(id)); 282 | }); 283 | }, 284 | 285 | click: function(ev) { 286 | var element = ev.srcElement || ev.currentTarget; 287 | if (!element) { 288 | console.warn('wut, no id'); return; 289 | } 290 | 291 | var id = $(element).attr('data-id'); 292 | this.navEvents.trigger('clickedID', id); 293 | } 294 | }); 295 | 296 | exports.LevelDropdownView = LevelDropdownView; 297 | 298 | -------------------------------------------------------------------------------- /src/js/views/multiView.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Q = require('q'); 3 | // horrible hack to get localStorage Backbone plugin 4 | var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone; 5 | 6 | var ModalTerminal = require('../views').ModalTerminal; 7 | var ContainedBase = require('../views').ContainedBase; 8 | var ConfirmCancelView = require('../views').ConfirmCancelView; 9 | var LeftRightView = require('../views').LeftRightView; 10 | var ModalAlert = require('../views').ModalAlert; 11 | var GitDemonstrationView = require('../views/gitDemonstrationView').GitDemonstrationView; 12 | 13 | var BuilderViews = require('../views/builderViews'); 14 | var MarkdownPresenter = BuilderViews.MarkdownPresenter; 15 | 16 | var KeyboardListener = require('../util/keyboard').KeyboardListener; 17 | var GitError = require('../util/errors').GitError; 18 | 19 | var MultiView = Backbone.View.extend({ 20 | tagName: 'div', 21 | className: 'multiView', 22 | // ms to debounce the nav functions 23 | navEventDebounce: 550, 24 | deathTime: 700, 25 | 26 | // a simple mapping of what childViews we support 27 | typeToConstructor: { 28 | ModalAlert: ModalAlert, 29 | GitDemonstrationView: GitDemonstrationView, 30 | MarkdownPresenter: MarkdownPresenter 31 | }, 32 | 33 | initialize: function(options) { 34 | options = options || {}; 35 | this.childViewJSONs = options.childViews || [{ 36 | type: 'ModalAlert', 37 | options: { 38 | markdown: 'Woah wtf!!' 39 | } 40 | }, { 41 | type: 'GitDemonstrationView', 42 | options: { 43 | command: 'git checkout -b side; git commit; git commit' 44 | } 45 | }, { 46 | type: 'ModalAlert', 47 | options: { 48 | markdown: 'Im second' 49 | } 50 | }]; 51 | this.deferred = options.deferred || Q.defer(); 52 | 53 | this.childViews = []; 54 | this.currentIndex = 0; 55 | 56 | this.navEvents = _.clone(Backbone.Events); 57 | this.navEvents.on('negative', this.getNegFunc(), this); 58 | this.navEvents.on('positive', this.getPosFunc(), this); 59 | this.navEvents.on('quit', this.finish, this); 60 | 61 | this.keyboardListener = new KeyboardListener({ 62 | events: this.navEvents, 63 | aliasMap: { 64 | left: 'negative', 65 | right: 'positive', 66 | enter: 'positive', 67 | esc: 'quit' 68 | } 69 | }); 70 | 71 | this.render(); 72 | if (!options.wait) { 73 | this.start(); 74 | } 75 | }, 76 | 77 | onWindowFocus: function() { 78 | // nothing here for now... 79 | // TODO -- add a cool glow effect? 80 | }, 81 | 82 | getAnimationTime: function() { 83 | return 700; 84 | }, 85 | 86 | getPromise: function() { 87 | return this.deferred.promise; 88 | }, 89 | 90 | getPosFunc: function() { 91 | return _.debounce(_.bind(function() { 92 | this.navForward(); 93 | }, this), this.navEventDebounce, true); 94 | }, 95 | 96 | getNegFunc: function() { 97 | return _.debounce(_.bind(function() { 98 | this.navBackward(); 99 | }, this), this.navEventDebounce, true); 100 | }, 101 | 102 | lock: function() { 103 | this.locked = true; 104 | }, 105 | 106 | unlock: function() { 107 | this.locked = false; 108 | }, 109 | 110 | navForward: function() { 111 | // we need to prevent nav changes when a git demonstration view hasnt finished 112 | if (this.locked) { return; } 113 | if (this.currentIndex === this.childViews.length - 1) { 114 | this.hideViewIndex(this.currentIndex); 115 | this.finish(); 116 | return; 117 | } 118 | 119 | this.navIndexChange(1); 120 | }, 121 | 122 | navBackward: function() { 123 | if (this.currentIndex === 0) { 124 | return; 125 | } 126 | 127 | this.navIndexChange(-1); 128 | }, 129 | 130 | navIndexChange: function(delta) { 131 | this.hideViewIndex(this.currentIndex); 132 | this.currentIndex += delta; 133 | this.showViewIndex(this.currentIndex); 134 | }, 135 | 136 | hideViewIndex: function(index) { 137 | this.childViews[index].hide(); 138 | }, 139 | 140 | showViewIndex: function(index) { 141 | this.childViews[index].show(); 142 | }, 143 | 144 | finish: function() { 145 | // first we stop listening to keyboard and give that back to UI, which 146 | // other views will take if they need to 147 | this.keyboardListener.mute(); 148 | 149 | _.each(this.childViews, function(childView) { 150 | childView.die(); 151 | }); 152 | 153 | this.deferred.resolve(); 154 | }, 155 | 156 | start: function() { 157 | // steal the window focus baton 158 | this.showViewIndex(this.currentIndex); 159 | }, 160 | 161 | createChildView: function(viewJSON) { 162 | var type = viewJSON.type; 163 | if (!this.typeToConstructor[type]) { 164 | throw new Error('no constructor for type "' + type + '"'); 165 | } 166 | var view = new this.typeToConstructor[type](_.extend( 167 | {}, 168 | viewJSON.options, 169 | { wait: true } 170 | )); 171 | return view; 172 | }, 173 | 174 | addNavToView: function(view, index) { 175 | var leftRight = new LeftRightView({ 176 | events: this.navEvents, 177 | // we want the arrows to be on the same level as the content (not 178 | // beneath), so we go one level up with getDestination() 179 | destination: view.getDestination(), 180 | showLeft: (index !== 0), 181 | lastNav: (index === this.childViewJSONs.length - 1) 182 | }); 183 | if (view.receiveMetaNav) { 184 | view.receiveMetaNav(leftRight, this); 185 | } 186 | }, 187 | 188 | render: function() { 189 | // go through each and render... show the first 190 | _.each(this.childViewJSONs, function(childViewJSON, index) { 191 | var childView = this.createChildView(childViewJSON); 192 | this.childViews.push(childView); 193 | this.addNavToView(childView, index); 194 | }, this); 195 | } 196 | }); 197 | 198 | exports.MultiView = MultiView; 199 | 200 | -------------------------------------------------------------------------------- /src/js/views/rebaseView.js: -------------------------------------------------------------------------------- 1 | var GitError = require('../util/errors').GitError; 2 | var _ = require('underscore'); 3 | var Q = require('q'); 4 | // horrible hack to get localStorage Backbone plugin 5 | var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone; 6 | 7 | var ModalTerminal = require('../views').ModalTerminal; 8 | var ContainedBase = require('../views').ContainedBase; 9 | var ConfirmCancelView = require('../views').ConfirmCancelView; 10 | var LeftRightView = require('../views').LeftRightView; 11 | 12 | var InteractiveRebaseView = ContainedBase.extend({ 13 | tagName: 'div', 14 | template: _.template($('#interactive-rebase-template').html()), 15 | 16 | initialize: function(options) { 17 | this.deferred = options.deferred; 18 | this.rebaseMap = {}; 19 | this.entryObjMap = {}; 20 | 21 | this.rebaseEntries = new RebaseEntryCollection(); 22 | options.toRebase.reverse(); 23 | _.each(options.toRebase, function(commit) { 24 | var id = commit.get('id'); 25 | this.rebaseMap[id] = commit; 26 | 27 | // make basic models for each commit 28 | this.entryObjMap[id] = new RebaseEntry({ 29 | id: id 30 | }); 31 | this.rebaseEntries.add(this.entryObjMap[id]); 32 | }, this); 33 | 34 | this.container = new ModalTerminal({ 35 | title: 'Interactive Rebase' 36 | }); 37 | this.render(); 38 | 39 | // show the dialog holder 40 | this.show(); 41 | }, 42 | 43 | confirm: function() { 44 | this.die(); 45 | 46 | // get our ordering 47 | var uiOrder = []; 48 | this.$('ul.rebaseEntries li').each(function(i, obj) { 49 | uiOrder.push(obj.id); 50 | }); 51 | 52 | // now get the real array 53 | var toRebase = []; 54 | _.each(uiOrder, function(id) { 55 | // the model pick check 56 | if (this.entryObjMap[id].get('pick')) { 57 | toRebase.unshift(this.rebaseMap[id]); 58 | } 59 | }, this); 60 | toRebase.reverse(); 61 | 62 | this.deferred.resolve(toRebase); 63 | // garbage collection will get us 64 | this.$el.html(''); 65 | }, 66 | 67 | render: function() { 68 | var json = { 69 | num: _.keys(this.rebaseMap).length 70 | }; 71 | 72 | var destination = this.container.getInsideElement(); 73 | this.$el.html(this.template(json)); 74 | $(destination).append(this.el); 75 | 76 | // also render each entry 77 | var listHolder = this.$('ul.rebaseEntries'); 78 | this.rebaseEntries.each(function(entry) { 79 | new RebaseEntryView({ 80 | el: listHolder, 81 | model: entry 82 | }); 83 | }, this); 84 | 85 | // then make it reorderable.. 86 | listHolder.sortable({ 87 | axis: 'y', 88 | placeholder: 'rebaseEntry transitionOpacity ui-state-highlight', 89 | appendTo: 'parent' 90 | }); 91 | 92 | this.makeButtons(); 93 | }, 94 | 95 | makeButtons: function() { 96 | // control for button 97 | var deferred = Q.defer(); 98 | deferred.promise 99 | .then(_.bind(function() { 100 | this.confirm(); 101 | }, this)) 102 | .fail(_.bind(function() { 103 | // empty array does nothing, just like in git 104 | this.hide(); 105 | this.deferred.resolve([]); 106 | }, this)) 107 | .done(); 108 | 109 | // finally get our buttons 110 | new ConfirmCancelView({ 111 | destination: this.$('.confirmCancel'), 112 | deferred: deferred 113 | }); 114 | } 115 | }); 116 | 117 | var RebaseEntry = Backbone.Model.extend({ 118 | defaults: { 119 | pick: true 120 | }, 121 | 122 | toggle: function() { 123 | this.set('pick', !this.get('pick')); 124 | } 125 | }); 126 | 127 | var RebaseEntryCollection = Backbone.Collection.extend({ 128 | model: RebaseEntry 129 | }); 130 | 131 | var RebaseEntryView = Backbone.View.extend({ 132 | tagName: 'li', 133 | template: _.template($('#interactive-rebase-entry-template').html()), 134 | 135 | toggle: function() { 136 | this.model.toggle(); 137 | 138 | // toggle a class also 139 | this.listEntry.toggleClass('notPicked', !this.model.get('pick')); 140 | }, 141 | 142 | initialize: function(options) { 143 | this.render(); 144 | }, 145 | 146 | render: function() { 147 | var json = this.model.toJSON(); 148 | this.$el.append(this.template(this.model.toJSON())); 149 | 150 | // hacky :( who would have known jquery barfs on ids with %'s and quotes 151 | this.listEntry = this.$el.children(':last'); 152 | 153 | this.listEntry.delegate('#toggleButton', 'click', _.bind(function() { 154 | this.toggle(); 155 | }, this)); 156 | } 157 | }); 158 | 159 | exports.InteractiveRebaseView = InteractiveRebaseView; 160 | -------------------------------------------------------------------------------- /src/js/visuals/animation/animationFactory.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | 4 | var Animation = require('./index').Animation; 5 | var GRAPHICS = require('../../util/constants').GRAPHICS; 6 | 7 | /****************** 8 | * This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time. 9 | * The tricky thing is that when a new commit has to be "born," say in the middle of a rebase 10 | * or something, it must animate out from the parent position to it's birth position. 11 | 12 | * These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree, 13 | * store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots. 14 | * and then essentially animate the entire tree too. 15 | */ 16 | 17 | // essentially a static class 18 | var AnimationFactory = function() { 19 | 20 | }; 21 | 22 | AnimationFactory.prototype.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) { 23 | if (!animationQueue) { 24 | throw new Error("Need animation queue to add closure to!"); 25 | } 26 | 27 | var time = GRAPHICS.defaultAnimationTime * 1.0; 28 | var bounceTime = time * 2; 29 | 30 | // essentially refresh the entire tree, but do a special thing for the commit 31 | var visNode = commit.get('visNode'); 32 | 33 | var animation = function() { 34 | // this takes care of refs and all that jazz, and updates all the positions 35 | gitVisuals.refreshTree(time); 36 | 37 | visNode.setBirth(); 38 | visNode.parentInFront(); 39 | gitVisuals.visBranchesFront(); 40 | 41 | visNode.animateUpdatedPosition(bounceTime, 'bounce'); 42 | visNode.animateOutgoingEdges(time); 43 | }; 44 | 45 | animationQueue.add(new Animation({ 46 | closure: animation, 47 | duration: Math.max(time, bounceTime) 48 | })); 49 | }; 50 | 51 | AnimationFactory.prototype.overrideOpacityDepth2 = function(attr, opacity) { 52 | opacity = (opacity === undefined) ? 1 : opacity; 53 | 54 | var newAttr = {}; 55 | 56 | _.each(attr, function(partObj, partName) { 57 | newAttr[partName] = {}; 58 | _.each(partObj, function(val, key) { 59 | if (key == 'opacity') { 60 | newAttr[partName][key] = opacity; 61 | } else { 62 | newAttr[partName][key] = val; 63 | } 64 | }); 65 | }); 66 | return newAttr; 67 | }; 68 | 69 | AnimationFactory.prototype.overrideOpacityDepth3 = function(snapShot, opacity) { 70 | var newSnap = {}; 71 | 72 | _.each(snapShot, function(visObj, visID) { 73 | newSnap[visID] = this.overrideOpacityDepth2(visObj, opacity); 74 | }, this); 75 | return newSnap; 76 | }; 77 | 78 | AnimationFactory.prototype.genCommitBirthClosureFromSnapshot = function(step, gitVisuals) { 79 | var time = GRAPHICS.defaultAnimationTime * 1.0; 80 | var bounceTime = time * 1.5; 81 | 82 | var visNode = step.newCommit.get('visNode'); 83 | var afterAttrWithOpacity = this.overrideOpacityDepth2(step.afterSnapshot[visNode.getID()]); 84 | var afterSnapWithOpacity = this.overrideOpacityDepth3(step.afterSnapshot); 85 | 86 | var animation = function() { 87 | visNode.setBirthFromSnapshot(step.beforeSnapshot); 88 | visNode.parentInFront(); 89 | gitVisuals.visBranchesFront(); 90 | 91 | visNode.animateToAttr(afterAttrWithOpacity, bounceTime, 'bounce'); 92 | visNode.animateOutgoingEdgesToAttr(afterSnapWithOpacity, bounceTime); 93 | }; 94 | 95 | return animation; 96 | }; 97 | 98 | AnimationFactory.prototype.refreshTree = function(animationQueue, gitVisuals) { 99 | animationQueue.add(new Animation({ 100 | closure: function() { 101 | gitVisuals.refreshTree(); 102 | } 103 | })); 104 | }; 105 | 106 | AnimationFactory.prototype.rebaseAnimation = function(animationQueue, rebaseResponse, 107 | gitEngine, gitVisuals) { 108 | 109 | this.rebaseHighlightPart(animationQueue, rebaseResponse, gitEngine); 110 | this.rebaseBirthPart(animationQueue, rebaseResponse, gitEngine, gitVisuals); 111 | }; 112 | 113 | AnimationFactory.prototype.rebaseHighlightPart = function(animationQueue, rebaseResponse, gitEngine) { 114 | var fullTime = GRAPHICS.defaultAnimationTime * 0.66; 115 | var slowTime = fullTime * 2.0; 116 | 117 | // we want to highlight all the old commits 118 | var oldCommits = rebaseResponse.toRebaseArray; 119 | // we are either highlighting to a visBranch or a visNode 120 | var visBranch = rebaseResponse.destinationBranch.get('visBranch'); 121 | if (!visBranch) { 122 | // in the case where we rebase onto a commit 123 | visBranch = rebaseResponse.destinationBranch.get('visNode'); 124 | } 125 | 126 | _.each(oldCommits, function(oldCommit) { 127 | var visNode = oldCommit.get('visNode'); 128 | animationQueue.add(new Animation({ 129 | closure: function() { 130 | visNode.highlightTo(visBranch, slowTime, 'easeInOut'); 131 | }, 132 | duration: fullTime * 1.5 133 | })); 134 | 135 | }, this); 136 | 137 | this.delay(animationQueue, fullTime * 2); 138 | }; 139 | 140 | AnimationFactory.prototype.rebaseBirthPart = function(animationQueue, rebaseResponse, 141 | gitEngine, gitVisuals) { 142 | var rebaseSteps = rebaseResponse.rebaseSteps; 143 | 144 | var newVisNodes = []; 145 | _.each(rebaseSteps, function(step) { 146 | var visNode = step.newCommit.get('visNode'); 147 | 148 | newVisNodes.push(visNode); 149 | visNode.setOpacity(0); 150 | visNode.setOutgoingEdgesOpacity(0); 151 | }, this); 152 | 153 | var previousVisNodes = []; 154 | _.each(rebaseSteps, function(rebaseStep, index) { 155 | var toOmit = newVisNodes.slice(index + 1); 156 | 157 | var snapshotPart = this.genFromToSnapshotAnimation( 158 | rebaseStep.beforeSnapshot, 159 | rebaseStep.afterSnapshot, 160 | toOmit, 161 | previousVisNodes, 162 | gitVisuals 163 | ); 164 | var birthPart = this.genCommitBirthClosureFromSnapshot(rebaseStep, gitVisuals); 165 | 166 | var animation = function() { 167 | snapshotPart(); 168 | birthPart(); 169 | }; 170 | 171 | animationQueue.add(new Animation({ 172 | closure: animation, 173 | duration: GRAPHICS.defaultAnimationTime * 1.5 174 | })); 175 | 176 | previousVisNodes.push(rebaseStep.newCommit.get('visNode')); 177 | }, this); 178 | 179 | // need to delay to let bouncing finish 180 | this.delay(animationQueue); 181 | 182 | this.refreshTree(animationQueue, gitVisuals); 183 | }; 184 | 185 | AnimationFactory.prototype.delay = function(animationQueue, time) { 186 | time = time || GRAPHICS.defaultAnimationTime; 187 | animationQueue.add(new Animation({ 188 | closure: function() { }, 189 | duration: time 190 | })); 191 | }; 192 | 193 | AnimationFactory.prototype.genSetAllCommitOpacities = function(visNodes, opacity) { 194 | // need to slice for closure 195 | var nodesToAnimate = visNodes.slice(0); 196 | 197 | return function() { 198 | _.each(nodesToAnimate, function(visNode) { 199 | visNode.setOpacity(opacity); 200 | visNode.setOutgoingEdgesOpacity(opacity); 201 | }); 202 | }; 203 | }; 204 | 205 | AnimationFactory.prototype.stripObjectsFromSnapshot = function(snapShot, toOmit) { 206 | var ids = []; 207 | _.each(toOmit, function(obj) { 208 | ids.push(obj.getID()); 209 | }); 210 | 211 | var newSnapshot = {}; 212 | _.each(snapShot, function(val, key) { 213 | if (_.include(ids, key)) { 214 | // omit 215 | return; 216 | } 217 | newSnapshot[key] = val; 218 | }, this); 219 | return newSnapshot; 220 | }; 221 | 222 | AnimationFactory.prototype.genFromToSnapshotAnimation = function( 223 | beforeSnapshot, 224 | afterSnapshot, 225 | commitsToOmit, 226 | commitsToFixOpacity, 227 | gitVisuals) { 228 | 229 | // we want to omit the commit outgoing edges 230 | var toOmit = []; 231 | _.each(commitsToOmit, function(visNode) { 232 | toOmit.push(visNode); 233 | toOmit = toOmit.concat(visNode.get('outgoingEdges')); 234 | }); 235 | 236 | var fixOpacity = function(obj) { 237 | if (!obj) { return; } 238 | _.each(obj, function(attr, partName) { 239 | obj[partName].opacity = 1; 240 | }); 241 | }; 242 | 243 | // HORRIBLE loop to fix opacities all throughout the snapshot 244 | _.each([beforeSnapshot, afterSnapshot], function(snapShot) { 245 | _.each(commitsToFixOpacity, function(visNode) { 246 | fixOpacity(snapShot[visNode.getID()]); 247 | _.each(visNode.get('outgoingEdges'), function(visEdge) { 248 | fixOpacity(snapShot[visEdge.getID()]); 249 | }); 250 | }); 251 | }); 252 | 253 | return function() { 254 | gitVisuals.animateAllFromAttrToAttr(beforeSnapshot, afterSnapshot, toOmit); 255 | }; 256 | }; 257 | 258 | exports.AnimationFactory = AnimationFactory; 259 | 260 | -------------------------------------------------------------------------------- /src/js/visuals/animation/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | var GLOBAL = require('../../util/constants').GLOBAL; 4 | 5 | var Animation = Backbone.Model.extend({ 6 | defaults: { 7 | duration: 300, 8 | closure: null 9 | }, 10 | 11 | validateAtInit: function() { 12 | if (!this.get('closure')) { 13 | throw new Error('give me a closure!'); 14 | } 15 | }, 16 | 17 | initialize: function(options) { 18 | this.validateAtInit(); 19 | }, 20 | 21 | run: function() { 22 | this.get('closure')(); 23 | } 24 | }); 25 | 26 | var AnimationQueue = Backbone.Model.extend({ 27 | defaults: { 28 | animations: null, 29 | index: 0, 30 | callback: null, 31 | defer: false 32 | }, 33 | 34 | initialize: function(options) { 35 | this.set('animations', []); 36 | if (!options.callback) { 37 | console.warn('no callback'); 38 | } 39 | }, 40 | 41 | add: function(animation) { 42 | if (!animation instanceof Animation) { 43 | throw new Error("Need animation not something else"); 44 | } 45 | 46 | this.get('animations').push(animation); 47 | }, 48 | 49 | start: function() { 50 | this.set('index', 0); 51 | 52 | // set the global lock that we are animating 53 | GLOBAL.isAnimating = true; 54 | this.next(); 55 | }, 56 | 57 | finish: function() { 58 | // release lock here 59 | GLOBAL.isAnimating = false; 60 | this.get('callback')(); 61 | }, 62 | 63 | next: function() { 64 | // ok so call the first animation, and then set a timeout to call the next. 65 | // since an animation is defined as taking a specific amount of time, 66 | // we can simply just use timeouts rather than promises / deferreds. 67 | // for graphical displays that require an unknown amount of time, use deferreds 68 | // but not animation queue (see the finishAnimation for that) 69 | var animations = this.get('animations'); 70 | var index = this.get('index'); 71 | if (index >= animations.length) { 72 | this.finish(); 73 | return; 74 | } 75 | 76 | var next = animations[index]; 77 | var duration = next.get('duration'); 78 | 79 | next.run(); 80 | 81 | this.set('index', index + 1); 82 | setTimeout(_.bind(function() { 83 | this.next(); 84 | }, this), duration); 85 | } 86 | }); 87 | 88 | exports.Animation = Animation; 89 | exports.AnimationQueue = AnimationQueue; 90 | -------------------------------------------------------------------------------- /src/js/visuals/tree.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | 4 | var VisBase = Backbone.Model.extend({ 5 | removeKeys: function(keys) { 6 | _.each(keys, function(key) { 7 | if (this.get(key)) { 8 | this.get(key).remove(); 9 | } 10 | }, this); 11 | }, 12 | 13 | animateAttrKeys: function(keys, attrObj, speed, easing) { 14 | // either we animate a specific subset of keys or all 15 | // possible things we could animate 16 | keys = _.extend( 17 | {}, 18 | { 19 | include: ['circle', 'arrow', 'rect', 'path', 'text'], 20 | exclude: [] 21 | }, 22 | keys || {} 23 | ); 24 | 25 | var attr = this.getAttributes(); 26 | 27 | // safely insert this attribute into all the keys we want 28 | _.each(keys.include, function(key) { 29 | attr[key] = _.extend( 30 | {}, 31 | attr[key], 32 | attrObj 33 | ); 34 | }); 35 | 36 | _.each(keys.exclude, function(key) { 37 | delete attr[key]; 38 | }); 39 | 40 | this.animateToAttr(attr, speed, easing); 41 | } 42 | }); 43 | 44 | exports.VisBase = VisBase; 45 | 46 | -------------------------------------------------------------------------------- /src/js/visuals/visBase.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | 4 | var VisBase = Backbone.Model.extend({ 5 | removeKeys: function(keys) { 6 | _.each(keys, function(key) { 7 | if (this.get(key)) { 8 | this.get(key).remove(); 9 | } 10 | }, this); 11 | }, 12 | 13 | animateAttrKeys: function(keys, attrObj, speed, easing) { 14 | // either we animate a specific subset of keys or all 15 | // possible things we could animate 16 | keys = _.extend( 17 | {}, 18 | { 19 | include: ['circle', 'arrow', 'rect', 'path', 'text'], 20 | exclude: [] 21 | }, 22 | keys || {} 23 | ); 24 | 25 | var attr = this.getAttributes(); 26 | 27 | // safely insert this attribute into all the keys we want 28 | _.each(keys.include, function(key) { 29 | attr[key] = _.extend( 30 | {}, 31 | attr[key], 32 | attrObj 33 | ); 34 | }); 35 | 36 | _.each(keys.exclude, function(key) { 37 | delete attr[key]; 38 | }); 39 | 40 | this.animateToAttr(attr, speed, easing); 41 | } 42 | }); 43 | 44 | exports.VisBase = VisBase; 45 | 46 | -------------------------------------------------------------------------------- /src/js/visuals/visEdge.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Backbone = require('backbone'); 3 | var GRAPHICS = require('../util/constants').GRAPHICS; 4 | 5 | var VisBase = require('../visuals/visBase').VisBase; 6 | 7 | var VisEdge = VisBase.extend({ 8 | defaults: { 9 | tail: null, 10 | head: null, 11 | animationSpeed: GRAPHICS.defaultAnimationTime, 12 | animationEasing: GRAPHICS.defaultEasing 13 | }, 14 | 15 | validateAtInit: function() { 16 | var required = ['tail', 'head']; 17 | _.each(required, function(key) { 18 | if (!this.get(key)) { 19 | throw new Error(key + ' is required!'); 20 | } 21 | }, this); 22 | }, 23 | 24 | getID: function() { 25 | return this.get('tail').get('id') + '.' + this.get('head').get('id'); 26 | }, 27 | 28 | initialize: function() { 29 | this.validateAtInit(); 30 | 31 | // shorthand for the main objects 32 | this.gitVisuals = this.get('gitVisuals'); 33 | this.gitEngine = this.get('gitEngine'); 34 | 35 | this.get('tail').get('outgoingEdges').push(this); 36 | }, 37 | 38 | remove: function() { 39 | this.removeKeys(['path']); 40 | this.gitVisuals.removeVisEdge(this); 41 | }, 42 | 43 | genSmoothBezierPathString: function(tail, head) { 44 | var tailPos = tail.getScreenCoords(); 45 | var headPos = head.getScreenCoords(); 46 | return this.genSmoothBezierPathStringFromCoords(tailPos, headPos); 47 | }, 48 | 49 | genSmoothBezierPathStringFromCoords: function(tailPos, headPos) { 50 | // we need to generate the path and control points for the bezier. format 51 | // is M(move abs) C (curve to) (control point 1) (control point 2) (final point) 52 | // the control points have to be __below__ to get the curve starting off straight. 53 | 54 | var coords = function(pos) { 55 | return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); 56 | }; 57 | var offset = function(pos, dir, delta) { 58 | delta = delta || GRAPHICS.curveControlPointOffset; 59 | return { 60 | x: pos.x, 61 | y: pos.y + delta * dir 62 | }; 63 | }; 64 | var offset2d = function(pos, x, y) { 65 | return { 66 | x: pos.x + x, 67 | y: pos.y + y 68 | }; 69 | }; 70 | 71 | // first offset tail and head by radii 72 | tailPos = offset(tailPos, -1, this.get('tail').getRadius()); 73 | headPos = offset(headPos, 1, this.get('head').getRadius()); 74 | 75 | var str = ''; 76 | // first move to bottom of tail 77 | str += 'M' + coords(tailPos) + ' '; 78 | // start bezier 79 | str += 'C'; 80 | // then control points above tail and below head 81 | str += coords(offset(tailPos, -1)) + ' '; 82 | str += coords(offset(headPos, 1)) + ' '; 83 | // now finish 84 | str += coords(headPos); 85 | 86 | // arrow head 87 | var delta = GRAPHICS.arrowHeadSize || 10; 88 | str += ' L' + coords(offset2d(headPos, -delta, delta)); 89 | str += ' L' + coords(offset2d(headPos, delta, delta)); 90 | str += ' L' + coords(headPos); 91 | 92 | // then go back, so we can fill correctly 93 | str += 'C'; 94 | str += coords(offset(headPos, 1)) + ' '; 95 | str += coords(offset(tailPos, -1)) + ' '; 96 | str += coords(tailPos); 97 | 98 | return str; 99 | }, 100 | 101 | getBezierCurve: function() { 102 | return this.genSmoothBezierPathString(this.get('tail'), this.get('head')); 103 | }, 104 | 105 | getStrokeColor: function() { 106 | return GRAPHICS.visBranchStrokeColorNone; 107 | }, 108 | 109 | setOpacity: function(opacity) { 110 | opacity = (opacity === undefined) ? 1 : opacity; 111 | 112 | this.get('path').attr({opacity: opacity}); 113 | }, 114 | 115 | genGraphics: function(paper) { 116 | var pathString = this.getBezierCurve(); 117 | 118 | var path = paper.path(pathString).attr({ 119 | 'stroke-width': GRAPHICS.visBranchStrokeWidth, 120 | 'stroke': this.getStrokeColor(), 121 | 'stroke-linecap': 'round', 122 | 'stroke-linejoin': 'round', 123 | 'fill': this.getStrokeColor() 124 | }); 125 | path.toBack(); 126 | this.set('path', path); 127 | }, 128 | 129 | getOpacity: function() { 130 | var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail')); 131 | var map = { 132 | 'branch': 1, 133 | 'head': GRAPHICS.edgeUpstreamHeadOpacity, 134 | 'none': GRAPHICS.edgeUpstreamNoneOpacity 135 | }; 136 | 137 | if (map[stat] === undefined) { throw new Error('bad stat'); } 138 | return map[stat]; 139 | }, 140 | 141 | getAttributes: function() { 142 | var newPath = this.getBezierCurve(); 143 | var opacity = this.getOpacity(); 144 | return { 145 | path: { 146 | path: newPath, 147 | opacity: opacity 148 | } 149 | }; 150 | }, 151 | 152 | animateUpdatedPath: function(speed, easing) { 153 | var attr = this.getAttributes(); 154 | this.animateToAttr(attr, speed, easing); 155 | }, 156 | 157 | animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { 158 | // an animation of 0 is essentially setting the attribute directly 159 | this.animateToAttr(fromAttr, 0); 160 | this.animateToAttr(toAttr, speed, easing); 161 | }, 162 | 163 | animateToAttr: function(attr, speed, easing) { 164 | if (speed === 0) { 165 | this.get('path').attr(attr.path); 166 | return; 167 | } 168 | 169 | this.get('path').toBack(); 170 | this.get('path').stop().animate( 171 | attr.path, 172 | speed !== undefined ? speed : this.get('animationSpeed'), 173 | easing || this.get('animationEasing') 174 | ); 175 | } 176 | }); 177 | 178 | var VisEdgeCollection = Backbone.Collection.extend({ 179 | model: VisEdge 180 | }); 181 | 182 | exports.VisEdgeCollection = VisEdgeCollection; 183 | exports.VisEdge = VisEdge; 184 | -------------------------------------------------------------------------------- /src/js/visuals/visualization.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | // horrible hack to get localStorage Backbone plugin 3 | var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone; 4 | 5 | var Collections = require('../models/collections'); 6 | var CommitCollection = Collections.CommitCollection; 7 | var BranchCollection = Collections.BranchCollection; 8 | var EventBaton = require('../util/eventBaton').EventBaton; 9 | 10 | var GitVisuals = require('../visuals').GitVisuals; 11 | 12 | var Visualization = Backbone.View.extend({ 13 | initialize: function(options) { 14 | options = options || {}; 15 | this.options = options; 16 | this.customEvents = _.clone(Backbone.Events); 17 | this.containerElement = options.containerElement; 18 | 19 | var _this = this; 20 | // we want to add our canvas somewhere 21 | var container = options.containerElement || $('#canvasHolder')[0]; 22 | new Raphael(container, 200, 200, function() { 23 | // raphael calls with paper as this for some inane reason... 24 | var paper = this; 25 | // use process.nextTick to go from sync to async 26 | process.nextTick(function() { 27 | _this.paperInitialize(paper, options); 28 | }); 29 | }); 30 | }, 31 | 32 | paperInitialize: function(paper, options) { 33 | this.treeString = options.treeString; 34 | this.paper = paper; 35 | 36 | var Main = require('../app'); 37 | // if we dont want to receive keyoard input (directly), 38 | // make a new event baton so git engine steals something that no one 39 | // is broadcasting to 40 | this.eventBaton = (options.noKeyboardInput) ? 41 | new EventBaton(): 42 | Main.getEventBaton(); 43 | 44 | this.commitCollection = new CommitCollection(); 45 | this.branchCollection = new BranchCollection(); 46 | 47 | this.gitVisuals = new GitVisuals({ 48 | commitCollection: this.commitCollection, 49 | branchCollection: this.branchCollection, 50 | paper: this.paper, 51 | noClick: this.options.noClick, 52 | smallCanvas: this.options.smallCanvas 53 | }); 54 | 55 | var GitEngine = require('../git').GitEngine; 56 | this.gitEngine = new GitEngine({ 57 | collection: this.commitCollection, 58 | branches: this.branchCollection, 59 | gitVisuals: this.gitVisuals, 60 | eventBaton: this.eventBaton 61 | }); 62 | this.gitEngine.init(); 63 | this.gitVisuals.assignGitEngine(this.gitEngine); 64 | 65 | this.myResize(); 66 | 67 | $(window).on('resize', _.bind(function() { 68 | this.myResize(); 69 | }, this)); 70 | 71 | this.gitVisuals.drawTreeFirstTime(); 72 | if (this.treeString) { 73 | this.gitEngine.loadTreeFromString(this.treeString); 74 | } 75 | if (this.options.zIndex) { 76 | this.setTreeIndex(this.options.zIndex); 77 | } 78 | 79 | this.shown = false; 80 | this.setTreeOpacity(0); 81 | // reflow needed 82 | process.nextTick(_.bind(this.fadeTreeIn, this)); 83 | 84 | this.customEvents.trigger('gitEngineReady'); 85 | this.customEvents.trigger('paperReady'); 86 | }, 87 | 88 | setTreeIndex: function(level) { 89 | $(this.paper.canvas).css('z-index', level); 90 | }, 91 | 92 | setTreeOpacity: function(level) { 93 | if (level === 0) { 94 | this.shown = false; 95 | } 96 | 97 | $(this.paper.canvas).css('opacity', level); 98 | }, 99 | 100 | getAnimationTime: function() { return 300; }, 101 | 102 | fadeTreeIn: function() { 103 | this.shown = true; 104 | $(this.paper.canvas).animate({opacity: 1}, this.getAnimationTime()); 105 | }, 106 | 107 | fadeTreeOut: function() { 108 | this.shown = false; 109 | $(this.paper.canvas).animate({opacity: 0}, this.getAnimationTime()); 110 | }, 111 | 112 | hide: function() { 113 | this.fadeTreeOut(); 114 | // remove click handlers by toggling visibility 115 | setTimeout(_.bind(function() { 116 | $(this.paper.canvas).css('visibility', 'hidden'); 117 | }, this), this.getAnimationTime()); 118 | }, 119 | 120 | show: function() { 121 | $(this.paper.canvas).css('visibility', 'visible'); 122 | setTimeout(_.bind(this.fadeTreeIn, this), 10); 123 | }, 124 | 125 | showHarsh: function() { 126 | $(this.paper.canvas).css('visibility', 'visible'); 127 | this.setTreeOpacity(1); 128 | }, 129 | 130 | resetFromThisTreeNow: function(treeString) { 131 | this.treeString = treeString; 132 | }, 133 | 134 | reset: function() { 135 | this.setTreeOpacity(0); 136 | if (this.treeString) { 137 | this.gitEngine.loadTreeFromString(this.treeString); 138 | } else { 139 | this.gitEngine.defaultInit(); 140 | } 141 | this.fadeTreeIn(); 142 | }, 143 | 144 | tearDown: function() { 145 | this.gitEngine.tearDown(); 146 | this.gitVisuals.tearDown(); 147 | delete this.paper; 148 | }, 149 | 150 | die: function() { 151 | this.fadeTreeOut(); 152 | setTimeout(_.bind(function() { 153 | if (!this.shown) { 154 | this.tearDown(); 155 | } 156 | }, this), this.getAnimationTime()); 157 | }, 158 | 159 | myResize: function() { 160 | if (!this.paper) { return; } 161 | 162 | var smaller = 1; 163 | var el = this.el; 164 | 165 | var width = el.clientWidth - smaller; 166 | var height = el.clientHeight - smaller; 167 | 168 | // if we don't have a container, we need to set our 169 | // position absolutely to whatever we are tracking 170 | if (!this.containerElement) { 171 | var left = el.offsetLeft; 172 | var top = el.offsetTop; 173 | 174 | $(this.paper.canvas).css({ 175 | position: 'absolute', 176 | left: left + 'px', 177 | top: top + 'px' 178 | }); 179 | } 180 | 181 | this.paper.setSize(width, height); 182 | this.gitVisuals.canvasResize(width, height); 183 | } 184 | }); 185 | 186 | exports.Visualization = Visualization; 187 | 188 | -------------------------------------------------------------------------------- /src/levels/index.js: -------------------------------------------------------------------------------- 1 | // Each level is part of a "sequence;" levels within 2 | // a sequence proceed in the order listed here 3 | exports.levelSequences = { 4 | intro: [ 5 | require('../levels/intro/1').level, 6 | require('../levels/intro/2').level, 7 | require('../levels/intro/3').level, 8 | require('../levels/intro/4').level, 9 | require('../levels/intro/5').level 10 | ], 11 | rebase: [ 12 | require('../levels/rebase/1').level, 13 | require('../levels/rebase/2').level 14 | ], 15 | mixed: [ 16 | require('../levels/mixed/1').level, 17 | require('../levels/mixed/2').level, 18 | require('../levels/mixed/3').level 19 | ] 20 | }; 21 | 22 | // there are also cute names and such for sequences 23 | exports.sequenceInfo = { 24 | intro: { 25 | // displayName: 'Introduction Sequence', 26 | // about: 'A nicely paced introduction to the majority of git commands' 27 | displayName: '기본 명령어', 28 | about: '브랜치 관련 주요 git 명령어를 깔끔하게 알려드립니다' 29 | }, 30 | rebase: { 31 | // displayName: 'Master the Rebase Luke!', 32 | // about: 'What is this whole rebase hotness everyone is talking about? Find out!' 33 | displayName: '리베이스 완전정복!', 34 | about: '그 좋다고들 말하는 rebase에 대해 알아봅시다!' 35 | }, 36 | mixed: { 37 | // displayName: 'A Mixed Bag', 38 | // about: 'A mixed bag of Git techniques, tricks, and tips' 39 | displayName: '종합선물세트', 40 | about: 'Git을 다루는 다양한 팁과 테크닉을 다양하게 알아봅니다' 41 | } 42 | }; -------------------------------------------------------------------------------- /src/levels/intro/1.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "name": 'Git 커밋 소개', 3 | "goalTreeString": "{\"branches\":{\"master\":{\"target\":\"C3\",\"id\":\"master\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C2\"],\"id\":\"C3\"}},\"HEAD\":{\"target\":\"master\",\"id\":\"HEAD\"}}", 4 | "solutionCommand": "git commit;git commit", 5 | "startTree": "{\"branches\":{\"master\":{\"target\":\"C1\",\"id\":\"master\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"}},\"HEAD\":{\"target\":\"master\",\"id\":\"HEAD\"}}", 6 | "hint": "'git commit'이라고 두 번 치세요!", 7 | "disabledMap" : { 8 | "git revert": true 9 | }, 10 | "startDialog": { 11 | "childViews": [ 12 | { 13 | "type": "ModalAlert", 14 | "options": { 15 | "markdowns": [ 16 | "## Git 커밋", 17 | // "A commit in a git repository records a snapshot of all the files in your directory. It\'s like a giant copy and paste, but even better!", 18 | "커밋은 Git 저장소에 여러분의 디렉토리에 있는 모든 파일에 대한 스냅샷을 기록하는 것입니다. 디렉토리 전체에 대한 복사해 붙이기와 비슷하지만 훨씬 유용합니다!", 19 | "", 20 | // "Git wants to keep commits as lightweight as possible though, so it doesn't just copy the entire directory every time you commit. It actually stores each commit as a set of changes, or a \"delta\", from one version of the repository to the next. That\'s why most commits have a parent commit above them -- you\'ll see this later in our visualizations.", 21 | "Git은 커밋을 가능한한 가볍게 유지하고자 해서, 커밋할 때마다 디렉토리 전체를 복사하는 일은 하지 않습니다. 각 커밋은 저장소의 이전 버전과 다음 버전의 변경내역(\"delta\"라고도 함)을 저장합니다. 그래서 대부분의 커밋이 그 커밋 위에 부모 커밋을 가리키고 있게 되는 것입니다. -- 곧 그림으로 된 화면에서 살펴보게 될 것입니다.", 22 | "", 23 | //"In order to clone a repository, you have to unpack or \"resolve\" all these deltas. That's why you might see the command line output:", 24 | "저장소를 복제(clone)하려면, 그 모든 변경분(delta)를 풀어내야하는데, 그 때문에 명령행 결과로 아래와 같이 보게됩니다. ", 25 | "", 26 | "`resolving deltas`", 27 | "", 28 | //"when cloning a repo.", 29 | //"", 30 | //"It's a lot to take in, but for now you can think of commits as snapshots of the project. Commits are very light and switching between them is wicked fast!" 31 | "알아야할 것이 꽤 많습니다만, 일단은 커밋을 프로젝트의 각각의 스냅샷들로 생각하시는 걸로 충분합니다. 커밋은 매우 가볍고 커밋 사이의 전환도 매우 빠르다는 것을 기억해주세요!" 32 | ] 33 | } 34 | }, 35 | { 36 | "type": "GitDemonstrationView", 37 | "options": { 38 | "beforeMarkdowns": [ 39 | // "Let's see what this looks like in practice. On the right we have a visualization of a (small) git repository. There are two commits right now -- the first initial commit, `C0`, and one commit after that `C1` that might have some meaningful changes.", 40 | "연습할 때 어떻게 보이는지 확인해보죠. 오른쪽 화면에 git 저장소를 그림으로 표현해 놓았습니다. 현재 두번 커밋한 상태입니다 -- 첫번째 커밋으로 `C0`, 그 다음으로 `C1`이라는 어떤 의미있는 변화가 있는 커밋이 있습니다.", 41 | "", 42 | // "Hit the button below to make a new commit" 43 | "아래 버튼을 눌러 새로운 커밋을 만들어보세요" 44 | ], 45 | "afterMarkdowns": [ 46 | // "There we go! Awesome. We just made changes to the repository and saved them as a commit. The commit we just made has a parent, `C1`, which references which commit it was based off of." 47 | "이렇게 보입니다! 멋지죠. 우리는 방금 저장소 내용을 변경해서 한번의 커밋으로 저장했습니다. 방금 만든 커밋은 부모는 `C1`이고, 어떤 커밋을 기반으로 변경된 것인지를 가리킵니다." 48 | ], 49 | "command": "git commit", 50 | "beforeCommand": "" 51 | } 52 | }, 53 | { 54 | "type": "ModalAlert", 55 | "options": { 56 | "markdowns": [ 57 | // "Go ahead and try it out on your own! After this window closes, make two commits to complete the level" 58 | "계속해서 직접 한번 해보세요! 이 창을 닫고, 커밋을 두 번 하면 다음 레벨로 넘어갑니다" 59 | ] 60 | } 61 | } 62 | ] 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/levels/intro/2.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "goalTreeString": "{\"branches\":{\"master\":{\"target\":\"C1\",\"id\":\"master\"},\"bugFix\":{\"target\":\"C1\",\"id\":\"bugFix\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"}},\"HEAD\":{\"target\":\"bugFix\",\"id\":\"HEAD\"}}", 3 | "solutionCommand": "git branch bugFix;git checkout bugFix", 4 | // "hint": "Make a new branch with \"git branch [name]\" and check it out with \"git checkout [name]\"", 5 | "hint": "\"git branch [브랜치명]\"으로 새 브랜치를 만들고, \"git checkout [브랜치명]\"로 그 브랜치로 이동하세요", 6 | "name": "Git에서 브랜치 쓰기", 7 | "disabledMap" : { 8 | "git revert": true 9 | }, 10 | "startDialog": { 11 | "childViews": [ 12 | { 13 | "type": "ModalAlert", 14 | "options": { 15 | "markdowns": [ 16 | // "## Git Branches", 17 | "## Git 브랜치", 18 | "", 19 | // "Branches in Git are incredibly lightweight as well. They are simply references to a specific commit -- nothing more. This is why many Git enthusiasts chant the mantra:", 20 | "깃의 브랜치도 놀랍도록 가볍습니다. 브랜치는 특정 커밋에 대한 참조(reference)에 지나지 않습니다. 이런 사실 때문에 수많은 Git 애찬론자들이 자주 이렇게 말하곤 합니다:", 21 | "", 22 | "```", 23 | // "branch early, and branch often", 24 | "브랜치를 서둘러서, 그리고 자주 만드세요", 25 | "```", 26 | "", 27 | // "Because there is no storage / memory overhead with making many branches, it's easier to logically divide up your work than have big beefy branches.", 28 | "브랜치를 많이 만들어도 메모리나 디스크 공간에 부담이 되지 않기 때문에, 여러분의 작업을 커다른 브랜치로 만들기 보다, 작은 단위로 잘게 나누는 것이 좋습니다.", 29 | "", 30 | // "When we start mixing branches and commits, we will see how these two features combine. For now though, just remember that a branch essentially says \"I want to include the work of this commit and all parent commits.\"" 31 | "브랜치와 커밋을 같이 쓸 때, 어떻게 두 기능이 조화를 이루는지 알아보겠습니다. 하지만 우선은, 단순히 브랜치를 \"하나의 커밋과 그 부모 커밋들을 포함하는 작업 내역\"이라고 기억하시면 됩니다." 32 | ] 33 | } 34 | }, 35 | { 36 | "type": "GitDemonstrationView", 37 | "options": { 38 | "beforeMarkdowns": [ 39 | // "Let's see what branches look like in practice.", 40 | "브랜치가 어떤 것인지 연습해보죠.", 41 | "", 42 | // "Here we will check out a new branch named `newImage`" 43 | "`newImage`라는 브랜치를 살펴보겠습니다." 44 | ], 45 | "afterMarkdowns": [ 46 | // "There, that's all there is to branching! The branch `newImage` now refers to commit `C1`" 47 | "저 그림에 브랜치의 모든 것이 담겨있습니다! 브랜치 `newImage`가 커밋 `C1`를 가리킵니다" 48 | ], 49 | "command": "git branch newImage", 50 | "beforeCommand": "" 51 | } 52 | }, 53 | { 54 | "type": "GitDemonstrationView", 55 | "options": { 56 | "beforeMarkdowns": [ 57 | // "Let's try to put some work on this new branch. Hit the button below" 58 | "이 새로운 브랜치에 약간의 작업을 더해봅시다. 아래 버튼을 눌러주세요" 59 | ], 60 | "afterMarkdowns": [ 61 | // "Oh no! The `master` branch moved but the `newImage` branch didn't! That's because we weren't \"on\" the new branch, which is why the asterisk (*) was on `master`" 62 | "앗! `master` 브랜치가 움직이고, `newImage` 브랜치는 이동하지 않았네요! 그건 우리가 새 브랜치 위에 있지 않았었기 때문입니다. 별표(*)가 `master`에 있었던 것이죠." 63 | ], 64 | "command": "git commit", 65 | "beforeCommand": "git branch newImage" 66 | } 67 | }, 68 | { 69 | "type": "GitDemonstrationView", 70 | "options": { 71 | "beforeMarkdowns": [ 72 | // "Let's tell git we want to checkout the branch with", 73 | // "", 74 | // "```", 75 | // "git checkout [name]", 76 | // "```", 77 | // "", 78 | // "This will put us on the new branch before committing our changes" 79 | "아래의 명령으로 새 브랜치로 이동해 봅시다.", 80 | "", 81 | "```", 82 | "git checkout [브랜치명]", 83 | "```", 84 | "", 85 | "이렇게 하면 변경분을 커밋하기 전에 새 브랜치로 이동하게 됩니다." 86 | 87 | ], 88 | "afterMarkdowns": [ 89 | // "There we go! Our changes were recorded on the new branch" 90 | "이거죠! 이제 우리의 변경이 새 브랜치에 기록되었습니다!" 91 | ], 92 | "command": "git checkout newImage; git commit", 93 | "beforeCommand": "git branch newImage" 94 | } 95 | }, 96 | { 97 | "type": "ModalAlert", 98 | "options": { 99 | "markdowns": [ 100 | // "Ok! You are all ready to get branching. Once this window closes,", 101 | // "make a new branch named `bugFix` and switch to that branch" 102 | "좋아요! 이제 직접 브랜치 작업을 연습해봅시다. 이 창을 닫고,", 103 | "`bugFix`라는 새 브랜치를 만드시고, 그 브랜치로 이동해보세요" 104 | ] 105 | } 106 | } 107 | ] 108 | } 109 | }; -------------------------------------------------------------------------------- /src/levels/intro/3.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "goalTreeString": "{\"branches\":{\"master\":{\"target\":\"C4\",\"id\":\"master\"},\"bugFix\":{\"target\":\"C2\",\"id\":\"bugFix\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C1\"],\"id\":\"C3\"},\"C4\":{\"parents\":[\"C2\",\"C3\"],\"id\":\"C4\"}},\"HEAD\":{\"target\":\"master\",\"id\":\"HEAD\"}}", 3 | "solutionCommand": "git checkout -b bugFix;git commit;git checkout master;git commit;git merge bugFix", 4 | // "name": "Merging in Git", 5 | "name": "Git에서 브랜치 합치기(Merge)", 6 | // "hint": "Remember to commit in the order specified (bugFix before master)", 7 | "hint": "말씀드린 순서대로 커밋해주세요 (bugFix에 먼저 커밋하고 master에 커밋)", 8 | "disabledMap" : { 9 | "git revert": true 10 | }, 11 | "startDialog": { 12 | "childViews": [ 13 | { 14 | "type": "ModalAlert", 15 | "options": { 16 | "markdowns": [ 17 | // "## Branches and Merging", 18 | // "", 19 | // "Great! We now know how to commit and branch. Now we need to learn some kind of way of combining the work from two different branches together. This will allow us to branch off, develop a new feature, and then combine it back in.", 20 | // "", 21 | // "The first method to combine work that we will examine is `git merge`. Merging in Git creates a special commit that has two unique parents. A commit with two parents essentially means \"I want to include all the work from this parent over here and this one over here, *and* the set of all their parents.\"", 22 | // "", 23 | // "It's easier with visuals, let's check it out in the next view" 24 | "## 브랜치와 합치기(Merge)", 25 | "", 26 | "좋습니다! 지금까지 커밋하고 브랜치를 만드는 방법을 알아봤습니다. 이제 두 별도의 브랜치를 합치는 몇가지 방법을 알아볼 차례입니다. 이제부터 배우는 방법으로 브랜치를 따고, 새 기능을 개발 한 다음 합칠 수 있게 될 것입니다.", 27 | "", 28 | "처음으로 살펴볼 방법은 `git merge`입니다. Git의 합치기(merge)는 두 개의 부모(parent)를 가리키는 특별한 커밋을 만들어 냅니다. 두개의 부모가 있는 커밋이라는 것은 \"한 부모의 모든 작업내역과 나머지 부모의 모든 작업, *그리고* 그 두 부모의 모든 부모들의 작업내역을 포함한다\"라는 의미가 있습니다. ", 29 | "", 30 | "그림으로 보는게 이해하기 쉬워요. 다음 화면을 봅시다." 31 | 32 | ] 33 | } 34 | }, 35 | { 36 | "type": "GitDemonstrationView", 37 | "options": { 38 | "beforeMarkdowns": [ 39 | // "Here we have two branches; each has one commit that's unique. This means that neither branch includes the entire set of \"work\" in the repository that we have done. Let's fix that with merge.", 40 | // "", 41 | // "We will `merge` the branch `bugFix` into `master`" 42 | "여기에 브랜치가 두 개 있습니다. 각 브랜치에 독립된 커밋이 하나씩 있구요. 그 말은 이 저장소에 지금까지 작업한 내역이 나뉘어 담겨 있다는 얘기입니다. 두 브랜치를 합쳐서(merge) 이 문제를 해결해 볼까요?", 43 | "", 44 | "`bugFix` 브랜치를 `master` 브랜치에 합쳐(merge) 보겠습니다." 45 | ], 46 | "afterMarkdowns": [ 47 | // "Woah! See that? First of all, `master` now points to a commit that has two parents. If you follow the arrows upstream from `master`, you will hit every commit along the way to the root. This means that `master` contains all the work in the repository now.", 48 | "보셨어요? 우선, `master`가 두 부모가 있는 커밋을 가리키고 있습니다. ", 49 | "", 50 | // "Also, see how the colors of the commits changed? To help with learning, I have included some color coordination. Each branch has a unique color. Each commit turns a color that is the blended combination of all the branches that contain that commit.", 51 | "또, 커밋들의 색이 바뀐 것을 눈치 채셨나요? 이해를 돕기위해 색상으로 구분해 표현했습니다. 각 브랜치는 그 브랜치만의 색상으로 그렸습니다. 브랜치가 합쳐지는 커밋의 경우에는, 그 브랜치들의 색을 조합한 색상으로 표시 했습니다.", 52 | "", 53 | // "So here we see that the `master` branch color is blended into all the commits, but the `bugFix` color is not. Let's fix that..." 54 | "그런식으로 여기에 `bugFix`브랜치 쪽을 제외한 나머지 커밋만 `master` 브랜치의 색으로 칠해져 있습니다. 이걸 고쳐보죠..." 55 | ], 56 | "command": "git merge bugFix master", 57 | "beforeCommand": "git checkout -b bugFix; git commit; git checkout master; git commit" 58 | } 59 | }, 60 | { 61 | "type": "GitDemonstrationView", 62 | "options": { 63 | "beforeMarkdowns": [ 64 | // "Let's merge `master` into `bugFix`:" 65 | "이제 `master` 브랜치에 `bugFix`를 합쳐(merge) 봅시다:" 66 | ], 67 | "afterMarkdowns": [ 68 | // "Since `bugFix` was downstream of `master`, git didn't have to do any work; it simply just moved `bugFix` to the same commit `master` was attached to.", 69 | "`bugFix`가 `master`의 부모쪽에 있었기 때문에, git이 별다른 일을 할 필요가 없었습니다; 간단히 `bugFix`를 `master`가 붙어 있는 커밋으로 이동시켰을 뿐입니다.", 70 | "", 71 | // "Now all the commits are the same color, which means each branch contains all the work in the repository! Woohoo" 72 | "짜잔! 이제 모든 커밋의 색이 같아졌고, 이는 두 브랜치가 모두 저장소의 모든 작업 내역을 포함하고 있다는 뜻입니다." 73 | ], 74 | "command": "git merge master bugFix", 75 | "beforeCommand": "git checkout -b bugFix; git commit; git checkout master; git commit; git merge bugFix master" 76 | } 77 | }, 78 | { 79 | "type": "ModalAlert", 80 | "options": { 81 | "markdowns": [ 82 | // "To complete this level, do the following steps:", 83 | // "", 84 | // "* Make a new branch called `bugFix`", 85 | // "* Checkout the `bugFix` branch with `git checkout bugFix`", 86 | // "* Commit once", 87 | // "* Go back to `master` with `git checkout`", 88 | // "* Commit another time", 89 | // "* Merge the branch `bugFix` into `master` with `git merge`", 90 | // "", 91 | // "*Remember, you can always re-display this dialog with \"help level\"!*" 92 | "아래 작업을 해서 이 레벨을 통과하세요:", 93 | "", 94 | "* `bugFix`라는 새 브랜치를 만듭니다", 95 | "* `git checkout bugFix`를 입력해 `bugFix` 브랜치로 이동(checkout)합니다.", 96 | "* 커밋 한 번 하세요", 97 | "* `git checkout` 명령어를 이용해 `master`브랜치로 돌아갑니다", 98 | "* 커밋 또 하세요", 99 | "* `git merge` 명령어로 `bugFix`브랜치를 `master`에 합쳐 넣습니다.", 100 | "", 101 | "*아 그리고, \"help level\" 명령어로 이 안내창을 다시 볼 수 있다는 것을 기억해 두세요!*" 102 | 103 | ] 104 | } 105 | } 106 | ] 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /src/levels/intro/4.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "goalTreeString": "%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22master%22%7D%2C%22bugFix%22%3A%7B%22target%22%3A%22C2%27%22%2C%22id%22%3A%22bugFix%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C2%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22bugFix%22%2C%22id%22%3A%22HEAD%22%7D%7D", 3 | "solutionCommand": "git checkout -b bugFix;git commit;git checkout master;git commit;git checkout bugFix;git rebase master", 4 | // "name": "Rebase Introduction", 5 | // "hint": "Make sure you commit from bugFix first", 6 | "name": "리베이스(rebase)의 기본", 7 | "hint": "bugFix 브랜치에서 먼저 커밋하세요", 8 | "disabledMap" : { 9 | "git revert": true 10 | }, 11 | "startDialog": { 12 | "childViews": [ 13 | { 14 | "type": "ModalAlert", 15 | "options": { 16 | "markdowns": [ 17 | // "## Git Rebase", 18 | "## Git 리베이스(Rebase)", 19 | "", 20 | // "The second way of combining work between branches is *rebasing.* Rebasing essentially takes a set of commits, \"copies\" them, and plops them down somewhere else.", 21 | "브랜치끼리의 작업을 접목하는 두번째 방법은 *리베이스(rebase)*입니다. 리베이스는 기본적으로 커밋들을 모아서 복사한 뒤, 다른 곳에 떨궈 놓는 것입니다.", 22 | "", 23 | // "While this sounds confusing, the advantage of rebasing is that it can be used to make a nice linear sequence of commits. The commit log / history of the repository will be a lot cleaner if only rebasing is allowed.", 24 | "조금 어려게 느껴질 수 있지만, 리베이스를 하면 커밋들의 흐름을 보기 좋게 한 줄로 만들 수 있다는 장점이 있습니다. 리베이스를 쓰면 저장소의 커밋 로그와 이력이 한결 깨끗해집니다.", 25 | "", 26 | // "Let's see it in action..." 27 | "어떻게 동작하는지 살펴볼까요..." 28 | ] 29 | } 30 | }, 31 | { 32 | "type": "GitDemonstrationView", 33 | "options": { 34 | "beforeMarkdowns": [ 35 | // "Here we have two branches yet again; note that the bugFix branch is currently selected (note the asterisk)", 36 | "여기 또 브랜치 두 개가 있습니다; bugFix브랜치가 현재 선택됐다는 점 눈여겨 보세요 (별표 표시)", 37 | "", 38 | // "We would like to move our work from bugFix directly onto the work from master. That way it would look like these two features were developed sequentially, when in reality they were developed in parallel.", 39 | "bugFix 브랜치에서의 작업을 master 브랜치 위로 직접 옮겨 놓으려고 합니다. 그렇게 하면, 실제로는 두 기능을 따로따로 개발했지만, 마치 순서대로 개발한 것처럼 보이게 됩니다.", 40 | "", 41 | // "Let's do that with the `git rebase` command" 42 | "`git rebase` 명령어로 함께 해보죠." 43 | ], 44 | "afterMarkdowns": [ 45 | // "Awesome! Now the work from our bugFix branch is right on top of master and we have a nice linear sequence of commits.", 46 | "오! 이제 bugFix 브랜치의 작업 내용이 master의 바로 위에 깔끔한 한 줄의 커밋으로 보이게 됐습니다.", 47 | "", 48 | // "Note that the commit C3 still exists somewhere (it has a faded appearance in the tree), and C3' is the \"copy\" that we rebased onto master.", 49 | "C3 커밋은 어딘가에 아직 남아있고(그림에서 흐려짐), C3'는 master 위에 올려 놓은 복사본입니다.", 50 | "", 51 | // "The only problem is that master hasn't been updated either, let's do that now..." 52 | "master가 아직 그대로라는 문제가 남아있는데요, 바로 해결해보죠..." 53 | ], 54 | "command": "git rebase master", 55 | "beforeCommand": "git commit; git checkout -b bugFix C1; git commit" 56 | } 57 | }, 58 | { 59 | "type": "GitDemonstrationView", 60 | "options": { 61 | "beforeMarkdowns": [ 62 | // "Now we are checked out on the `master` branch. Let's do ahead and rebase onto `bugFix`..." 63 | "우리는 지금 `master` 브랜치를 선택한 상태입니다. `bugFix` 브랜치쪽으로 리베이스 해보겠습니다..." 64 | ], 65 | "afterMarkdowns": [ 66 | // "There! Since `master` was downstream of `bugFix`, git simply moved the `master` branch reference forward in history." 67 | "보세요! `master`가 `bugFix`의 부모쪽에 있었기 때문에, 단순히 그 브랜치를 더 앞쪽의 커밋을 가리키게 이동하는 것이 전부입니다." 68 | ], 69 | "command": "git rebase bugFix", 70 | "beforeCommand": "git commit; git checkout -b bugFix C1; git commit; git rebase master; git checkout master" 71 | } 72 | }, 73 | { 74 | "type": "ModalAlert", 75 | "options": { 76 | "markdowns": [ 77 | // "To complete this level, do the following", 78 | "이하 작업을 하면 이번 레벨을 통과합니다", 79 | "", 80 | // "* Checkout a new branch named `bugFix`", 81 | "* `bugFix`라는 새 브랜치를 만들어 선택하세요", 82 | // "* Commit once", 83 | "* 커밋 한 번 합니다", 84 | // "* Go back to master and commit again", 85 | "* master로 돌아가서 또 커밋합니다", 86 | // "* Check out bugFix again and rebase onto master", 87 | "* bugFix를 다시 선택하고 master에 리베이스 하세요", 88 | "", 89 | // "Good luck!" 90 | "화이팅!" 91 | ] 92 | } 93 | } 94 | ] 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/levels/intro/5.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "goalTreeString": "%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%7D%2C%22pushed%22%3A%7B%22target%22%3A%22C2%27%22%2C%22id%22%3A%22pushed%22%7D%2C%22local%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22local%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C2%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22pushed%22%2C%22id%22%3A%22HEAD%22%7D%7D", 3 | "solutionCommand": "git reset HEAD~1;git checkout pushed;git revert HEAD", 4 | "startTree": "{\"branches\":{\"master\":{\"target\":\"C1\",\"id\":\"master\"},\"pushed\":{\"target\":\"C2\",\"id\":\"pushed\"},\"local\":{\"target\":\"C3\",\"id\":\"local\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C1\"],\"id\":\"C3\"}},\"HEAD\":{\"target\":\"local\",\"id\":\"HEAD\"}}", 5 | // "name": "Reversing Changes in Git", 6 | "name": "Git에서 작업 되돌리기", 7 | "hint": "", 8 | "startDialog": { 9 | "childViews": [ 10 | { 11 | "type": "ModalAlert", 12 | "options": { 13 | "markdowns": [ 14 | // "## Reversing Changes in Git", 15 | "## Git에서 작업 되돌리기", 16 | "", 17 | // "There are many ways to reverse changes in Git. And just like committing, reversing changes in Git has both a low-level component (staging individual files or chunks) and a high-level component (how the changes are actually reversed). Our application will focus on the latter.", 18 | "Git에는 작업한 것을 되돌리는 여러가지 방법이 있습니다. 변경내역을 되돌리는 것도 커밋과 마찬가지로 낮은 수준의 일(개별 파일이나 묶음을 스테이징 하는 것)과 높은 수준의 일(실제 변경이 복구되는 방법)이 있는데요, 여기서는 후자에 집중해 알려드릴게요.", 19 | "", 20 | // "There are two primary ways to undo changes in Git -- one is using `git reset` and the other is using `git revert`. We will look at each of these in the next dialog", 21 | "Git에서 변경한 내용을 되돌리는 방법은 크게 두가지가 있습니다 -- 하나는 `git reset`을 쓰는거고, 다른 하나는 `git revert`를 사용하는 것입니다. 다음 화면에서 하나씩 알아보겠습니다.", 22 | "" 23 | ] 24 | } 25 | }, 26 | { 27 | "type": "GitDemonstrationView", 28 | "options": { 29 | "beforeMarkdowns": [ 30 | // "## Git Reset", 31 | "## Git 리셋(reset)", 32 | "", 33 | // "`git reset` reverts changes by moving a branch reference backwards in time to an older commit. In this sense you can think of it as \"rewriting history;\" `git reset` will move a branch backwards as if the commit had never been made in the first place.", 34 | "`git reset`은 브랜치로 하여금 예전의 커밋을 가리키도록 이동시키는 방식으로 변경 내용을 되돌립니다. 이런 관점에서 \"히스토리를 고쳐쓴다\"라고 말할 수 있습니다. 즉, `git reset`은 마치 애초에 커밋하지 않은 것처럼 예전 커밋으로 브랜치를 옮기는 것입니다.", 35 | "", 36 | // "Let's see what that looks like:" 37 | "어떤 그림인지 한번 보죠:" 38 | ], 39 | "afterMarkdowns": [ 40 | // "Nice! Git simply moved the master branch reference back to `C1`; now our local repository is in a state as if `C2` had never happened" 41 | "그림에서처럼 master 브랜치가 가리키던 커밋을 `C1`로 다시 옮겼습니다; 이러면 로컬 저장소에는 마치 `C2`커밋이 아예 없었던 것과 마찬가지 상태가 됩니다." 42 | ], 43 | "command": "git reset HEAD~1", 44 | "beforeCommand": "git commit" 45 | } 46 | }, 47 | { 48 | "type": "GitDemonstrationView", 49 | "options": { 50 | "beforeMarkdowns": [ 51 | // "## Git Revert", 52 | "## Git 리버트(revert)", 53 | "", 54 | // "While reseting works great for local branches on your own machine, it's method of \"rewriting history\" doesn't work for remote branches that others are using.", 55 | "각자의 컴퓨터에서 작업하는 로컬 브랜치의 경우 리셋(reset)을 잘 쓸 수 있습니다만, \"히스토리를 고쳐쓴다\"는 점 때문에 다른 사람이 작업하는 리모트 브랜치에는 쓸 수 없습니다.", 56 | "", 57 | // "In order to reverse changes and *share* those reversed changes with others, we need to use `git revert`. Let's see it in action" 58 | "변경분을 되돌리고, 이 되돌린 내용을 다른 사람들과 *공유하기* 위해서는, `git revert`를 써야합니다. 예제로 살펴볼게요." 59 | ], 60 | "afterMarkdowns": [ 61 | // "Weird, a new commit plopped down below the commit we wanted to reverse. That's because this new commit `C2'` introduces *changes* -- it just happens to introduce changes that exactly reverses the commit of `C2`.", 62 | "어색하게도, 우리가 되돌리려고한 커밋의 아래에 새로운 커밋이 생겼습니다. `C2`라는 새로운 커밋에 *변경내용*이 기록되는데요, 이 변경내역이 정확히 `C2` 커밋 내용의 반대되는 내용입니다.", 63 | "", 64 | // "With reverting, you can push out your changes to share with others." 65 | "리버트를 하면 다른 사람들에게도 변경 내역을 밀어(push) 보낼 수 있습니다." 66 | ], 67 | "command": "git revert HEAD", 68 | "beforeCommand": "git commit" 69 | } 70 | }, 71 | { 72 | "type": "ModalAlert", 73 | "options": { 74 | "markdowns": [ 75 | // "To complete this level, reverse the two most recent commits on both `local` and `pushed`.", 76 | "이 레벨을 통과하려면, `local` 브랜치와 `pushed` 브랜치에 있는 최근 두 번의 커밋을 되돌려 보세요.", 77 | "", 78 | // "Keep in mind that `pushed` is a remote branch and `local` is a local branch -- that should help you chose your methods." 79 | "`pushed`는 리모트 브랜치이고, `local`은 로컬 브랜치임을 신경쓰셔서 작업하세요 -- 어떤 방법을 선택하실지 떠오르시죠?" 80 | ] 81 | } 82 | } 83 | ] 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/levels/mixed/1.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "compareOnlyMasterHashAgnostic": true, 3 | "disabledMap" : { 4 | "git revert": true 5 | }, 6 | "goalTreeString": "%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C4%27%22%2C%22id%22%3A%22master%22%7D%2C%22debug%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22debug%22%7D%2C%22printf%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22printf%22%7D%2C%22bugFix%22%3A%7B%22target%22%3A%22C4%27%22%2C%22id%22%3A%22bugFix%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C4%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D", 7 | "solutionCommand": "git checkout master;git cherry-pick C4", 8 | "startTree": "{\"branches\":{\"master\":{\"target\":\"C1\",\"id\":\"master\"},\"debug\":{\"target\":\"C2\",\"id\":\"debug\"},\"printf\":{\"target\":\"C3\",\"id\":\"printf\"},\"bugFix\":{\"target\":\"C4\",\"id\":\"bugFix\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C2\"],\"id\":\"C3\"},\"C4\":{\"parents\":[\"C3\"],\"id\":\"C4\"}},\"HEAD\":{\"target\":\"bugFix\",\"id\":\"HEAD\"}}", 9 | // "name": "Grabbing Just 1 Commit", 10 | "name": "딱 한개의 커밋만 가져오기", 11 | // "hint": "Remember, interactive rebase or cherry-pick is your friend here", 12 | "hint": "대화식 리베이스(rebase -i)나 or 체리픽(cherry-pick)을 사용하세요", 13 | "startDialog": { 14 | "childViews": [ 15 | { 16 | "type": "ModalAlert", 17 | "options": { 18 | "markdowns": [ 19 | // "## Locally stacked commits", 20 | "## 로컬에 쌓인 커밋들", 21 | "", 22 | // "Here's a development situation that often happens: I'm trying to track down a bug but it is quite elusive. In order to aid in my detective work, I put in a few debug commands and a few print statements.", 23 | "개발중에 종종 이런 상황이 생깁니다: 잘 띄지 않는 버그를 찾아서 해결하려고, 어떤 부분의 문제인지를 찾기 위해 디버그용 코드와 화면에 정보를 프린트하는 코드 몇 줄 넣습니다. ", 24 | "", 25 | // "All of these debugging / print statements are in their own branches. Finally I track down the bug, fix it, and rejoice!", 26 | "디버깅용 코드나 프린트 명령은 그 브랜치에 들어있습니다. 마침내 버그를 찾아서 고쳤고, 원래 작업하는 브랜치에 합치면 됩니다!", 27 | "", 28 | // "Only problem is that I now need to get my `bugFix` back into the `master` branch! I could simply fast-forward `master`, but then `master` would get all my debug statements." 29 | "이제 `bugFix`브랜치의 내용을 `master`에 합쳐 넣으려 하지만, 한 가지 문제가 있습니다. 그냥 간단히 `master`브랜치를 최신 커밋으로 이동시킨다면(fast-forward) 그 불필요한 디버그용 코드들도 함께 들어가 버린다는 문제죠." 30 | ] 31 | } 32 | }, 33 | { 34 | "type": "ModalAlert", 35 | "options": { 36 | "markdowns": [ 37 | // "This is where the magic of Git comes in. There are a few ways to do this, but the two most straightforward ways are:", 38 | "여기에서 Git의 마법이 드러납니다. 이 문제를 해결하는 여러가지 방법이 있습니다만, 가장 간단한 두가지 방법 아래와 같습니다:", 39 | "", 40 | "* `git rebase -i`", 41 | "* `git cherry-pick`", 42 | "", 43 | // "Interactive (the `-i`) rebasing allows you to chose which commits you want to keep or discard. It also allows you to reorder commits. This can be helpful if you want to toss out some work.", 44 | "대화형 (-i 옵션) 리베이스(rebase)로는 어떤 커밋을 취하거나 버릴지를 선택할 수 있습니다. 또 커밋의 순서를 바꿀 수도 있습니다. 이 커맨드로 어떤 작업의 일부만 골라내기에 유용합니다.", 45 | "", 46 | // "Cherry-picking allows you to pick individual commits and plop them down on top of `HEAD`" 47 | "체리픽(cherry-pick)은 개별 커밋을 골라서 `HEAD`위에 떨어뜨릴 수 있습니다." 48 | ] 49 | } 50 | }, 51 | { 52 | "type": "ModalAlert", 53 | "options": { 54 | "markdowns": [ 55 | // "This is a later level so we will leave it up to you to decide, but in order to complete the level, make sure `master` receives the commit that `bugFix` references." 56 | "이번 레벨을 통과하기 위해 어떤 방법을 쓰시든 자유입니다만, `master`브랜치가 `bugFix` 브랜치의 커밋을 일부 가져오게 해주세요." 57 | ] 58 | } 59 | } 60 | ] 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/levels/mixed/2.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "disabledMap" : { 3 | "git cherry-pick": true, 4 | "git revert": true 5 | }, 6 | "compareOnlyMaster": true, 7 | "goalTreeString": "%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C3%27%27%22%2C%22id%22%3A%22master%22%7D%2C%22newImage%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22newImage%22%7D%2C%22caption%22%3A%7B%22target%22%3A%22C3%27%27%22%2C%22id%22%3A%22caption%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C2%27%27%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%27%27%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C2%27%27%27%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D", 8 | "solutionCommand": "git rebase -i HEAD~2;git commit --amend;git rebase -i HEAD~2;git rebase caption master", 9 | "startTree": "{\"branches\":{\"master\":{\"target\":\"C1\",\"id\":\"master\"},\"newImage\":{\"target\":\"C2\",\"id\":\"newImage\"},\"caption\":{\"target\":\"C3\",\"id\":\"caption\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C2\"],\"id\":\"C3\"}},\"HEAD\":{\"target\":\"caption\",\"id\":\"HEAD\"}}", 10 | // "name": "Juggling Commits", 11 | "name": "커밋들 갖고 놀기", 12 | // "hint": "The first command is git rebase -i HEAD~2", 13 | "hint": "첫번째 명령은 git rebase -i HEAD~2 입니다", 14 | "startDialog": { 15 | "childViews": [ 16 | { 17 | "type": "ModalAlert", 18 | "options": { 19 | "markdowns": [ 20 | // "## Juggling Commits", 21 | "## 커밋들 갖고 놀기", 22 | "", 23 | // "Here's another situation that happens quite commonly. You have some changes (`newImage`) and another set of changes (`caption`) that are related, so they are stacked on top of each other in your repository (aka one after another).", 24 | "이번에도 꽤 자주 발생하는 상황입니다. `newImage`와 `caption` 브랜치에 각각의 변경내역이 있고 서로 약간 관련이 있어서, 저장소에 차례로 쌓여있는 상황입니다.", 25 | "", 26 | // "The tricky thing is that sometimes you need to make a small modification to an earlier commit. In this case, design wants us to change the dimensions of `newImage` slightly, even though that commit is way back in our history!!" 27 | "때로는 이전 커밋의 내용을 살짝 바꿔야하는 골치아픈 상황에 빠지게 됩니다. 이번에는 디자인 쪽에서 우리의 작업이력(history)에서는 이미 한참 전의 커밋 내용에 있는 `newImage`의 크기를 살짝 바꿔달라는 요청이 들어왔습니다." 28 | ] 29 | } 30 | }, 31 | { 32 | "type": "ModalAlert", 33 | "options": { 34 | "markdowns": [ 35 | // "We will overcome this difficulty by doing the following:", 36 | "이 문제를 다음과 같이 풀어봅시다:", 37 | "", 38 | // "* We will re-order the commits so the one we want to change is on top with `git rebase -i`", 39 | "* `git rebase -i` 명령으로 우리가 바꿀 커밋을 가장 최근 순서로 바꾸어 놓습니다", 40 | // "* We will `commit --amend` to make the slight modification", 41 | "* `commit --amend` 명령으로 커밋 내용을 정정합니다", 42 | // "* Then we will re-order the commits back to how they were previously with `git rebase -i`", 43 | "* 다시 `git rebase -i` 명령으로 이 전의 커밋 순서대로 되돌려 놓습니다", 44 | // "* Finally, we will move master to this updated part of the tree to finish the level (via your method of choosing)", 45 | "* 마지막으로, master를 지금 트리가 변경된 부분으로 이동합니다. (편하신 방법으로 하세요)", 46 | "", 47 | // "There are many ways to accomplish this overall goal (I see you eye-ing cherry-pick), and we will see more of them later, but for now let's focus on this technique." 48 | "이 목표를 달성하기 위해서는 많은 방법이 있는데요(체리픽을 고민중이시죠?), 체리픽은 나중에 더 살펴보기로 하고, 우선은 위의 방법으로 해결해보세요." 49 | ] 50 | } 51 | }, 52 | { 53 | "type": "ModalAlert", 54 | "options": { 55 | "markdowns": [ 56 | // "Lastly, pay attention to the goal state here -- since we move the commits twice, they both get an apostrophe appended. One more apostrophe is added for the commit we amend, which gives us the final form of the tree " 57 | "최종적으로, 목표 결과를 눈여겨 보세요 -- 우리가 커밋을 두 번 옮겼기 때문에, 두 커밋 모두 따옴표 표시가 붙어있습니다. 정정한(amend) 커밋은 따옴표가 추가로 하나 더 붙어있습니다." 58 | ] 59 | } 60 | } 61 | ] 62 | } 63 | }; -------------------------------------------------------------------------------- /src/levels/mixed/3.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "goalTreeString": "%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C3%27%22%2C%22id%22%3A%22master%22%7D%2C%22newImage%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22newImage%22%7D%2C%22caption%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22caption%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%27%27%22%5D%2C%22id%22%3A%22C3%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D", 3 | "solutionCommand": "git checkout master;git cherry-pick C2;git commit --amend;git cherry-pick C3", 4 | "disabledMap" : { 5 | "git revert": true 6 | }, 7 | "startTree": "{\"branches\":{\"master\":{\"target\":\"C1\",\"id\":\"master\"},\"newImage\":{\"target\":\"C2\",\"id\":\"newImage\"},\"caption\":{\"target\":\"C3\",\"id\":\"caption\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C2\"],\"id\":\"C3\"}},\"HEAD\":{\"target\":\"caption\",\"id\":\"HEAD\"}}", 8 | "compareOnlyMaster": true, 9 | // "name": "Juggling Commits #2", 10 | "name": "커밋 갖고 놀기 #2", 11 | // "hint": "Don't forget to forward master to the updated changes!", 12 | "hint": "master를 변경 완료한 커밋으로 이동(forward)시키는 것을 잊지 마세요!", 13 | "startDialog": { 14 | "childViews": [ 15 | { 16 | "type": "ModalAlert", 17 | "options": { 18 | "markdowns": [ 19 | // "## Juggling Commits #2", 20 | "## 커밋 갖고 놀기 #2", 21 | "", 22 | // "*If you haven't completed Juggling Commits #1 (the previous level), please do so before continuing*", 23 | "*만약 이전 레벨의 커밋 갖고 놀기 #1을 풀지 않으셨다면, 계속하기에 앞서서 꼭 풀어보세요*", 24 | "", 25 | // "As you saw in the last level, we used `rebase -i` to reorder the commits. Once the commit we wanted to change was on top, we could easily --amend it and re-order back to our preferred order.", 26 | "이전 레벨에서 보셨듯이 `rebase -i` 명령으로 커밋의 순서를 바꿀 수 있습니다. 정정할 커밋이 바로 직전(top)에 있으면 간단히 --amend로 수정할 수 있고, 그리고 나서 다시 원하는 순서로 되돌려 놓으면 됩니다.", 27 | "", 28 | // "The only issue here is that there is a lot of reordering going on, which can introduce rebase conflicts. Let's look at another method with `git cherry-pick`" 29 | "이번에 한가지 문제는 순서를 꽤 많이 바꿔야한다는 점인데요, 그러다가 리베이스중에 충돌이 날 수 있습니다. 이번에는 다른 방법인 `git cherry-pick`으로 해결해 봅시다." 30 | ] 31 | } 32 | }, 33 | { 34 | "type": "GitDemonstrationView", 35 | "options": { 36 | "beforeMarkdowns": [ 37 | // "Remember that git cherry-pick will plop down a commit from anywhere in the tree onto HEAD (as long as that commit isn't upstream).", 38 | "git cherry-pick으로 HEAD에다 어떤 커밋이든 떨어 뜨려 놓을 수 있다고 알려드린것 기억나세요? (단, 그 커밋이 현재 가리키고 있는 커밋이 아니어야합니다)", 39 | "", 40 | // "Here's a small refresher demo:" 41 | "간단한 데모로 다시 알려드리겠습니다:" 42 | ], 43 | "afterMarkdowns": [ 44 | // "Nice! Let's move on" 45 | "좋아요! 계속할게요" 46 | ], 47 | "command": "git cherry-pick C2", 48 | "beforeCommand": "git checkout -b bugFix; git commit; git checkout master; git commit" 49 | } 50 | }, 51 | { 52 | "type": "ModalAlert", 53 | "options": { 54 | "markdowns": [ 55 | // "So in this level, let's accomplish the same objective of amending `C2` once but avoid using `rebase -i`. I'll leave it up to you to figure it out! :D" 56 | "그럼 이번 레벨에서는 아까와 마찬가지로 `C2` 커밋의 내용을 정정하되, `rebase -i`를 쓰지 말고 해보세요. ^.~" 57 | ] 58 | } 59 | } 60 | ] 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/levels/rebase/1.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "compareOnlyMasterHashAgnostic": true, 3 | "disabledMap" : { 4 | "git revert": true 5 | }, 6 | "goalTreeString": "%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C7%27%22%2C%22id%22%3A%22master%22%7D%2C%22bugFix%22%3A%7B%22target%22%3A%22C3%27%22%2C%22id%22%3A%22bugFix%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C6%27%22%2C%22id%22%3A%22side%22%7D%2C%22another%22%3A%7B%22target%22%3A%22C7%27%22%2C%22id%22%3A%22another%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C6%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C6%22%7D%2C%22C7%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C7%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C6%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C6%27%22%7D%2C%22C7%27%22%3A%7B%22parents%22%3A%5B%22C6%27%22%5D%2C%22id%22%3A%22C7%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D", 7 | "solutionCommand": "git checkout bugFix;git rebase master;git checkout side;git rebase bugFix;git checkout another;git rebase side;git rebase another master", 8 | "startTree": "{\"branches\":{\"master\":{\"target\":\"C2\",\"id\":\"master\"},\"bugFix\":{\"target\":\"C3\",\"id\":\"bugFix\"},\"side\":{\"target\":\"C6\",\"id\":\"side\"},\"another\":{\"target\":\"C7\",\"id\":\"another\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C1\"],\"id\":\"C3\"},\"C4\":{\"parents\":[\"C0\"],\"id\":\"C4\"},\"C5\":{\"parents\":[\"C4\"],\"id\":\"C5\"},\"C6\":{\"parents\":[\"C5\"],\"id\":\"C6\"},\"C7\":{\"parents\":[\"C5\"],\"id\":\"C7\"}},\"HEAD\":{\"target\":\"master\",\"id\":\"HEAD\"}}", 9 | // "name": "Rebasing over 9000 times", 10 | "name": "9천번이 넘는 리베이스", 11 | // "hint": "Remember, the most efficient way might be to only update master at the end...", 12 | "hint": "아마도 master를 마지막에 업데이트하는 것이 가장 효율적인 방법일 것입니다...", 13 | "startDialog": { 14 | "childViews": [ 15 | { 16 | "type": "ModalAlert", 17 | "options": { 18 | "markdowns": [ 19 | // "### Rebasing Multiple Branches", 20 | "### 여러 브랜치를 리베이스(rebase)하기 ", 21 | "", 22 | // "Man, we have a lot of branches going on here! Let's rebase all the work from these branches onto master.", 23 | "음, 여기 꽤 여러개의 브랜치가 있습니다! 이 브랜치들의 모든 작업내역을 master에 리베이스 해볼까요?", 24 | "", 25 | // "Upper management is making this a bit trickier though -- they want the commits to all be in sequential order. So this means that our final tree should have `C7'` at the bottom, `C6'` above that, etc etc, etc all in order.", 26 | "윗선에서 일을 복잡하게 만드네요 -- 그 분들이 이 모든 커밋들을 순서에 맞게 정렬하라고 합니다. 그럼 결국 우리의 최종 목표 트리는 제일 아래에 `C7'` 커밋, 그 위에 `C6'` 커밋, 또 그 위에 순서대로 보여합니다.", 27 | "", 28 | // "If you mess up along the way, feel free to use `reset` to start over again. Be sure to check out our solution and see if you can do it in fewer commands!" 29 | "만일 작업중에 내용이 꼬인다면, `reset`이라고 쳐서 처음부터 다시 시작할 수 있습니다. 모범 답안을 확인해 보시고, 혹시 더 적은 수의 커맨드로 해결할 수 있는지 알아보세요!", 30 | ] 31 | } 32 | } 33 | ] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/levels/rebase/2.js: -------------------------------------------------------------------------------- 1 | exports.level = { 2 | "compareAllBranchesHashAgnostic": true, 3 | "disabledMap" : { 4 | "git revert": true 5 | }, 6 | "goalTreeString": "%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C5%22%2C%22id%22%3A%22master%22%7D%2C%22one%22%3A%7B%22target%22%3A%22C2%27%22%2C%22id%22%3A%22one%22%7D%2C%22two%22%3A%7B%22target%22%3A%22C2%27%27%22%2C%22id%22%3A%22two%22%7D%2C%22three%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22three%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C4%27%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C4%27%27%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C4%27%27%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%27%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22two%22%2C%22id%22%3A%22HEAD%22%7D%7D", 7 | "solutionCommand": "git checkout one; git cherry-pick C4; git cherry-pick C3; git cherry-pick C2; git checkout two; git cherry-pick C5; git cherry-pick C4; git cherry-pick C3; git cherry-pick C2; git branch -f three C2", 8 | "startTree": "{\"branches\":{\"master\":{\"target\":\"C5\",\"id\":\"master\"},\"one\":{\"target\":\"C1\",\"id\":\"one\"},\"two\":{\"target\":\"C1\",\"id\":\"two\"},\"three\":{\"target\":\"C1\",\"id\":\"three\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C2\"],\"id\":\"C3\"},\"C4\":{\"parents\":[\"C3\"],\"id\":\"C4\"},\"C5\":{\"parents\":[\"C4\"],\"id\":\"C5\"}},\"HEAD\":{\"target\":\"master\",\"id\":\"HEAD\"}}", 9 | // "name": "Branch Spaghetti", 10 | "name": "브랜치 스파게티", 11 | // "hint": "There are multiple ways to solve this! Cherry-pick is the easy / long way, but rebase -i can be a shortcut", 12 | "hint": "이 문제를 해결하는 방법은 여러가지가 있습니다! 체리픽(cherry-pick)이 가장 쉽지만 오래걸리는 방법이고, 리베이스(rebase -i)가 빠른 방법입니다", 13 | "startDialog": { 14 | "childViews": [ 15 | { 16 | "type": "ModalAlert", 17 | "options": { 18 | "markdowns": [ 19 | // "## Branch Spaghetti", 20 | "## 브랜치 스파게티", 21 | "", 22 | // "WOAHHHhhh Nelly! We have quite the goal to reach in this level.", 23 | "음, 이번에는 만만치 않습니다!", 24 | "", 25 | // "Here we have `master` that is a few commits ahead of branches `one` `two` and `three`. For whatever reason, we need to update these three other branches with modified versions of the last few commits on master.", 26 | "여기 `master` 브랜치의 몇 번 이전 커밋에 `one`, `two`,`three` 총 3개의 브랜치가 있습니다. 어떤 이유인지는 몰라도, master의 최근 커밋 몇 개를 나머지 세 개의 브랜치에 반영하려고 합니다.", 27 | "", 28 | // "Branch `one` needs a re-ordering and a deletion of `C5`. `two` needs pure reordering, and `three` only needs one commit!", 29 | "`one` 브랜치는 순서를 바꾸고 `C5`커밋을 삭제하고, `two`브랜치는 순서만 바꾸며, `three`브랜치는 하나의 커밋만 가져옵시다!", 30 | "", 31 | // "We will let you figure out how to solve this one -- make sure to check out our solution afterwards with `show solution`. " 32 | "자유롭게 이 문제를 풀어보시고 나서 `show solution`명령어로 모범 답안을 확인해보세요." 33 | ] 34 | } 35 | } 36 | ] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/style/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urigit/learnGitBranching/659e23fec29a84e043e3cfc9ccdfe5c2b879ddab/src/style/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/style/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urigit/learnGitBranching/659e23fec29a84e043e3cfc9ccdfe5c2b879ddab/src/style/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/style/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urigit/learnGitBranching/659e23fec29a84e043e3cfc9ccdfe5c2b879ddab/src/style/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | Mega Things 2 | ~~~~~~~~~~~~~~~~~~~~~~~~ 3 | [ ] origin support 4 | 5 | 6 | Big Things 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | [ ] compare settings for a level!!! integrated into builder... 9 | [ ] hash agnostic comparison 10 | [ ] rebase -i solution demonstration (blink and fade thing) 11 | [ ] hash agnotisc comparison with asserts for ammends 12 | [ ] tree pruning 13 | 14 | Medium things: 15 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 16 | [ ] fix clickthrough when goal and start are shown 17 | [ ] animating lock refactor -- not just a boolean, but a stack? 18 | [ ] fix refreshing during solution animation 19 | 20 | Cases to handle / things to edit 21 | ======================= 22 | 23 | Small things to implement: 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | Minor Bugs to fix: 27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | 29 | Big Bugs to fix: 30 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 31 | 32 | Ideas for cleaning 33 | ~~~~~~~~~~~~~ 34 | - CSS... a ton. Switch to less ? 35 | 36 | Done things: 37 | (I only started this on Dec 17th 2012 to get a better sense of what was done) 38 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | [x] get automated SHA hash appending in the html source :OOOOO... template? or what?t 40 | [x] import random level JSON 41 | [x] export / import tree from JSON 42 | [x] rebase bug... ugh 43 | [x] bug with weird tree string: 44 | [x] optimistic parsing of level and level builder commands, man that was toughwith circular imports 45 | [x] edit dialog 46 | [x] level builder dialog builder 47 | [x] level builder dialog tester 48 | [x] turn off button clicking for demonstration view :O 49 | [x] text grabber for commands 50 | [x] way to close testing view 51 | [x] allow demonstration view to have git commands beforehand 52 | [x] demonstration builder progress 53 | [x] markdowngrabber 54 | [x] sandbox and level command refresh 55 | [x] level builder finish 56 | [x] level builder? :OOO 57 | * basically just an extension of level (or sandbox), that has commands like 58 | ```save tree beginning``` or ```save tree goal``` and then a final 59 | dialog typing area thingy 60 | [x] vis branch z index reflow bug 61 | [x] fix terminal input field in general 62 | [x] warning for window size 63 | [x] esc on multiview quits absolutely 64 | [x] cross browser support... firefox only LULZ. should be just css right? WRONG 65 | [x] keyboard navigation for level selector 66 | [x] optional multiview on start 67 | [x] local storage for solved map 68 | [x] what if they just type "levels" ? 69 | [x] hookup for when solving happens 70 | [x] levels dropdown selection? 71 | [x] git demonstration view -- shouldnt be too bad. LOL WHAT A FUCKING JOKE like 4 hours 72 | [x] gotoSandbox command 73 | [x] "next level?" dialog after beating level 74 | [x] keyboard input for confirm / cancel 75 | [x] level arbiter (has everything by ID) 76 | [x] flip branches on the sides!! i wonder how to determine... 77 | [x] click handlers on goal visualization for the actual canvas elements 78 | [x] sandbox can launch and takedown levels 79 | [x] TWO epic bugs squashed: 80 | * Raphael process.nextTick needed 81 | * _.debounce on prototype 82 | [x] window zoom alert thing -- this just needs to be timeouted one more time 83 | [x] level teardown 84 | [x] great die for levels 85 | [x] show which level you are in! with a little thing on the top 86 | [x] allow command history to clear finished ones 87 | [x] put in some > into the rules for CSS 88 | [x] fix bug for multiview, i think its from the die() on everyone 89 | [x] fixed bug in command queue 90 | [x] better compare in levels 91 | [x] show solution 92 | [x] show goal 93 | [x] reset for sandbox command 94 | [x] do an after-paper-initialize type thing so we can insert git shim once 95 | git engine is done. 96 | [x] promise-based levels 97 | [x] fixed that to-do entry with the toggle Z thing. now its much more consistent 98 | [x] sandbox a real view now 99 | [x] awesome before and after shims with event baton stealing and passing back 100 | [x] sip from buffer with post-command hooks. ideally the git engine 101 | knows nothing about the level being played 102 | [x] fix tests 103 | [x] transition to deferreds / promises for command callbacks 104 | [x] awesome ability to have dialogs and such handle command processing and block 105 | [x] figure out why multiview baton passing doesn't work... 106 | [x] multiple things can process!!! 107 | [x] move command creation outside of the command view so multiple things 108 | can be responsible for specifying the waterfall associated with a command! 109 | [x] awesome zoom level polling and sweet event baton stealing :DDDDDDDDDDDDDD 110 | [x] then refactor keyboard input and UI.listen() to that event system 111 | [x] make some kind of "single listener" event system... will make keyboard stuff easy 112 | because then you just steal and release for modals and such 113 | [x] text input from the commandPromptView must flow down into 114 | filters. no hacky stuff anymore where it's part of the option parser, 115 | wtf 116 | [x] ok fuckit here is the deal. Command model has minimal logic -- it calls 117 | to a parse waterfall that expands any shortcuts needed, handles any instant 118 | commands, and then finally will handle the dispatching. I think this will be 119 | really nice :D 120 | [x] disabled map for levels 121 | [x] better click events on branches and commits 122 | [x] change to returning a promise for multiview 123 | [x] multiViews with multiple terminals... 124 | [x] debounce the forward and back methods 125 | [x] multiview makes all these arrow views which fire events 126 | [x] markdown parsing yay!! 127 | [x] check animation for command entry fading nicely wtf 128 | [x] no more CSS ids in views 129 | [x] promise-based confirm cnacel 130 | [x] rebase buttons view & styling 131 | [x] rebase entries styling 132 | [x] view for anything above the fold (modal view) 133 | [x] rebase styling (get it better. even cuter -- make it like a command window) 134 | [x] fix multiple rebases 135 | [x] z index reflow update 136 | [x] level finish animation 137 | [x] refactor visualization 138 | [x] aliases replace when put into commands 139 | [x] headless Git for testing (send it commands and expected trees) 140 | [x] few tests 141 | [x] Great git test coverage 142 | [x] gitEngine loads from tree immediately, not the weird thing we have now! 143 | [x] nice opacity fade in 144 | [x] clean up require 145 | [x] promise based callback for interactive rebase WITH FAIL awesome 146 | 147 | --------------------------------------------------------------------------------