mervenoyan commited on
Commit
45a32e2
·
1 Parent(s): 5319ac2

initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. CNAME +1 -0
  2. LICENSE.md +21 -0
  3. README.md +1 -0
  4. assets/favicon.ico +0 -0
  5. assets/font/fontawesome-webfont.eot +0 -0
  6. assets/font/fontawesome-webfont.svg +0 -0
  7. assets/font/fontawesome-webfont.ttf +0 -0
  8. assets/font/fontawesome-webfont.woff +0 -0
  9. assets/learnGitBranching.png +0 -0
  10. checkgit.sh +6 -0
  11. gulpfile.js +242 -0
  12. package.json +51 -0
  13. src/js/actions/CommandLineActions.js +19 -0
  14. src/js/actions/GlobalStateActions.js +38 -0
  15. src/js/actions/LevelActions.js +25 -0
  16. src/js/actions/LocaleActions.js +32 -0
  17. src/js/app/index.js +363 -0
  18. src/js/commands/index.js +208 -0
  19. src/js/constants/AppConstants.js +42 -0
  20. src/js/dialogs/confirmShowSolution.js +192 -0
  21. src/js/dialogs/levelBuilder.js +368 -0
  22. src/js/dialogs/nextLevel.js +216 -0
  23. src/js/dialogs/sandbox.js +812 -0
  24. src/js/dispatcher/AppDispatcher.js +24 -0
  25. src/js/git/commands.js +955 -0
  26. src/js/git/gitShim.js +83 -0
  27. src/js/git/headless.js +145 -0
  28. src/js/git/index.js +3202 -0
  29. src/js/graph/index.js +167 -0
  30. src/js/graph/treeCompare.js +465 -0
  31. src/js/intl/checkStrings.js +33 -0
  32. src/js/intl/index.js +118 -0
  33. src/js/intl/strings.js +0 -0
  34. src/js/level/builder.js +408 -0
  35. src/js/level/disabledMap.js +42 -0
  36. src/js/level/index.js +672 -0
  37. src/js/level/parseWaterfall.js +122 -0
  38. src/js/log/index.js +31 -0
  39. src/js/mercurial/commands.js +261 -0
  40. src/js/models/collections.js +128 -0
  41. src/js/models/commandModel.js +309 -0
  42. src/js/react_views/CommandHistoryView.jsx +107 -0
  43. src/js/react_views/CommandView.jsx +134 -0
  44. src/js/react_views/CommandsHelperBarView.jsx +71 -0
  45. src/js/react_views/HelperBarView.jsx +70 -0
  46. src/js/react_views/IntlHelperBarView.jsx +157 -0
  47. src/js/react_views/LevelToolbarView.jsx +89 -0
  48. src/js/react_views/MainHelperBarView.jsx +76 -0
  49. src/js/sandbox/commands.js +219 -0
  50. src/js/sandbox/index.js +487 -0
CNAME ADDED
@@ -0,0 +1 @@
 
 
1
+ learnGitBranching.js.org
LICENSE.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## MIT License
2
+
3
+ Copyright (c) 2012 Peter Cottle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -6,6 +6,7 @@ colorTo: green
6
  sdk: static
7
  pinned: false
8
  license: mit
 
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
6
  sdk: static
7
  pinned: false
8
  license: mit
9
+ app_file: /src/template.index.html
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
assets/favicon.ico ADDED
assets/font/fontawesome-webfont.eot ADDED
Binary file (38.7 kB). View file
 
assets/font/fontawesome-webfont.svg ADDED
assets/font/fontawesome-webfont.ttf ADDED
Binary file (68.5 kB). View file
 
assets/font/fontawesome-webfont.woff ADDED
Binary file (41.8 kB). View file
 
assets/learnGitBranching.png ADDED
checkgit.sh ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ GIT_STATUS=$(git status --porcelain | wc -l )
2
+ if [[ GIT_STATUS -ne 0 ]]; then
3
+ echo "${1:-Source files were modified}"
4
+ git status
5
+ exit $GIT_STATUS
6
+ fi;
gulpfile.js ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var { execSync } = require('child_process');
2
+ var {
3
+ writeFileSync, readdirSync, readFileSync,
4
+ existsSync, statSync, mkdirSync, copyFileSync,
5
+ } = require('fs');
6
+ var path = require('path');
7
+
8
+ var glob = require('glob');
9
+ var _ = require('underscore');
10
+
11
+ var { src, dest, series, watch } = require('gulp');
12
+ var log = require('fancy-log');
13
+ var gHash = require('gulp-hash');
14
+ var gClean = require('gulp-clean');
15
+ var concat = require('gulp-concat');
16
+ var cleanCSS = require('gulp-clean-css');
17
+ var gTerser = require('gulp-terser');
18
+ var gJasmine = require('gulp-jasmine');
19
+ var { minify } = require('html-minifier');
20
+ var { SpecReporter } = require('jasmine-spec-reporter');
21
+ var gJshint = require('gulp-jshint');
22
+
23
+ var source = require('vinyl-source-stream');
24
+ var buffer = require('vinyl-buffer');
25
+ var browserify = require('browserify');
26
+ var babelify = require('babelify');
27
+
28
+ _.templateSettings.interpolate = /\{\{(.+?)\}\}/g;
29
+ _.templateSettings.escape = /\{\{\{(.*?)\}\}\}/g;
30
+ _.templateSettings.evaluate = /\{\{-(.*?)\}\}/g;
31
+
32
+ // precompile for speed
33
+ var indexFile = readFileSync('src/template.index.html').toString();
34
+ var indexTemplate = _.template(indexFile);
35
+
36
+ var compliments = [
37
+ 'Thanks to Hong4rc for the modern and amazing gulp workflow!',
38
+ 'I hope you all have a great day :)'
39
+ ];
40
+ var compliment = (done) => {
41
+ var index = Math.floor(Math.random() * compliments.length);
42
+
43
+ log(compliments[index]);
44
+ done();
45
+ };
46
+
47
+ const lintStrings = (done) => {
48
+ execSync('node src/js/intl/checkStrings');
49
+ done();
50
+ };
51
+
52
+
53
+ var destDir = './build/';
54
+
55
+ var copyRecursiveSync = (src, dest) => {
56
+ var exists = existsSync(src);
57
+ var stats = exists && statSync(src);
58
+ var isDirectory = exists && stats.isDirectory();
59
+ if (isDirectory) {
60
+ mkdirSync(dest);
61
+ readdirSync(src).forEach((childItemName) => {
62
+ copyRecursiveSync(
63
+ path.join(src, childItemName),
64
+ path.join(dest, childItemName));
65
+ });
66
+ } else {
67
+ copyFileSync(src, dest);
68
+ }
69
+ };
70
+
71
+ var buildIndex = function(done) {
72
+ log('Building index...');
73
+
74
+ // first find the one in here that we want
75
+ var buildFiles = readdirSync(destDir);
76
+
77
+ var jsRegex = /bundle-[\.\w]+\.js/;
78
+ var jsFile = buildFiles.find(function(name) {
79
+ return jsRegex.exec(name);
80
+ });
81
+ if (!jsFile) {
82
+ throw new Error('no hashed min file found!');
83
+ }
84
+ log('Found hashed js file: ' + jsFile);
85
+
86
+ var styleRegex = /main-[\.\w]+\.css/;
87
+ var styleFile = buildFiles.find(function(name) {
88
+ return styleRegex.exec(name);
89
+ });
90
+ if (!styleFile) {
91
+ throw new Error('no hashed css file found!');
92
+ }
93
+ log('Found hashed style file: ' + styleFile);
94
+
95
+ var buildDir = process.env.CI ? '.' : 'build';
96
+
97
+ // output these filenames to our index template
98
+ var outputIndex = indexTemplate({
99
+ buildDir,
100
+ jsFile,
101
+ styleFile,
102
+ });
103
+
104
+ if (process.env.NODE_ENV === 'production') {
105
+ outputIndex = minify(outputIndex, {
106
+ minifyJS: true,
107
+ collapseWhitespace: true,
108
+ processScripts: ['text/html'],
109
+ removeComments: true,
110
+ });
111
+ }
112
+
113
+ if (process.env.CI) {
114
+ writeFileSync('build/index.html', outputIndex);
115
+ copyRecursiveSync('assets', 'build/assets');
116
+ } else {
117
+ writeFileSync('index.html', outputIndex);
118
+ }
119
+ done();
120
+ };
121
+
122
+ var getBundle = function() {
123
+ return browserify({
124
+ entries: [...glob.sync('src/**/*.js'), ...glob.sync('src/**/*.jsx')],
125
+ debug: true,
126
+ })
127
+ .transform(babelify, { presets: ['@babel/preset-react'] })
128
+ .bundle()
129
+ .pipe(source('bundle.js'))
130
+ .pipe(buffer())
131
+ .pipe(gHash());
132
+ };
133
+
134
+ var clean = function () {
135
+ return src(destDir, { read: false, allowEmpty: true })
136
+ .pipe(gClean());
137
+ };
138
+
139
+ var jshint = function() {
140
+ return src([
141
+ 'gulpfile.js',
142
+ '__tests__/',
143
+ 'src/'
144
+ ])
145
+ .pipe(gJshint())
146
+ .pipe(gJshint.reporter('default'));
147
+ };
148
+
149
+ var ifyBuild = function() {
150
+ return getBundle()
151
+ .pipe(dest(destDir));
152
+ };
153
+
154
+ var miniBuild = function() {
155
+ process.env.NODE_ENV = 'production';
156
+ return getBundle()
157
+ .pipe(gTerser())
158
+ .pipe(dest(destDir));
159
+ };
160
+
161
+ var style = function() {
162
+ var chain = src('src/style/*.css')
163
+ .pipe(concat('main.css'));
164
+
165
+ if (process.env.NODE_ENV === 'production') {
166
+ chain = chain.pipe(cleanCSS());
167
+ }
168
+
169
+ return chain.pipe(gHash())
170
+ .pipe(dest(destDir));
171
+ };
172
+
173
+ var jasmine = function() {
174
+ return src('__tests__/*.spec.js')
175
+ .pipe(gJasmine({
176
+ config: {
177
+ verbose: true,
178
+ random: false,
179
+ },
180
+ reporter: new SpecReporter(),
181
+ }));
182
+ };
183
+
184
+ var gitAdd = function(done) {
185
+ execSync('git add build/');
186
+ done();
187
+ };
188
+
189
+ var gitDeployMergeMain = function(done) {
190
+ execSync('git checkout gh-pages && git merge main -m "merge main"');
191
+ done();
192
+ };
193
+
194
+ var gitDeployPushOrigin = function(done) {
195
+ execSync('git commit -am "rebuild for prod"; ' +
196
+ 'git push origin gh-pages --force && ' +
197
+ 'git branch -f trunk gh-pages && ' +
198
+ 'git checkout main'
199
+ );
200
+ done();
201
+ };
202
+
203
+ var fastBuild = series(clean, ifyBuild, style, buildIndex, jshint);
204
+
205
+ var build = series(
206
+ clean,
207
+ miniBuild, style, buildIndex,
208
+ gitAdd, jasmine, jshint,
209
+ lintStrings, compliment
210
+ );
211
+
212
+ var deploy = series(
213
+ clean,
214
+ jasmine,
215
+ jshint,
216
+ gitDeployMergeMain,
217
+ build,
218
+ gitDeployPushOrigin,
219
+ compliment
220
+ );
221
+
222
+ var lint = series(jshint, compliment);
223
+
224
+ var watching = function() {
225
+ return watch([
226
+ 'gulpfile.js',
227
+ '__tests__/git.spec.js',
228
+ 'src/js/**/*.js',
229
+ 'src/js/**/**/*.js',
230
+ 'src/levels/**/*.js'
231
+ ], series([fastBuild , jasmine, jshint, lintStrings]));
232
+ };
233
+
234
+ module.exports = {
235
+ default: build,
236
+ lint,
237
+ fastBuild,
238
+ watching,
239
+ build,
240
+ test: jasmine,
241
+ deploy,
242
+ };
package.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "LearnGitBranching",
3
+ "version": "0.8.0",
4
+ "description": "An interactive git visualization to challenge and educate!",
5
+ "homepage": "https://learngitbranching.js.org",
6
+ "author": "Peter Cottle <petermcottle@gmail.com>",
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "test": "gulp test"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/pcottle/learnGitBranching"
14
+ },
15
+ "devDependencies": {
16
+ "@babel/core": "^7.17.5",
17
+ "@babel/preset-react": "^7.16.7",
18
+ "babelify": "^10.0.0",
19
+ "browserify": "^17.0.0",
20
+ "fancy-log": "^1.3.3",
21
+ "glob": "^7.2.0",
22
+ "gulp": "^4.0.2",
23
+ "gulp-clean": "^0.4.0",
24
+ "gulp-clean-css": "^4.3.0",
25
+ "gulp-concat": "^2.6.1",
26
+ "gulp-hash": "^4.2.2",
27
+ "gulp-jasmine": "^4.0.0",
28
+ "gulp-jshint": "^2.1.0",
29
+ "gulp-terser": "^2.1.0",
30
+ "gulp-uglify": "^3.0.2",
31
+ "html-minifier": "^4.0.0",
32
+ "jasmine-spec-reporter": "^7.0.0",
33
+ "jshint": "^2.13.4",
34
+ "prompt": "^1.2.2",
35
+ "vinyl-buffer": "^1.0.1",
36
+ "vinyl-source-stream": "^2.0.0"
37
+ },
38
+ "dependencies": {
39
+ "backbone": "^1.4.0",
40
+ "flux": "^4.0.3",
41
+ "jquery": "^3.4.0",
42
+ "jquery-ui": "^1.13.1",
43
+ "marked": "^4.0.12",
44
+ "prop-types": "^15.8.1",
45
+ "q": "^1.5.1",
46
+ "raphael": "^2.1.0",
47
+ "react": "^17.0.2",
48
+ "react-dom": "^17.0.2",
49
+ "underscore": "^1.13.2"
50
+ }
51
+ }
src/js/actions/CommandLineActions.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var AppConstants = require('../constants/AppConstants');
4
+ var AppDispatcher = require('../dispatcher/AppDispatcher');
5
+
6
+ var ActionTypes = AppConstants.ActionTypes;
7
+
8
+ var CommandLineActions = {
9
+
10
+ submitCommand: function(text) {
11
+ AppDispatcher.handleViewAction({
12
+ type: ActionTypes.SUBMIT_COMMAND,
13
+ text: text
14
+ });
15
+ }
16
+
17
+ };
18
+
19
+ module.exports = CommandLineActions;
src/js/actions/GlobalStateActions.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var AppConstants = require('../constants/AppConstants');
4
+ var AppDispatcher = require('../dispatcher/AppDispatcher');
5
+
6
+ var ActionTypes = AppConstants.ActionTypes;
7
+
8
+ var GlobalStateActions = {
9
+
10
+ changeIsAnimating: function(isAnimating) {
11
+ AppDispatcher.handleViewAction({
12
+ type: ActionTypes.CHANGE_IS_ANIMATING,
13
+ isAnimating: isAnimating
14
+ });
15
+ },
16
+
17
+ levelSolved: function() {
18
+ AppDispatcher.handleViewAction({
19
+ type: ActionTypes.LEVEL_SOLVED,
20
+ });
21
+ },
22
+
23
+ disableLevelInstructions: function() {
24
+ AppDispatcher.handleViewAction({
25
+ type: ActionTypes.DISABLE_LEVEL_INSTRUCTIONS,
26
+ });
27
+ },
28
+
29
+ changeFlipTreeY: function(flipTreeY) {
30
+ AppDispatcher.handleViewAction({
31
+ type: ActionTypes.CHANGE_FLIP_TREE_Y,
32
+ flipTreeY: flipTreeY
33
+ });
34
+ }
35
+
36
+ };
37
+
38
+ module.exports = GlobalStateActions;
src/js/actions/LevelActions.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var AppConstants = require('../constants/AppConstants');
4
+ var AppDispatcher = require('../dispatcher/AppDispatcher');
5
+
6
+ var ActionTypes = AppConstants.ActionTypes;
7
+
8
+ var LevelActions = {
9
+
10
+ setLevelSolved: function(levelID) {
11
+ AppDispatcher.handleViewAction({
12
+ type: ActionTypes.SET_LEVEL_SOLVED,
13
+ levelID: levelID
14
+ });
15
+ },
16
+
17
+ resetLevelsSolved: function() {
18
+ AppDispatcher.handleViewAction({
19
+ type: ActionTypes.RESET_LEVELS_SOLVED
20
+ });
21
+ }
22
+
23
+ };
24
+
25
+ module.exports = LevelActions;
src/js/actions/LocaleActions.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var AppConstants = require('../constants/AppConstants');
4
+ var AppDispatcher = require('../dispatcher/AppDispatcher');
5
+
6
+ var ActionTypes = AppConstants.ActionTypes;
7
+
8
+ var LocaleActions = {
9
+
10
+ changeLocale: function(newLocale) {
11
+ AppDispatcher.handleViewAction({
12
+ type: ActionTypes.CHANGE_LOCALE,
13
+ locale: newLocale
14
+ });
15
+ },
16
+
17
+ changeLocaleFromURI: function(newLocale) {
18
+ AppDispatcher.handleURIAction({
19
+ type: ActionTypes.CHANGE_LOCALE,
20
+ locale: newLocale
21
+ });
22
+ },
23
+
24
+ changeLocaleFromHeader: function(header) {
25
+ AppDispatcher.handleViewAction({
26
+ type: ActionTypes.CHANGE_LOCALE_FROM_HEADER,
27
+ header: header
28
+ });
29
+ }
30
+ };
31
+
32
+ module.exports = LocaleActions;
src/js/app/index.js ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Backbone = require('backbone');
2
+ var jQuery = require('jquery');
3
+ var EventEmitter = require('events').EventEmitter;
4
+ var React = require('react');
5
+ var ReactDOM = require('react-dom');
6
+
7
+ var util = require('../util');
8
+ var intl = require('../intl');
9
+ var LocaleStore = require('../stores/LocaleStore');
10
+ var LocaleActions = require('../actions/LocaleActions');
11
+
12
+ /**
13
+ * Globals
14
+ */
15
+
16
+ Backbone.$ = jQuery;
17
+
18
+ // Bypass jasmine
19
+ if (util.isBrowser()) {
20
+ window.jQuery = jQuery;
21
+ window.$ = jQuery;
22
+ window.Raphael = require('raphael');
23
+ }
24
+
25
+ var events = Object.assign(
26
+ {},
27
+ EventEmitter.prototype,
28
+ {
29
+ trigger: function() {
30
+ // alias this for backwards compatibility
31
+ this.emit.apply(this, arguments);
32
+ }
33
+ }
34
+ );
35
+ // Allow unlimited listeners, so FF doesn't break
36
+ events.setMaxListeners(0);
37
+ var commandUI;
38
+ var sandbox;
39
+ var eventBaton;
40
+ var levelDropdown;
41
+
42
+ ///////////////////////////////////////////////////////////////////////
43
+
44
+ var init = function() {
45
+ /**
46
+ * There is a decent amount of bootstrapping we need just to hook
47
+ * everything up. The init() method takes on these responsibilities,
48
+ * including but not limited to:
49
+ * - setting up Events and EventBaton
50
+ * - calling the constructor for the main visualization
51
+ * - initializing the command input bar
52
+ * - handling window.focus and zoom events
53
+ **/
54
+ var Sandbox = require('../sandbox/').Sandbox;
55
+ var EventBaton = require('../util/eventBaton').EventBaton;
56
+ var LevelDropdownView = require('../views/levelDropdownView').LevelDropdownView;
57
+
58
+ eventBaton = new EventBaton();
59
+ commandUI = new CommandUI();
60
+ sandbox = new Sandbox();
61
+ levelDropdown = new LevelDropdownView({
62
+ wait: true
63
+ });
64
+
65
+ LocaleStore.subscribe(function() {
66
+ if (LocaleStore.getLocale() !== LocaleStore.getDefaultLocale()) {
67
+ intlRefresh();
68
+ }
69
+ });
70
+ events.on('vcsModeChange', vcsModeRefresh);
71
+
72
+ initRootEvents(eventBaton);
73
+ initDemo(sandbox);
74
+ // unfortunate global export for casper tests
75
+ window.LocaleStore = LocaleStore;
76
+ window.LocaleActions = LocaleActions;
77
+ window.intl = intl;
78
+ };
79
+
80
+ var vcsModeRefresh = function(eventData) {
81
+ if (!window.$) { return; }
82
+
83
+ var mode = eventData.mode;
84
+ var isGit = eventData.mode === 'git';
85
+
86
+ var displayMode = mode.slice(0, 1).toUpperCase() + mode.slice(1);
87
+ var otherMode = (displayMode === 'Git') ? 'Hg' : 'Git';
88
+ var regex = new RegExp(otherMode, 'g');
89
+
90
+ document.title = intl.str('learn-git-branching').replace(regex, displayMode);
91
+ $('span.vcs-mode-aware').each(function(i, el) {
92
+ var text = $(el).text().replace(regex, displayMode);
93
+ $(el).text(text);
94
+ });
95
+
96
+ $('body').toggleClass('gitMode', isGit);
97
+ $('body').toggleClass('hgMode', !isGit);
98
+ };
99
+
100
+ var insertAlternateLinks = function(pageId) {
101
+ // For now pageId is null, which would link to the main page.
102
+ // In future if pageId is provided this method should link to a specific page
103
+
104
+ // The value of the hreflang attribute identifies the language (in ISO 639-1 format)
105
+ // and optionally a region (in ISO 3166-1 Alpha 2 format) of an alternate URL
106
+
107
+ var altLinks = LocaleStore.getSupportedLocales().map(function(langCode) {
108
+ var url = "https://learngitbranching.js.org/?locale=" + langCode;
109
+ return '<link rel="alternate" hreflang="'+langCode+'" href="' + url +'" />';
110
+ });
111
+ var defaultUrl = "https://learngitbranching.js.org/?locale=" + LocaleStore.getDefaultLocale();
112
+ altLinks.push('<link rel="alternate" hreflang="x-default" href="' + defaultUrl +'" />');
113
+ $('head').prepend(altLinks);
114
+
115
+ };
116
+
117
+ var intlRefresh = function() {
118
+ if (!window.$) { return; }
119
+ var countryCode = LocaleStore.getLocale().split("_")[0];
120
+ $("html").attr('lang', countryCode);
121
+ $("meta[http-equiv='content-language']").attr("content", countryCode);
122
+ $('span.intl-aware').each(function(i, el) {
123
+ var intl = require('../intl');
124
+ var key = $(el).attr('data-intl');
125
+ $(el).text(intl.str(key));
126
+ });
127
+ };
128
+
129
+ var initRootEvents = function(eventBaton) {
130
+ // we always want to focus the text area to collect input
131
+ var focusTextArea = function() {
132
+ $('#commandTextField').focus();
133
+ };
134
+ focusTextArea();
135
+
136
+ $(window).focus(function(e) {
137
+ eventBaton.trigger('windowFocus', e);
138
+ });
139
+ $(document).click(function(e) {
140
+ eventBaton.trigger('documentClick', e);
141
+ });
142
+ $(document).bind('keydown', function(e) {
143
+ eventBaton.trigger('docKeydown', e);
144
+ });
145
+ $(document).bind('keyup', function(e) {
146
+ eventBaton.trigger('docKeyup', e);
147
+ });
148
+ $(window).on('resize', function(e) {
149
+ events.trigger('resize', e);
150
+ });
151
+
152
+ eventBaton.stealBaton('docKeydown', function() { });
153
+ eventBaton.stealBaton('docKeyup', function() { });
154
+
155
+ // the default action on window focus and document click is to just focus the text area
156
+ eventBaton.stealBaton('windowFocus', focusTextArea);
157
+ eventBaton.stealBaton('documentClick', focusTextArea);
158
+
159
+ // but when the input is fired in the text area, we pipe that to whoever is
160
+ // listenining
161
+ var makeKeyListener = function(name) {
162
+ return function() {
163
+ var args = [name];
164
+ Array.prototype.slice.apply(arguments).forEach(function(arg) {
165
+ args.push(arg);
166
+ });
167
+ eventBaton.trigger.apply(eventBaton, args);
168
+ };
169
+ };
170
+
171
+ $('#commandTextField').on('keydown', makeKeyListener('keydown'));
172
+ $('#commandTextField').on('keyup', makeKeyListener('keyup'));
173
+ $(window).trigger('resize');
174
+ };
175
+
176
+ var initDemo = function(sandbox) {
177
+ var params = util.parseQueryString(window.location.href);
178
+
179
+ // being the smart programmer I am (not), I don't include a true value on demo, so
180
+ // I have to check if the key exists here
181
+ var commands;
182
+ if (/(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent) || /android/i.test(navigator.userAgent)) {
183
+ sandbox.mainVis.customEvents.on('gitEngineReady', function() {
184
+ eventBaton.trigger('commandSubmitted', 'mobile alert');
185
+ });
186
+ }
187
+
188
+ if (params.hasOwnProperty('demo')) {
189
+ commands = [
190
+ "git commit; git checkout -b bugFix C1; git commit; git merge main; git checkout main; git commit; git rebase bugFix;",
191
+ "delay 1000; reset;",
192
+ "level advanced1 --noFinishDialog --noStartCommand --noIntroDialog;",
193
+ "delay 2000; show goal; delay 1000; hide goal;",
194
+ "git checkout bugFix; git rebase main; git checkout side; git rebase bugFix;",
195
+ "git checkout another; git rebase side; git rebase another main;",
196
+ "help; levels"
197
+ ];
198
+ } else if (params.hasOwnProperty('hgdemo')) {
199
+ commands = [
200
+ 'importTreeNow {"branches":{"main":{"target":"C3","id":"main"},"feature":{"target":"C2","id":"feature"},"debug":{"target":"C4","id":"debug"}},"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"],"id":"C4"}},"HEAD":{"target":"feature","id":"HEAD"}}',
201
+ 'delay 1000',
202
+ 'git rebase main',
203
+ 'delay 1000',
204
+ 'undo',
205
+ 'hg book',
206
+ 'delay 1000',
207
+ 'hg rebase -d main'
208
+ ];
209
+ commands = commands.join(';#').split('#'); // hax
210
+ } else if (params.hasOwnProperty('hgdemo2')) {
211
+ commands = [
212
+ 'importTreeNow {"branches":{"main":{"target":"C3","id":"main"},"feature":{"target":"C2","id":"feature"},"debug":{"target":"C4","id":"debug"}},"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"],"id":"C4"}},"HEAD":{"target":"debug","id":"HEAD"}}',
213
+ 'delay 1000',
214
+ 'git rebase main',
215
+ 'delay 1000',
216
+ 'undo',
217
+ 'hg sum',
218
+ 'delay 1000',
219
+ 'hg rebase -d main'
220
+ ];
221
+ commands = commands.join(';#').split('#'); // hax
222
+ } else if (params.hasOwnProperty('remoteDemo')) {
223
+ commands = [
224
+ 'git clone',
225
+ 'git commit',
226
+ 'git fakeTeamwork',
227
+ 'git pull',
228
+ 'git push',
229
+ 'git commit',
230
+ 'git fakeTeamwork',
231
+ 'git pull --rebase',
232
+ 'git push',
233
+ 'levels'
234
+ ];
235
+ commands = commands.join(';#').split('#'); // hax
236
+ } else if (params.gist_level_id) {
237
+ $.ajax({
238
+ url: 'https://api.github.com/gists/' + params.gist_level_id,
239
+ type: 'GET',
240
+ dataType: 'jsonp',
241
+ success: function(response) {
242
+ var data = response.data || {};
243
+ var files = data.files || {};
244
+ if (!Object.keys(files).length) {
245
+ console.warn('no files found');
246
+ return;
247
+ }
248
+ var file = files[Object.keys(files)[0]];
249
+ if (!file.content) {
250
+ console.warn('file empty');
251
+ }
252
+ eventBaton.trigger(
253
+ 'commandSubmitted',
254
+ 'importLevelNow ' + escape(file.content) + '; clear; show goal;'
255
+ );
256
+ }
257
+ });
258
+ } else if (!params.hasOwnProperty('NODEMO')) {
259
+ commands = [
260
+ "help;",
261
+ "levels"
262
+ ];
263
+ }
264
+ if (params.hasOwnProperty('STARTREACT')) {
265
+ /*
266
+ ReactDOM.render(
267
+ React.createElement(CommandView, {}),
268
+ document.getElementById(params['STARTREACT'])
269
+ );*/
270
+ }
271
+ if (commands) {
272
+ sandbox.mainVis.customEvents.on('gitEngineReady', function() {
273
+ eventBaton.trigger('commandSubmitted', commands.join(''));
274
+ });
275
+ }
276
+
277
+ if (params.locale !== undefined && params.locale.length) {
278
+ LocaleActions.changeLocaleFromURI(params.locale);
279
+ } else {
280
+ tryLocaleDetect();
281
+ }
282
+
283
+ insertAlternateLinks();
284
+
285
+ if (params.command) {
286
+ var command = unescape(params.command);
287
+ sandbox.mainVis.customEvents.on('gitEngineReady', function() {
288
+ eventBaton.trigger('commandSubmitted', command);
289
+ });
290
+ }
291
+
292
+ };
293
+
294
+ function tryLocaleDetect() {
295
+ // use navigator to get the locale setting
296
+ changeLocaleFromHeaders(navigator.language || navigator.browserLanguage);
297
+ }
298
+
299
+ function changeLocaleFromHeaders(langString) {
300
+ LocaleActions.changeLocaleFromHeader(langString);
301
+ }
302
+
303
+ if (require('../util').isBrowser()) {
304
+ // this file gets included via node sometimes as well
305
+ $(document).ready(init);
306
+ }
307
+
308
+ /**
309
+ * the UI method simply bootstraps the command buffer and
310
+ * command prompt views. It only interacts with user input
311
+ * and simply pipes commands to the main events system
312
+ **/
313
+ function CommandUI() {
314
+ Backbone.$ = $; // lol WTF BACKBONE MANAGE YOUR DEPENDENCIES
315
+ var Views = require('../views');
316
+ var Collections = require('../models/collections');
317
+ var CommandViews = require('../views/commandViews');
318
+ var CommandHistoryView = require('../react_views/CommandHistoryView.jsx');
319
+ var MainHelperBarView = require('../react_views/MainHelperBarView.jsx');
320
+
321
+ this.commandCollection = new Collections.CommandCollection();
322
+ this.commandBuffer = new Collections.CommandBuffer({
323
+ collection: this.commandCollection
324
+ });
325
+
326
+ this.commandPromptView = new CommandViews.CommandPromptView({
327
+ el: $('#commandLineBar')
328
+ });
329
+
330
+ ReactDOM.render(
331
+ React.createElement(MainHelperBarView),
332
+ document.getElementById('helperBarMount')
333
+ );
334
+ ReactDOM.render(
335
+ React.createElement(
336
+ CommandHistoryView,
337
+ { commandCollection: this.commandCollection }
338
+ ),
339
+ document.getElementById('commandDisplay')
340
+ );
341
+ }
342
+
343
+ exports.getEvents = function() {
344
+ return events;
345
+ };
346
+
347
+ exports.getSandbox = function() {
348
+ return sandbox;
349
+ };
350
+
351
+ exports.getEventBaton = function() {
352
+ return eventBaton;
353
+ };
354
+
355
+ exports.getCommandUI = function() {
356
+ return commandUI;
357
+ };
358
+
359
+ exports.getLevelDropdown = function() {
360
+ return levelDropdown;
361
+ };
362
+
363
+ exports.init = init;
src/js/commands/index.js ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var intl = require('../intl');
2
+
3
+ var Errors = require('../util/errors');
4
+ var GitCommands = require('../git/commands');
5
+ var MercurialCommands = require('../mercurial/commands');
6
+
7
+ var CommandProcessError = Errors.CommandProcessError;
8
+ var CommandResult = Errors.CommandResult;
9
+
10
+ var commandConfigs = {
11
+ 'git': GitCommands.commandConfig,
12
+ 'hg': MercurialCommands.commandConfig
13
+ };
14
+
15
+ var commands = {
16
+ execute: function(vcs, name, engine, commandObj) {
17
+ if (!commandConfigs[vcs][name]) {
18
+ throw new Error('i don\'t have a command for ' + name);
19
+ }
20
+ var config = commandConfigs[vcs][name];
21
+ if (config.delegate) {
22
+ return this.delegateExecute(config, engine, commandObj);
23
+ }
24
+
25
+ config.execute.call(this, engine, commandObj);
26
+ },
27
+
28
+ delegateExecute: function(config, engine, commandObj) {
29
+ // we have delegated to another vcs command, so lets
30
+ // execute that and get the result
31
+ var result = config.delegate.call(this, engine, commandObj);
32
+
33
+ if (result.multiDelegate) {
34
+ // we need to do multiple delegations with
35
+ // a different command at each step
36
+ result.multiDelegate.forEach(function(delConfig) {
37
+ // copy command, and then set opts
38
+ commandObj.setOptionsMap(delConfig.options || {});
39
+ commandObj.setGeneralArgs(delConfig.args || []);
40
+
41
+ commandConfigs[delConfig.vcs][delConfig.name].execute.call(this, engine, commandObj);
42
+ }, this);
43
+ } else {
44
+ config = commandConfigs[result.vcs][result.name];
45
+ // commandObj is PASSED BY REFERENCE
46
+ // and modified in the function
47
+ commandConfigs[result.vcs][result.name].execute.call(this, engine, commandObj);
48
+ }
49
+ },
50
+
51
+ blankMap: function() {
52
+ return {git: {}, hg: {}};
53
+ },
54
+
55
+ getShortcutMap: function() {
56
+ var map = this.blankMap();
57
+ this.loop(function(config, name, vcs) {
58
+ if (!config.sc) {
59
+ return;
60
+ }
61
+ map[vcs][name] = config.sc;
62
+ }, this);
63
+ return map;
64
+ },
65
+
66
+ getOptionMap: function() {
67
+ var optionMap = this.blankMap();
68
+ this.loop(function(config, name, vcs) {
69
+ var displayName = config.displayName || name;
70
+ var thisMap = {};
71
+ // start all options off as disabled
72
+ (config.options || []).forEach(function(option) {
73
+ thisMap[option] = false;
74
+ });
75
+ optionMap[vcs][displayName] = thisMap;
76
+ });
77
+ return optionMap;
78
+ },
79
+
80
+ getRegexMap: function() {
81
+ var map = this.blankMap();
82
+ this.loop(function(config, name, vcs) {
83
+ var displayName = config.displayName || name;
84
+ map[vcs][displayName] = config.regex;
85
+ });
86
+ return map;
87
+ },
88
+
89
+ /**
90
+ * which commands count for the git golf game
91
+ */
92
+ getCommandsThatCount: function() {
93
+ var counted = this.blankMap();
94
+ this.loop(function(config, name, vcs) {
95
+ if (config.dontCountForGolf) {
96
+ return;
97
+ }
98
+ counted[vcs][name] = config.regex;
99
+ });
100
+ return counted;
101
+ },
102
+
103
+ loop: function(callback, context) {
104
+ Object.keys(commandConfigs).forEach(function(vcs) {
105
+ var commandConfig = commandConfigs[vcs];
106
+ Object.keys(commandConfig).forEach(function(name) {
107
+ var config = commandConfig[name];
108
+ callback(config, name, vcs);
109
+ });
110
+ });
111
+ }
112
+ };
113
+
114
+ var parse = function(str) {
115
+ var vcs;
116
+ var method;
117
+ var options;
118
+
119
+ // see if we support this particular command
120
+ var regexMap = commands.getRegexMap();
121
+ Object.keys(regexMap).forEach(function (thisVCS) {
122
+ var map = regexMap[thisVCS];
123
+ Object.keys(map).forEach(function(thisMethod) {
124
+ var regex = map[thisMethod];
125
+ if (regex.exec(str)) {
126
+ vcs = thisVCS;
127
+ method = thisMethod;
128
+ // every valid regex has to have the parts of
129
+ // <vcs> <command> <stuff>
130
+ // because there are always two space-groups
131
+ // before our "stuff" we can simply
132
+ // split on space-groups and grab everything after
133
+ // the second:
134
+ options = str.match(/('.*?'|".*?"|\S+)/g).slice(2);
135
+ }
136
+ });
137
+ });
138
+
139
+ if (!method) {
140
+ return false;
141
+ }
142
+
143
+ // we support this command!
144
+ // parse off the options and assemble the map / general args
145
+ var parsedOptions = new CommandOptionParser(vcs, method, options);
146
+ var error = parsedOptions.explodeAndSet();
147
+ return {
148
+ toSet: {
149
+ generalArgs: parsedOptions.generalArgs,
150
+ supportedMap: parsedOptions.supportedMap,
151
+ error: error,
152
+ vcs: vcs,
153
+ method: method,
154
+ options: options,
155
+ eventName: 'processGitCommand'
156
+ }
157
+ };
158
+ };
159
+
160
+ /**
161
+ * CommandOptionParser
162
+ */
163
+ function CommandOptionParser(vcs, method, options) {
164
+ this.vcs = vcs;
165
+ this.method = method;
166
+ this.rawOptions = options;
167
+
168
+ this.supportedMap = commands.getOptionMap()[vcs][method];
169
+ if (this.supportedMap === undefined) {
170
+ throw new Error('No option map for ' + method);
171
+ }
172
+
173
+ this.generalArgs = [];
174
+ }
175
+
176
+ CommandOptionParser.prototype.explodeAndSet = function() {
177
+ for (var i = 0; i < this.rawOptions.length; i++) {
178
+ var part = this.rawOptions[i];
179
+
180
+ if (part.slice(0,1) == '-') {
181
+ // it's an option, check supportedMap
182
+ if (this.supportedMap[part] === undefined) {
183
+ return new CommandProcessError({
184
+ msg: intl.str(
185
+ 'option-not-supported',
186
+ { option: part }
187
+ )
188
+ });
189
+ }
190
+
191
+ var next = this.rawOptions[i + 1];
192
+ var optionArgs = [];
193
+ if (next && next.slice(0,1) !== '-') {
194
+ // only store the next argument as this
195
+ // option value if its not another option
196
+ i++;
197
+ optionArgs = [next];
198
+ }
199
+ this.supportedMap[part] = optionArgs;
200
+ } else {
201
+ // must be a general arg
202
+ this.generalArgs.push(part);
203
+ }
204
+ }
205
+ };
206
+
207
+ exports.commands = commands;
208
+ exports.parse = parse;
src/js/constants/AppConstants.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var keyMirror = require('../util/keyMirror');
4
+
5
+ var CHANGE_EVENT = 'change';
6
+
7
+ module.exports = {
8
+
9
+ CHANGE_EVENT: CHANGE_EVENT,
10
+
11
+ StoreSubscribePrototype: {
12
+ subscribe: function(cb) {
13
+ this.on(CHANGE_EVENT, cb);
14
+ },
15
+
16
+ unsubscribe: function(cb) {
17
+ this.removeListener(CHANGE_EVENT, cb);
18
+ }
19
+ },
20
+
21
+ ActionTypes: keyMirror({
22
+ SET_LEVEL_SOLVED: null,
23
+ RESET_LEVELS_SOLVED: null,
24
+ CHANGE_IS_ANIMATING: null,
25
+ CHANGE_FLIP_TREE_Y: null,
26
+ SUBMIT_COMMAND: null,
27
+ CHANGE_LOCALE: null,
28
+ CHANGE_LOCALE_FROM_HEADER: null,
29
+ DISABLE_LEVEL_INSTRUCTIONS: null,
30
+ /**
31
+ * only dispatched when you actually
32
+ * solve the level, not ask for solution
33
+ * or solve it again.
34
+ */
35
+ SOLVE_LEVEL: null
36
+ }),
37
+
38
+ PayloadSources: keyMirror({
39
+ VIEW_ACTION: null,
40
+ URI_ACTION: null
41
+ })
42
+ };
src/js/dialogs/confirmShowSolution.js ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ exports.dialog = {
2
+ 'en_US': [{
3
+ type: 'ModalAlert',
4
+ options: {
5
+ markdowns: [
6
+ '## Are you sure you want to see the solution?',
7
+ '',
8
+ 'I believe in you! You can do it'
9
+ ]
10
+ }
11
+ }],
12
+ 'de_DE': [{
13
+ type: 'ModalAlert',
14
+ options: {
15
+ markdowns: [
16
+ '## Bist du sicher, dass du die Auflösung sehen willst?',
17
+ '',
18
+ 'Ich glaube an dich! Du schaffst das!'
19
+ ]
20
+ }
21
+ }],
22
+ 'zh_CN': [{
23
+ type: 'ModalAlert',
24
+ options: {
25
+ markdowns: [
26
+ '## 确定要看答案吗?',
27
+ '',
28
+ '相信自己,你可以的!'
29
+ ]
30
+ }
31
+ }],
32
+ 'zh_TW': [{
33
+ type: 'ModalAlert',
34
+ options: {
35
+ markdowns: [
36
+ '## 確定偷看解答嗎?',
37
+ '',
38
+ '我相信你!你可以的'
39
+ ]
40
+ }
41
+ }],
42
+ 'es_AR': [{
43
+ type: 'ModalAlert',
44
+ options: {
45
+ markdowns: [
46
+ '## ¿Realmente querés ver la solución?',
47
+ '',
48
+ '¡Creo en vos! ¡Dale que podés!'
49
+ ]
50
+ }
51
+ }],
52
+ 'es_MX': [{
53
+ type: 'ModalAlert',
54
+ options: {
55
+ markdowns: [
56
+ '## ¿Estás seguro de que quieres ver la solución?',
57
+ '',
58
+ '¡Creo en ti! ¡Yo sé que puedes!'
59
+ ]
60
+ }
61
+ }],
62
+ 'es_ES': [{
63
+ type: 'ModalAlert',
64
+ options: {
65
+ markdowns: [
66
+ '## ¿Estás seguro de que quieres ver la solución?',
67
+ '',
68
+ '¡Creo en ti! ¡Ánimo!'
69
+ ]
70
+ }
71
+ }],
72
+ 'pt_BR': [{
73
+ type: 'ModalAlert',
74
+ options: {
75
+ markdowns: [
76
+ '## Tem certeza que quer ver a solução?',
77
+ '',
78
+ 'Vamos lá, acredito que você consegue!'
79
+ ]
80
+ }
81
+ }],
82
+ 'gl': [{
83
+ type: 'ModalAlert',
84
+ options: {
85
+ markdowns: [
86
+ '## ¿Queres ver a solución?',
87
+ '',
88
+ 'Seguro que podes, ¡inténtao unha vez máis!'
89
+ ]
90
+ }
91
+ }],
92
+ 'fr_FR': [{
93
+ type: 'ModalAlert',
94
+ options: {
95
+ markdowns: [
96
+ '## Êtes-vous sûr de vouloir voir la solution ?',
97
+ '',
98
+ 'Je crois en vous ! Vous pouvez le faire !'
99
+ ]
100
+ }
101
+ }],
102
+ 'ja': [{
103
+ type: 'ModalAlert',
104
+ options: {
105
+ markdowns: [
106
+ '## どうしても正解がみたいですか?',
107
+ '',
108
+ '頑張れ頑張れできるできる絶対できる頑張れもっとやれるって',
109
+ '',
110
+ 'やれる気持ちの問題だ頑張れ頑張れそこだ!',
111
+ '',
112
+ 'そこで諦めるな絶対に頑張れ積極的にポジティブに頑張る頑張る',
113
+ '',
114
+ '北京だって頑張ってるんだから!'
115
+ ]
116
+ }
117
+ }],
118
+ 'ru_RU': [{
119
+ type: 'ModalAlert',
120
+ options: {
121
+ markdowns: [
122
+ '## Уверен, что хочешь посмотреть решение?',
123
+ '',
124
+ 'Мы верим в тебя! Не прыгай! Ты сможешь!'
125
+ ]
126
+ }
127
+ }],
128
+ 'uk': [{
129
+ type: 'ModalAlert',
130
+ options: {
131
+ markdowns: [
132
+ '## Впевнений, що хочеш побачити розв’язок?',
133
+ '',
134
+ 'Я вірю в тебе! Ти впораєшся!'
135
+ ]
136
+ }
137
+ }],
138
+ 'vi': [{
139
+ type: 'ModalAlert',
140
+ options: {
141
+ markdowns: [
142
+ '## Bạn chắc là muốn xem đáp án chứ?',
143
+ '',
144
+ 'Tôi tin ở bạn! Bạn có thể làm được!'
145
+ ]
146
+ }
147
+ }],
148
+ 'sl_SI': [{
149
+ type: 'ModalAlert',
150
+ options: {
151
+ markdowns: [
152
+ '## Si prepričan, da hočeš videti rešitev?',
153
+ '',
154
+ 'Verjamem vate! Maš ti to! Ali pač ne?'
155
+ ]
156
+ }
157
+ }],
158
+ 'pl': [{
159
+ type: 'ModalAlert',
160
+ options: {
161
+ markdowns: [
162
+ '## Czy na pewno chcesz zobaczyć rozwiązanie?',
163
+ '',
164
+ 'Wierzę w Ciebie! Możesz to zrobić'
165
+ ]
166
+ }
167
+ }],
168
+ 'ta_IN': [{
169
+ type: 'ModalAlert',
170
+ options: {
171
+ markdowns: [
172
+ '## நீங்கள் நிச்சயமாக தீர்வை காண விரும்புகிறீர்களா?',
173
+ '',
174
+ 'நான் உங்களால் அதை செய்ய முடியும் என நினைக்கிறேன்!'
175
+ ]
176
+ }
177
+ }],
178
+ "it_IT": [
179
+ {
180
+ type: "ModalAlert",
181
+ options: {
182
+ markdowns: [
183
+ "## Sicuro di voler sbirciare la soluzione?",
184
+ "",
185
+ "Io credo in te, dai che ce la fai!",
186
+ ],
187
+ },
188
+ },
189
+ ],
190
+ };
191
+
192
+
src/js/dialogs/levelBuilder.js ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ exports.dialog = {
2
+ 'en_US': [{
3
+ type: 'ModalAlert',
4
+ options: {
5
+ markdowns: [
6
+ '## Welcome to the level builder!',
7
+ '',
8
+ 'Here are the main steps:',
9
+ '',
10
+ ' * Set up the initial environment with git commands',
11
+ ' * Define the starting tree with ```define start```',
12
+ ' * Enter the series of git commands that compose the (optimal) solution',
13
+ ' * Define the goal tree with ```define goal```. Defining the goal also defines the solution',
14
+ ' * Optionally define a hint with ```define hint```',
15
+ ' * Edit the name with ```define name```',
16
+ ' * Optionally define a nice start dialog with ```edit dialog```',
17
+ ' * Enter the command ```finish``` to output your level JSON!'
18
+ ]
19
+ }
20
+ }],
21
+ 'de_DE': [{
22
+ type: 'ModalAlert',
23
+ options: {
24
+ markdowns: [
25
+ '## Willkommen zum Level-Editor!',
26
+ '',
27
+ 'So funktioniert\'s:',
28
+ '',
29
+ ' * Stelle mit Git-Befehlen die Ausgangssituation her',
30
+ ' * Leg den Startpunkt mit ```define start``` fest',
31
+ ' * Gib eine Abfolge von Git-Befehlen ein, welche die (optimale) Lösung darstellen',
32
+ ' * Leg den Ziel-Baum mit ```define goal``` fest. Damit markierst du den Endpunkt der Lösung',
33
+ ' * Gib einen Hinweis mittels ```define hint``` an, wenn du willst',
34
+ ' * Änder den Namen mittels ```define name```',
35
+ ' * Wenn du magst, erstelle einen schönen Einführungsdialog mit ```edit dialog```',
36
+ ' * Gib das Kommando ```finish``` ein um deinen Level als JSON auszugeben'
37
+ ]
38
+ }
39
+ }],
40
+ 'zh_CN': [{
41
+ type: 'ModalAlert',
42
+ options: {
43
+ markdowns: [
44
+ '## 欢迎使用关卡生成器!',
45
+ '',
46
+ '关键步骤如下:',
47
+ '',
48
+ ' * 用 Git 命令建立初始环境',
49
+ ' * 用 ```define start``` 命令定义初始提交树',
50
+ ' * 输入一系列 Git 命令作为(最佳)答案',
51
+ ' * 用 ```define goal``` 命令定义目标提交树。定义目标的同时也定义了答案',
52
+ ' * (选做)还可以用 ```define hint``` 命令定义提示',
53
+ ' * 用 ```define name``` 命令设置关卡名称',
54
+ ' * (选做)还可以用 ```edit dialog``` 定义一个漂亮的开始对话框',
55
+ ' * 用 ```finish``` 命令就可以输出你的关卡的JSON数据了!'
56
+ ]
57
+ }
58
+ }],
59
+ 'zh_TW': [{
60
+ type: 'ModalAlert',
61
+ options: {
62
+ markdowns: [
63
+ '## 歡迎使用關卡編輯器!',
64
+ '',
65
+ '主要步驟如下:',
66
+ '',
67
+ ' * 使用 git 指令建立初始環境',
68
+ ' * 使用 ```define start``` 指令定義起始樹',
69
+ ' * 輸入一系列 git 命令,編好答案',
70
+ ' * 使用 ```define goal``` 指令定義目標樹。定義目標的同時定義答案',
71
+ ' * 還可以用 ```define hint``` 指令定義一個提示',
72
+ ' * 用 ```define name``` 修改名稱',
73
+ ' * 還可以用 ```edit dialog``` 定義一個漂亮的開始對話視窗',
74
+ ' * 輸入 ```finish``` 即可將您的關卡輸出為 JSON!'
75
+ ]
76
+ }
77
+ }],
78
+ 'es_AR': [{
79
+ type: 'ModalAlert',
80
+ options: {
81
+ markdowns: [
82
+ '## ¡Te damos la bienvenida al constructor de niveles!',
83
+ '',
84
+ 'Estos son los pasos principales:',
85
+ '',
86
+ ' * Prepará el entorno inicial usando comandos de Git',
87
+ ' * Definí el árbol inicial con ```define start```',
88
+ ' * Ingresá la serie de comandos de git que representan la solución óptima',
89
+ ' * Definí el árbol objetivo con ```define goal```. El objetivo también determina la solución',
90
+ ' * Opcionalmente, definí pistas con ```define hint```',
91
+ ' * Dale un nombre con ```define name```',
92
+ ' * Opcionalmente, definí un mensaje inicial con ```edit dialog```',
93
+ ' * ¡Ingresá el comando ```finish``` para obtener tu nivel en formato JSON!'
94
+ ]
95
+ }
96
+ }],
97
+ 'es_MX': [{
98
+ type: 'ModalAlert',
99
+ options: {
100
+ markdowns: [
101
+ '## ¡Bienvenido al constructor de niveles!',
102
+ '',
103
+ 'Estos son los pasos principales:',
104
+ '',
105
+ ' * Preparar el entorno inicial usando comandos de Git',
106
+ ' * Definir el árbol inicial con ```define start```',
107
+ ' * Introducir la serie de comandos de git que representan la solución óptima',
108
+ ' * Crear el árbol objetivo con ```define goal```. El objetivo también determina la solución',
109
+ ' * Opcionalmente, crea pistas con ```define hint```',
110
+ ' * Dale un nombre con ```define name```',
111
+ ' * Opcionalmente, crea un mensaje inicial con ```edit dialog```',
112
+ ' * ¡Introduce el comando ```finish``` para obtener tu nivel en formato JSON!'
113
+ ]
114
+ }
115
+ }],
116
+ 'es_ES': [{
117
+ type: 'ModalAlert',
118
+ options: {
119
+ markdowns: [
120
+ '## ¡Bienvenido al constructor de niveles!',
121
+ '',
122
+ 'Estos son los pasos principales:',
123
+ '',
124
+ ' * Prepara el entorno inicial usando comandos de Git',
125
+ ' * Define el árbol inicial con ```define start```',
126
+ ' * Introduce la serie de comandos de git que representan la solución óptima',
127
+ ' * Crea el árbol objetivo con ```define goal```. El objetivo también determina la solución',
128
+ ' * Opcionalmente, crea pistas con ```define hint```',
129
+ ' * Dale un nombre con ```define name```',
130
+ ' * Opcionalmente, crea un mensaje inicial con ```edit dialog```',
131
+ ' * ¡Introduce el comando ```finish``` para obtener tu nivel en formato JSON!'
132
+ ]
133
+ }
134
+ }],
135
+ 'pt_BR': [{
136
+ type: 'ModalAlert',
137
+ options: {
138
+ markdowns: [
139
+ '## Bem-vindo ao construtor de níveis!',
140
+ '',
141
+ 'Estes são os passos principais:',
142
+ '',
143
+ ' * Prepare o ambiente inicial usando comandos do Git',
144
+ ' * Define a árvore inicial com ```define start```',
145
+ ' * Insira a série de comandos do git que representam a solução ótima',
146
+ ' * Defina a árvore objetivo com ```define goal```. O objetivo também determina a solução',
147
+ ' * Opcionalmente, defina dicas com ```define hint```',
148
+ ' * Dê um nome com ```define name```',
149
+ ' * Opcionalmente, defina uma mensagem inicial com ```edit dialog```',
150
+ ' * Digite o comando ```finish``` para obter seu nível em formato JSON!'
151
+ ]
152
+ }
153
+ }],
154
+ 'gl': [{
155
+ type: 'ModalAlert',
156
+ options: {
157
+ markdowns: [
158
+ '## Benvido ó constructor de niveis!',
159
+ '',
160
+ 'Estes son os pasos principais:',
161
+ '',
162
+ ' * Prepara o eido inicial usando comandos de Git',
163
+ ' * Define a árbore inicial con ```define start```',
164
+ ' * Inserta a secuencia de comandos de git que representan a mellor solución',
165
+ ' * Define a árbore obxectivo con ```define goal```. O obxectivo tamén determina a solución',
166
+ ' * Opcionalmente, define axudas con ```define hint```',
167
+ ' * Dalle un nome con ```define name```',
168
+ ' * Opcionalmente, define unha mensaxe inicial con ```edit dialog```',
169
+ ' * Escribe o comando ```finish``` para obter seu nivel en formato JSON!'
170
+ ]
171
+ }
172
+ }],
173
+ 'fr_FR': [{
174
+ type: 'ModalAlert',
175
+ options: {
176
+ markdowns: [
177
+ '## Bienvenue dans l\'éditeur niveaux !',
178
+ '',
179
+ 'Voici les étapes principales :',
180
+ '',
181
+ ' * Mettez en place l\'environnement initial avec des commandes git',
182
+ ' * Définissez l\'arbre de départ avec ```define start```',
183
+ ' * Saisissez la série de commandes git qui composent la solution (optimale)',
184
+ ' * Définissez l\'arbre cible avec ```define goal```. Cela définit aussi la solution',
185
+ ' * Optionnellement, définissez un indice avec ```define hint```',
186
+ ' * Changez le nom avec ```define name```',
187
+ ' * Optionellement, definissez un joli dialogue de départ avec ```edit dialog```',
188
+ ' * Entrez la commande ```finish``` pour délivrer votre niveau JSON!'
189
+ ]
190
+ }
191
+ }],
192
+ 'ja': [{
193
+ type: 'ModalAlert',
194
+ options: {
195
+ markdowns: [
196
+ '## Levelエディタへようこそ!',
197
+ '',
198
+ 'ここでは、以下の主にステップを踏みます:',
199
+ '',
200
+ ' * Gitコマンドで初期設定をします',
201
+ ' * ```define start```で開始時のコミットツリーを定義します',
202
+ ' * 一連のGitコマンドの(最適な)解答を入力します',
203
+ ' * ```define goal```でゴールのコミットツリーを定義します(ゴールを定義するということは、解答を定義するということでもあります)',
204
+ ' * オプションで```define hint```でヒントを定義します',
205
+ ' * ```define name```で名前を編集します',
206
+ ' * オプションで```edit dialog```で良い感じに開始時のダイアログを定義します',
207
+ ' * ```finish```コマンドを打つことであなたのlevelがJSONで出力されます',
208
+ '',
209
+ '*Note: このダイアログは`help builder`で何回でも表示できます!活用してください!*'
210
+ ]
211
+ }
212
+ }],
213
+ 'ru_RU': [{
214
+ type: 'ModalAlert',
215
+ options: {
216
+ markdowns: [
217
+ '## Добро пожаловать в конструктор уровней!',
218
+ '',
219
+ 'Вот основные шаги:',
220
+ '',
221
+ ' * Настроить стартовое дерево при помощи команд git',
222
+ ' * Обозначить старовое дерево при помощи ```define start```',
223
+ ' * Ввести команды "оптимального" решения уровня',
224
+ ' * Обозначить цель уровня при помощи ```define goal```. Одновременно обозначится решение.',
225
+ ' * По желанию, можно указать подсказку при помощи ```define hint```',
226
+ ' * Указать название уровня при помощи ```define name```',
227
+ ' * По желанию, указать стартовое сообщение при помощи ```edit dialog```',
228
+ ' * Ввести ```finish``` и получить JSON с описанием уровня!'
229
+ ]
230
+ }
231
+ }],
232
+ 'uk': [{
233
+ type: 'ModalAlert',
234
+ options: {
235
+ markdowns: [
236
+ '## Ласкаво просимо до конструктора рівнів!',
237
+ '',
238
+ 'Ось основні кроки:',
239
+ '',
240
+ ' * Налаштувати початкове середовище за допомогою команд git',
241
+ ' * Визначити стартове дерево за допомогою ```define start```',
242
+ ' * Ввести набір команд, що описують (оптимальний) розв’язок',
243
+ ' * Визначити кінцеве дерево за допомогою ```define goal```. Одночасно це визначить розв’язок',
244
+ ' * Додатково можна задати підказку за допомогою ```define hint```',
245
+ ' * Редагувати назву рівня за допомогою ```define name```',
246
+ ' * Додатково можна вказати файний початковий діалог за допомогою ```edit dialog```',
247
+ ' * Ввести команду ```finish``` й отримати JSON з описом рівня!'
248
+ ]
249
+ }
250
+ }],
251
+ 'ko': [{
252
+ type: 'ModalAlert',
253
+ options: {
254
+ markdowns: [
255
+ '## 레벨 생성기 입니다. 환영합니다!',
256
+ '',
257
+ 'Here are the main steps:',
258
+ '',
259
+ ' * git 명령어로 초기 환경을 만들어주세요',
260
+ ' * 시작 트리를 ```define start```로 정의하세요',
261
+ ' * (최적화된)정답을 만드는 git 명령어들을 입력하세요',
262
+ ' * 골 트리를 ```define goal```로 정의해주세요. 골을 정의하면 정답도 같이 정의됩니다',
263
+ ' * ```define hint```로 원하면 힌트도 정의해줄수 있습니다',
264
+ ' * 문제의 이름을 ```define name```로 수정하세요',
265
+ ' * 시작 글이 필요하다면 ```edit dialog```로 쓸 수 있습니다',
266
+ ' * ```finish```로 여러분의 레벨을 JSON결과로 받을 수 있습니다!'
267
+ ]
268
+ }
269
+ }],
270
+ 'vi': [{
271
+ type: 'ModalAlert',
272
+ options: {
273
+ markdowns: [
274
+ '## Chào mừng đến trình tạo cấp độ!',
275
+ '',
276
+ 'Có những bước chính sau:',
277
+ '',
278
+ ' * Khởi tạo môi trường với các lệnh git',
279
+ ' * Định nghĩa cây để bắt đầu với ```define start```',
280
+ ' * Nhập chuỗi lệnh git để tạo đáp án (tốt nhất) của bạn',
281
+ ' * Định nghĩa cây mục tiêu với ```define goal```. Định nghĩa mục tiêu đồng thời cũng xác định đáp án',
282
+ ' * Có thể định nghĩa gợi ý với ```define hint```',
283
+ ' * Chỉnh sửa tên với ```define name```',
284
+ ' * Có thể định nghĩa hội thoại bắt đầu với ```edit dialog```',
285
+ ' * Nhập lệnh ```finish``` xuất cấp độ của bạn dưới dạng JSON!'
286
+ ]
287
+ }
288
+ }],
289
+ 'sl_SI': [{
290
+ type: 'ModalAlert',
291
+ options: {
292
+ markdowns: [
293
+ '## Dobrodošel v graditelju stopenj!',
294
+ '',
295
+ 'Tu so glavni koraki:',
296
+ '',
297
+ ' * Postavi začetno stanje z git ukazi',
298
+ ' * Določi začetno drevo z ```define start```',
299
+ ' * Vnesi zaporedje ukazov, ki predstavljajo (najboljšo) rešitev',
300
+ ' * Določi ciljno drevo z ```define goal```. Določanje cilja določi tudi rešitev',
301
+ ' * Opcijsko določi namig z ```define hint```',
302
+ ' * Uredi ime z ```define name```',
303
+ ' * Opcijsko določi ličen začetni dialog z ```edit dialog```',
304
+ ' * Vnesi ukaz ```finish``` za ustvarjanje JSON različice tvoje stopnje!'
305
+ ]
306
+ }
307
+ }],
308
+ 'pl': [{
309
+ type: 'ModalAlert',
310
+ options: {
311
+ markdowns: [
312
+ '## Witamy w kreatorze poziomów!',
313
+ '',
314
+ 'Oto główne kroki:',
315
+ '',
316
+ ' * Przygotuj środowisko początkowe za pomocą poleceń GIT-a',
317
+ ' * Zdefiniuj drzewo początkowe za pomocą ```define start```',
318
+ ' * Wprowadź serię poleceń GIT-a, które tworzą (optymalne) rozwiązanie',
319
+ ' * Utwórz drzewo celów za pomocą ```define goal```. Określenie celu określa również rozwiązanie',
320
+ ' * Opcjonalnie utwórz podpowiedzi (wskazówkę) za pomocą ```define hint```',
321
+ ' * Nadaj nazwę za pomocą ```define name```',
322
+ ' * Opcjonalnie, utwórz wiadomość początkową za pomocą ```edit dialog```',
323
+ ' * Wpisz polecenie ```finish```, aby wyświetlić swój poziom w JSON!'
324
+ ]
325
+ }
326
+ }],
327
+ 'ta_IN': [{
328
+ type: 'ModalAlert',
329
+ options: {
330
+ markdowns: [
331
+ '## நிலைகளை நிருவகிக்கும் கட்டமைப்பிற்க்கு வருக!',
332
+ '',
333
+ 'அடிப்படை நடைமுறைகள்:',
334
+ '',
335
+ ' * முதலாவதாக ஆரம்ப சூழலை git கட்டளைகள் கொன்டுகொன்டு அமைக்கவும்.',
336
+ ' * ```define start``` தொடக்க செயல் முறையை வரையறுக்கவும்.',
337
+ ' * உகந்த தீர்வினை அடையும் git கட்டளைகளின் தொடரை உள்ளிடவும்.',
338
+ ' * ```define goal``` கொண்டு இலக்கினை அடையும் கிளை வரைமுரைகளை தீர்வுடன் அமைக்கவும்.',
339
+ ' * தேவை எனில் ```define hint``` கொண்டு உதவி குறிப்பை வரையறுக்கவும்.',
340
+ ' * ```define name``` கொண்டு பெயரைத் திருத்தவும்.',
341
+ ' * தேவை எனில் ```edit dialog``` கொண்டு ஒரு நல்ல முன்னுறையை வரையறுக்கவும்.',
342
+ ' * ```finish``` கொண்டு இந்த நிலையின் JSON!-ஐ அச்சிடுக.'
343
+ ]
344
+ }
345
+ }],
346
+ "it_IT": [
347
+ {
348
+ type: "ModalAlert",
349
+ options: {
350
+ markdowns: [
351
+ "## Benvenuto al generatore di livelli !",
352
+ "",
353
+ "Ecco i passaggi principali:",
354
+ "",
355
+ " * Inizializza l'ambiente con i comandi git",
356
+ " * Definisci l'albero di partenza con ```define start```",
357
+ " * Inserisci la serie di comandi git che compongono la soluzione (ottimale)",
358
+ " * Definisci l'albero finale da ottenere con ```define goal```. L'albero finale costituisce la soluzione",
359
+ " * E' possibile inserire un suggerimento con ```define hint```",
360
+ " * Modifica il nome con ```define name```",
361
+ " * E' possibile personalizzare la finestra iniziale con ```edit dialog```",
362
+ " * Inserire il comando ```finish``` per generare il livello JSON!",
363
+ ],
364
+ },
365
+ },
366
+ ],
367
+ };
368
+
src/js/dialogs/nextLevel.js ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ exports.dialog = {
2
+ 'en_US': [{
3
+ type: 'ModalAlert',
4
+ options: {
5
+ markdowns: [
6
+ '## Great Job!!',
7
+ '',
8
+ 'You solved the level in *{numCommands}* command(s); ',
9
+ 'our solution uses {best}.'
10
+ ]
11
+ }
12
+ }],
13
+ 'de_DE': [{
14
+ type: 'ModalAlert',
15
+ options: {
16
+ markdowns: [
17
+ '## Super gemacht',
18
+ '',
19
+ 'Du hast den Level in *{numCommands}* Befehl(en) gelöst;',
20
+ 'meine Lösung besteht aus {best}.'
21
+ ]
22
+ }
23
+ }],
24
+ 'ja': [{
25
+ type: 'ModalAlert',
26
+ options: {
27
+ markdowns: [
28
+ '## 完成!',
29
+ '',
30
+ 'あなたは*{numCommands}*回のコマンドでこの課題をクリアしました; ',
31
+ '模範解答では{best}回です。',
32
+ '',
33
+ '模範解答は、右下の`?`メニューの`Solution`から見ることができます。'
34
+ ]
35
+ }
36
+ }],
37
+ 'zh_CN': [{
38
+ type: 'ModalAlert',
39
+ options: {
40
+ markdowns: [
41
+ '## 好样的!',
42
+ '',
43
+ '你用 *{numCommands}* 条命令通过了这一关;',
44
+ '我们的答案要用 {best} 条命令。'
45
+ ]
46
+ }
47
+ }],
48
+ 'zh_TW': [{
49
+ type: 'ModalAlert',
50
+ options: {
51
+ markdowns: [
52
+ '## 太棒了!',
53
+ '',
54
+ '您用了 *{numCommands}* 個指令完成這一關,',
55
+ '我們的解答用了 {best} 個。'
56
+ ]
57
+ }
58
+ }],
59
+ 'es_AR': [{
60
+ type: 'ModalAlert',
61
+ options: {
62
+ markdowns: [
63
+ '## ¡Buen trabajo!',
64
+ '',
65
+ 'Resolviste el nivel en *{numCommands}* comandos; ',
66
+ 'nuestra mejor solución usa {best}.'
67
+ ]
68
+ }
69
+ }],
70
+ 'es_MX': [{
71
+ type: 'ModalAlert',
72
+ options: {
73
+ markdowns: [
74
+ '## ¡Buen trabajo!',
75
+ '',
76
+ 'Resolviste el nivel en *{numCommands}* comandos; ',
77
+ 'nuestra mejor solución usa: {best}.'
78
+ ]
79
+ }
80
+ }],
81
+ 'es_ES': [{
82
+ type: 'ModalAlert',
83
+ options: {
84
+ markdowns: [
85
+ '## ¡Buen trabajo!',
86
+ '',
87
+ 'Resolviste el nivel en *{numCommands}* comandos; ',
88
+ 'nuestra mejor solución usa {best}.'
89
+ ]
90
+ }
91
+ }],
92
+ 'pt_BR': [{
93
+ type: 'ModalAlert',
94
+ options: {
95
+ markdowns: [
96
+ '## Bom trabalho!!',
97
+ '',
98
+ 'Você resolveu o nível usando *{numCommands}* comandos; ',
99
+ 'nossa melhor solução usa {best}.'
100
+ ]
101
+ }
102
+ }],
103
+ 'gl': [{
104
+ type: 'ModalAlert',
105
+ options: {
106
+ markdowns: [
107
+ '## Bo traballo!!',
108
+ '',
109
+ 'Resolviches o nivel empregando *{numCommands}* comandos; ',
110
+ 'a nosa mellor solución é en {best}.'
111
+ ]
112
+ }
113
+ }],
114
+ 'fr_FR': [{
115
+ type: 'ModalAlert',
116
+ options: {
117
+ markdowns: [
118
+ '## Beau Travail!!',
119
+ '',
120
+ 'Vous avez résolu le niveau en *{numCommands}* commande(s); ',
121
+ 'notre solution le fait en {best}.'
122
+ ]
123
+ }
124
+ }],
125
+ 'ru_RU': [{
126
+ type: 'ModalAlert',
127
+ options: {
128
+ markdowns: [
129
+ '## Супер!',
130
+ '',
131
+ 'Ты прошёл уровень. Количество использованных команд - *{numCommands}* ; ',
132
+ 'а наше решение состоит из {best}.'
133
+ ]
134
+ }
135
+ }],
136
+ 'uk': [{
137
+ type: 'ModalAlert',
138
+ options: {
139
+ markdowns: [
140
+ '## Молодець!',
141
+ '',
142
+ 'Ти пройшов рівень. Кількість використаних команд \u2014 *{numCommands}*; ',
143
+ 'наш розв’язок складається з {best}.'
144
+ ]
145
+ }
146
+ }],
147
+ 'ko': [{
148
+ type: 'ModalAlert',
149
+ options: {
150
+ markdowns: [
151
+ '## 훌륭합니다!!',
152
+ '',
153
+ '*{numCommands}*개의 명령으로 레벨을 통과했습니다; ',
154
+ '모범 답안은 {best}개를 사용합니다.'
155
+ ]
156
+ }
157
+ }],
158
+ 'vi': [{
159
+ type: 'ModalAlert',
160
+ options: {
161
+ markdowns: [
162
+ '## Làm tốt lắm!!',
163
+ '',
164
+ 'Bạn hoàn thành cấp độ này với *{numCommands}* câu lệnh; ',
165
+ 'Đáp án của chúng tôi sử dụng {best}.'
166
+ ]
167
+ }
168
+ }],
169
+ 'sl_SI': [{
170
+ type: 'ModalAlert',
171
+ options: {
172
+ markdowns: [
173
+ '## Dobro opravljeno!!',
174
+ '',
175
+ 'Rešil si stopnjo z *{numCommands}* ukazi; ',
176
+ 'naša rešitev uporabi {best}.'
177
+ ]
178
+ }
179
+ }],
180
+ 'pl': [{
181
+ type: 'ModalAlert',
182
+ options: {
183
+ markdowns: [
184
+ '## Dobra robota!!',
185
+ '',
186
+ 'Rozwiązałeś poziom używając *{numCommands}* poleceń/ia; ',
187
+ 'nasze rozwiązanie składa się z {best}.'
188
+ ]
189
+ }
190
+ }],
191
+ 'ta_IN': [{
192
+ type: 'ModalAlert',
193
+ options: {
194
+ markdowns: [
195
+ '## ஆக சிரந்த செயல்!!',
196
+ '',
197
+ 'நீங்கள் *{numCommands}* நிலைக்கான கட்டளை(கள்) கொண்டு தீர்வை அடிந்து விட்டீர்கள்; ',
198
+ 'நமது தீர்வு {best}-ஐ பயன்படுத்து கின்றது.'
199
+ ]
200
+ }
201
+ }],
202
+ "it_IT": [
203
+ {
204
+ type: "ModalAlert",
205
+ options: {
206
+ markdowns: [
207
+ "## Ben fatto!!",
208
+ "",
209
+ "Hai risolto il livello con *{numCommands}* comando(i); ",
210
+ "noi l'abbiamo risolto con {best}.",
211
+ ],
212
+ },
213
+ },
214
+ ],
215
+ };
216
+
src/js/dialogs/sandbox.js ADDED
@@ -0,0 +1,812 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ exports.dialog = {
2
+ 'en_US': [{
3
+ type: 'ModalAlert',
4
+ options: {
5
+ markdowns: [
6
+ '## Welcome to Learn Git Branching',
7
+ '',
8
+ 'Interested in learning Git? Well you\'ve come to the right place! ',
9
+ '"Learn Git Branching" is the most visual and interactive way to learn Git ',
10
+ 'on the web; you\'ll be challenged with exciting levels, given step-by-step ',
11
+ 'demonstrations of powerful features, and maybe even have a bit of fun along the way.',
12
+ '',
13
+ 'After this dialog you\'ll see the variety of levels we have to offer. If you\'re a ',
14
+ 'beginner, just go ahead and start with the first. If you already know some Git basics, ',
15
+ 'try some of our later more challenging levels.',
16
+ '',
17
+ 'You can see all the commands available with `show commands` at the terminal.',
18
+ '',
19
+ 'PS: Want to go straight to a sandbox next time?',
20
+ 'Try out ',
21
+ '[this special link](https://pcottle.github.io/learnGitBranching/?NODEMO)',
22
+ '',
23
+ 'PPS: GitHub has started naming the default branch `main` instead of `master` ',
24
+ 'to migrate away from biased terminology [(more details available here)](https://github.com/github/renaming). ',
25
+ 'In accordance with this industry-wide movement, we have also updated "Learn Git Branching" to ',
26
+ 'use `main` instead of `master` in our lessons. This rename should be fairly consistent by ',
27
+ 'now but if you notice any errors, feel free to submit a PR (or open an issue).'
28
+ ]
29
+ }
30
+ }],
31
+ 'es_AR': [{
32
+ type: 'ModalAlert',
33
+ options: {
34
+ markdowns: [
35
+ '## ¡Te damos la bienvenida a Learn Git Branching!',
36
+ '',
37
+ 'Esta aplicación está diseñada para ayudar a los principiantes ',
38
+ 'a manejar los poderosos conceptos que hay detrás del trabajo ',
39
+ 'con ramas (branches) en Git. Esperamos que disfrutes la aplicación ',
40
+ 'y tal vez incluso ¡que aprendas algo! ',
41
+ '',
42
+ '# ¡Demo!',
43
+ '',
44
+ 'Si no viste la demo, mirala en esta dirección:',
45
+ '',
46
+ '[https://pcottle.github.io/learnGitBranching/?locale=es_AR&demo](https://pcottle.github.io/learnGitBranching/?locale=es_AR&demo)',
47
+ '',
48
+ '¿Querés dejar de ver este mensaje? Agregale `NODEMO` a la URL para dejar de verlo, como en este link:',
49
+ '',
50
+ '[https://pcottle.github.io/learnGitBranching/?locale=es_AR&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=es_AR&NODEMO)'
51
+ ]
52
+ }
53
+ }, {
54
+ type: 'ModalAlert',
55
+ options: {
56
+ markdowns: [
57
+ '## Comandos de Git',
58
+ '',
59
+ 'Tenés una gran variedad de comandos de git en este sandbox. Estos incluyen: ',
60
+ '',
61
+ ' * commit',
62
+ ' * branch',
63
+ ' * checkout',
64
+ ' * cherry-pick',
65
+ ' * reset',
66
+ ' * revert',
67
+ ' * rebase',
68
+ ' * merge'
69
+ ]
70
+ }
71
+ }, {
72
+ type: 'ModalAlert',
73
+ options: {
74
+ markdowns: [
75
+ '## ¡Compartí!',
76
+ '',
77
+ 'Compartí tus árboles con tus amigos usando `export tree` e `import tree`',
78
+ '',
79
+ '¿Tenés una buena lección que compartir? Probá construyendo un nivel con `build level` o probá el nivel de un amigo con `import level`',
80
+ '',
81
+ 'Para ver todos los comandos disponibles, probá `show commands`. Hay algunas joyitas como `undo` y `reset`',
82
+ '',
83
+ 'Por ahora, arranquemos con los `levels`...'
84
+ ]
85
+ }
86
+ }],
87
+ 'es_MX': [{
88
+ type: 'ModalAlert',
89
+ options: {
90
+ markdowns: [
91
+ '## ¡Bienvenid@ a Learn Git Branching!',
92
+ '',
93
+ 'Esta aplicación está diseñada para ayudar a los principiantes',
94
+ 'a manejar los poderosos conceptos que hay detrás del trabajo',
95
+ 'con ramas (branches) en Git. Esperamos que disfrutes la aplicación',
96
+ 'y tal vez incluso ¡que aprendas algo!',
97
+ '',
98
+ '# ¡Demo!',
99
+ '',
100
+ 'Si no viste la demo, mirala en ésta dirección:',
101
+ '',
102
+ '[https://pcottle.github.io/learnGitBranching/?locale=es_MX&demo](https://pcottle.github.io/learnGitBranching/?locale=es_MX&demo)',
103
+ '',
104
+ '¿Harto de este mensaje? Agregale `NODEMO` a la URL para dejar de verlo, como en éste link:',
105
+ '',
106
+ '[https://pcottle.github.io/learnGitBranching/?locale=es_MX&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=es_MX&NODEMO)'
107
+ ]
108
+ }
109
+ }, {
110
+ type: 'ModalAlert',
111
+ options: {
112
+ markdowns: [
113
+ '## Comandos de git',
114
+ '',
115
+ 'Tienes una gran variedad de comandos de git en este sandbox. He aquí una lista de los incluidos:',
116
+ '',
117
+ ' * commit',
118
+ ' * branch',
119
+ ' * checkout',
120
+ ' * cherry-pick',
121
+ ' * reset',
122
+ ' * revert',
123
+ ' * rebase',
124
+ ' * merge'
125
+ ]
126
+ }
127
+ }, {
128
+ type: 'ModalAlert',
129
+ options: {
130
+ markdowns: [
131
+ '## ¡Comparte!',
132
+ '',
133
+ 'Comparte tus árboles con tus amigos usando `export tree` e `import tree`',
134
+ '',
135
+ '¿Tienes una buena lección que compartir? Prueba construyendo un nivel con `build level` o prueba el nivel de un amigo con `import level`',
136
+ '',
137
+ 'Para ver todos los comandos disponibles, prueba `show commands`. Hay algunos muy prácticos como `undo` y `reset`',
138
+ '',
139
+ 'Por ahora, arranquemos con los `levels`...'
140
+ ]
141
+ }
142
+ }],
143
+ 'es_ES': [{
144
+ type: 'ModalAlert',
145
+ options: {
146
+ markdowns: [
147
+ '## ¡Bienvenid@ a Learn Git Branching!',
148
+ '',
149
+ 'Esta aplicación está diseñada para ayudar a los principiantes ',
150
+ 'a manejar los poderosos conceptos que hay detrás del trabajo ',
151
+ 'con ramas (branches) en Git. Esperamos que disfrutes la aplicación ',
152
+ 'y tal vez incluso ¡que aprendas algo! ',
153
+ '',
154
+ '# ¡Demo!',
155
+ '',
156
+ 'Si no viste la demo, mírala en esta dirección:',
157
+ '',
158
+ '[https://pcottle.github.io/learnGitBranching/?locale=es_ES&demo](https://pcottle.github.io/learnGitBranching/?locale=es_ES&demo)',
159
+ '',
160
+ '¿Harto de este mensaje? Agrégale `NODEMO` a la URL para dejar de verlo, como en este link:',
161
+ '',
162
+ '[https://pcottle.github.io/learnGitBranching/?locale=es_ES&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=es_ES&NODEMO)'
163
+ ]
164
+ }
165
+ }, {
166
+ type: 'ModalAlert',
167
+ options: {
168
+ markdowns: [
169
+ '## Comandos de Git',
170
+ '',
171
+ 'Tienes una gran variedad de comandos de git en este sandbox. Estos incluyen: ',
172
+ '',
173
+ ' * commit',
174
+ ' * branch',
175
+ ' * checkout',
176
+ ' * cherry-pick',
177
+ ' * reset',
178
+ ' * revert',
179
+ ' * rebase',
180
+ ' * merge'
181
+ ]
182
+ }
183
+ }, {
184
+ type: 'ModalAlert',
185
+ options: {
186
+ markdowns: [
187
+ '## ¡Comparte!',
188
+ '',
189
+ 'Comparte tus árboles con tus amigos usando `export tree` e `import tree`',
190
+ '',
191
+ '¿Tienes una buena lección que compartir? Prueba construyendo un nivel con `build level` o prueba el nivel de un amigo con `import level`',
192
+ '',
193
+ 'Para ver todos los comandos disponibles, escribe `show commands`. Hay algunas joyitas como `undo` y `reset`',
194
+ '',
195
+ 'Por ahora, empecemos con los `levels`...'
196
+ ]
197
+ }
198
+ }],
199
+ 'pt_BR': [{
200
+ type: 'ModalAlert',
201
+ options: {
202
+ markdowns: [
203
+ '## Bem-vindo ao Learn Git Branching!',
204
+ '',
205
+ 'Este aplicativo foi desenvolvido para ajudar os iniciantes a ',
206
+ 'aprender os poderosos conceitos por trás do branching com ',
207
+ 'o git. Esperamos que você goste deste aplicativo e talvez ',
208
+ 'até aprenda alguma coisa!',
209
+ '',
210
+ '# Demo!',
211
+ '',
212
+ 'Se você ainda não viu o demo, veja aqui:',
213
+ '',
214
+ '[https://pcottle.github.io/learnGitBranching/?locale=pt_BR&demo](https://pcottle.github.io/learnGitBranching/?locale=pt_BR&demo)',
215
+ '',
216
+ 'Farto desta mensagem? Acrescente `NODEMO` ao endereço para se livrar dela, como no link abaixo:',
217
+ '',
218
+ '[https://pcottle.github.io/learnGitBranching/?locale=pt_BR&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=pt_BR&NODEMO)'
219
+ ]
220
+ }
221
+ }, {
222
+ type: 'ModalAlert',
223
+ options: {
224
+ markdowns: [
225
+ '## Comandos do git',
226
+ '',
227
+ 'Você tem à sua disposição no sandbox uma variedade de comandos do git:',
228
+ '',
229
+ ' * commit',
230
+ ' * branch',
231
+ ' * checkout',
232
+ ' * cherry-pick',
233
+ ' * reset',
234
+ ' * revert',
235
+ ' * rebase',
236
+ ' * merge'
237
+ ]
238
+ }
239
+ }, {
240
+ type: 'ModalAlert',
241
+ options: {
242
+ markdowns: [
243
+ '## Compartilhar é se importar!',
244
+ '',
245
+ 'Compartilhe árvores com seus amigos usando `export tree` e `import tree`',
246
+ '',
247
+ 'Tem uma grande lição para compartilhar? Tente construir um nível com `build level` ou experimente o nível de um amigo com `import level`',
248
+ '',
249
+ 'Para ver todos os comandos, use `show commands`. Há algumas jóias como `undo` e `reset`',
250
+ '',
251
+ 'Por hora, vamos começar com o `levels`...'
252
+ ]
253
+ }
254
+ }],
255
+ 'gl': [{
256
+ type: 'ModalAlert',
257
+ options: {
258
+ markdowns: [
259
+ '## Benvido a Learn Git Branching!',
260
+ '',
261
+ 'Esta aplicación foi desenvolvida para axudar os iniciados en git a ',
262
+ 'aprender os poderosos conceptos que hai por detrás do branching con ',
263
+ ' git. Agardamos que disfrutes desta aplicación, e tal vez, ',
264
+ 'ata aprendas algunha cousa!',
265
+ '',
266
+ '# Demostracións!',
267
+ '',
268
+ 'Se aínda non viches a demo, olla aquí:',
269
+ '',
270
+ '[https://pcottle.github.io/learnGitBranching/?locale=gl&demo](https://pcottle.github.io/learnGitBranching/?locale=gl&demo)',
271
+ '',
272
+ '¿Farto destas mensaxes? Engade `NODEMO` á dirección para librarte dela, como no link de abaixo:',
273
+ '',
274
+ '[https://pcottle.github.io/learnGitBranching/?locale=gl&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=gl&NODEMO)'
275
+ ]
276
+ }
277
+ }, {
278
+ type: 'ModalAlert',
279
+ options: {
280
+ markdowns: [
281
+ '## Comandos de git',
282
+ '',
283
+ 'Tes a túa disposición unha caixa de área con unha variedade de comandos de git:',
284
+ '',
285
+ ' * commit',
286
+ ' * branch',
287
+ ' * checkout',
288
+ ' * cherry-pick',
289
+ ' * reset',
290
+ ' * revert',
291
+ ' * rebase',
292
+ ' * merge'
293
+ ]
294
+ }
295
+ }, {
296
+ type: 'ModalAlert',
297
+ options: {
298
+ markdowns: [
299
+ '## Compartir e importar!',
300
+ '',
301
+ 'Comparte árbores cos seus amigas con `export tree` e `import tree`',
302
+ '',
303
+ '¿Tes un enlace moi grande para compartir? Intenta construír un nivel con `build level` ou importe o nivel dun amigo con `import level`',
304
+ '',
305
+ 'Para ver tódolos comandos, usa `show commands`. Hai algunha xoia como `undo` e `reset`',
306
+ '',
307
+ 'Por agora, imos comezar cos `levels`...'
308
+ ]
309
+ }
310
+ }],
311
+ 'de_DE': [{
312
+ type: 'ModalAlert',
313
+ options: {
314
+ markdowns: [
315
+ '## Willkommen bei Learn Git Branching!',
316
+ '',
317
+ 'Der Sinn dieser Anwendung ist, die umfangreichen und komplexen Zusammenhänge der Prozesse, die bei der Arbeit mit Git ablaufen, zu verdeutlichen. Ich hoffe du hast Spaß dabei und lernst vielleicht sogar etwas!',
318
+ '',
319
+ '# Demo!',
320
+ '',
321
+ 'Falls du die Demonstration noch nicht gesehen hast, schau sie dir hier an:',
322
+ '',
323
+ '[https://pcottle.github.io/learnGitBranching/?locale=de_DE&demo](https://pcottle.github.io/learnGitBranching/?locale=de_DE&demo)',
324
+ '',
325
+ 'Genervt von diesem Fenster? Häng `NODEMO` an die URL um es los zu werden, so wie hier:',
326
+ '',
327
+ '[https://pcottle.github.io/learnGitBranching/?locale=de_DE&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=de_DE&NODEMO)'
328
+ ]
329
+ }
330
+ }, {
331
+ type: 'ModalAlert',
332
+ options: {
333
+ markdowns: [
334
+ '## Git-Kommandos',
335
+ '',
336
+ 'Dir steht eine große Zahl von Git-Befehlen im Sandkasten-Modus zur Verfügung. Unter anderem',
337
+ '',
338
+ ' * commit',
339
+ ' * branch',
340
+ ' * checkout',
341
+ ' * cherry-pick',
342
+ ' * reset',
343
+ ' * revert',
344
+ ' * rebase',
345
+ ' * merge'
346
+ ]
347
+ }
348
+ }, {
349
+ type: 'ModalAlert',
350
+ options: {
351
+ markdowns: [
352
+ '## Teilen macht Spaß!',
353
+ '',
354
+ 'Teile diese Git-Bäume mit deinen Freunden mittels `export tree` und `import tree`.',
355
+ '',
356
+ 'Hast du Wissenswertes zu Git zu vermitteln? Versuch einen Level mit `build level` zu bauen oder probier den Level eines Freundes mit `import level` aus.',
357
+ '',
358
+ 'Um alle Kommandos zu sehen, gib `show commands` ein. Darunter gibt\'s kleine Schätze wie `undo` und `reset`.',
359
+ '',
360
+ 'Für\'s Erste lass uns mit `levels` anfangen ...'
361
+ ]
362
+ }
363
+ }],
364
+ 'ja': [{
365
+ type: 'ModalAlert',
366
+ options: {
367
+ markdowns: [
368
+ '## Learn Git Branchingへようこそ',
369
+ '',
370
+ 'gitのパワフルなブランチ機能のコンセプトが ',
371
+ '学びやすくなるようにこのアプリケーションを作りました。 ',
372
+ 'このアプリケーションを楽しんで使って頂いて、 ',
373
+ '何かを学習して頂けたなら嬉しいです。',
374
+ '',
375
+ '# とりあえず触ってみたい方へ:',
376
+ '',
377
+ '簡単なデモを用意してあるので、もしよければこちらもご覧ください:',
378
+ '',
379
+ '[https://pcottle.github.io/learnGitBranching/?locale=ja&demo](https://pcottle.github.io/learnGitBranching/?demo&locale=ja)',
380
+ '',
381
+ 'このダイアログ自体を省略するには、以下のようにURLの末尾にクエリストリング`NODEMO`を付加してアクセスしてください。',
382
+ '',
383
+ '[https://pcottle.github.io/learnGitBranching/?locale=ja&NODEMO](https://pcottle.github.io/learnGitBranching/?NODEMO&locale=ja)'
384
+ ]
385
+ }
386
+ }, {
387
+ type: 'ModalAlert',
388
+ options: {
389
+ markdowns: [
390
+ '## ここで学べるGitのオペレーション',
391
+ '',
392
+ 'ここでは、下記の種類のgitコマンドを学ぶことができます。',
393
+ '',
394
+ ' * commit',
395
+ ' * branch',
396
+ ' * checkout',
397
+ ' * cherry-pick',
398
+ ' * reset',
399
+ ' * revert',
400
+ ' * rebase',
401
+ ' * merge'
402
+ ]
403
+ }
404
+ }, {
405
+ type: 'ModalAlert',
406
+ options: {
407
+ markdowns: [
408
+ '## 学習した内容を共有できます',
409
+ '',
410
+ '画面左のコマンドプロンプトから`export tree`や`import tree`とタイプすることで、gitのツリー構造を友達に送ることができます',
411
+ '',
412
+ '何か教材になるようなケースはご存知ないでしょうか。`build level`で課題を作成したり、`import level`で他の人の課題に挑戦してみてください。',
413
+ '',
414
+ '何か詰まったことがあったら、右下メニューの?ボタンを押してみてください',
415
+ '',
416
+ 'それから、不自然な記号が出てきたときは顔を左方向に傾けてみるといいかもしれません :P(ペロッ)',
417
+ '',
418
+ 'それでは教材の選択画面に進んでみることにします。',
419
+ '',
420
+ '(なお、日本語版製作者のフォークサイトは[こちら](https://remore.github.io/learnGitBranching-ja/)になります。)'
421
+ ]
422
+ }
423
+ }],
424
+ 'zh_CN': [{
425
+ type: 'ModalAlert',
426
+ options: {
427
+ markdowns: [
428
+ '## 欢迎光临 Learn Git Branching',
429
+ '',
430
+ '你对 Git 感兴趣吗?那么算是来对地方了!',
431
+ '“Learning Git Branching” 可以说是目前为止最好的教程了,在沙盒里你能执行相应的命令,还能看到每个命令的执行情况;',
432
+ '通过一系列刺激的关卡挑战,逐步深入的学习 Git 的强大功能,在这个过程中你可能还会发现一些有意思的事情。',
433
+ '',
434
+ '关闭这个对话框以后,你会看到我们提供的许多关卡。如果你是初学者,从第一关开始逐个向后挑战就是了。',
435
+ '而如果你已经入门了,可以略过前面,直接挑战后面更有难度的关卡。',
436
+ '',
437
+ '### 演示',
438
+ '',
439
+ '如果你还没看过演示,请[到此](?demo)查看。',
440
+ '',
441
+ 'PS:想直接进入沙盒? 在 URL 后头加上 `NODEMO` 就可以了,试一下[这个链接](https://pcottle.github.io/learnGitBranching/?locale=zh_CN&NODEMO)'
442
+ ]
443
+ }
444
+ }],
445
+ 'zh_TW': [{
446
+ type: 'ModalAlert',
447
+ options: {
448
+ markdowns: [
449
+ '## 歡迎光臨 Learn Git Branching!',
450
+ '',
451
+ '本應用旨在幫助初學者領會 git 分支背後的強大概念。',
452
+ '希望你能喜歡這個應用,並學到知識!',
453
+ '',
454
+ '# 演示!',
455
+ '',
456
+ '如果你還沒看過演示,請到此查看:',
457
+ '',
458
+ '[https://pcottle.github.io/learnGitBranching/?locale=zh_TW&demo](https://pcottle.github.io/learnGitBranching/?locale=zh_TW&demo)',
459
+ '',
460
+ '厭煩這個對話視窗嗎?在 URL 後頭加上 `NODEMO` 就看不到它了,也可以直接點下邊這個連結:',
461
+ '',
462
+ '[https://pcottle.github.io/learnGitBranching/?locale=zh_TW&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=zh_TW&NODEMO)'
463
+ ]
464
+ }
465
+ }, {
466
+ type: 'ModalAlert',
467
+ options: {
468
+ markdowns: [
469
+ '## Git 命令',
470
+ '',
471
+ '在沙盒模式中,你有很多指令可用。包括:',
472
+ '',
473
+ ' * commit',
474
+ ' * branch',
475
+ ' * checkout',
476
+ ' * cherry-pick',
477
+ ' * reset',
478
+ ' * revert',
479
+ ' * rebase',
480
+ ' * merge'
481
+ ]
482
+ }
483
+ }, {
484
+ type: 'ModalAlert',
485
+ options: {
486
+ markdowns: [
487
+ '## 分享即關懷',
488
+ '',
489
+ '使用 `export tree` 和 `import tree` 與朋友分享 Git 樹',
490
+ '',
491
+ '有個好課程可以分享?試試用 `build level` 創建一個關卡,或者 `import level` 試試朋友的。',
492
+ '',
493
+ '言歸正傳,讓我們先從 `levels` 開始……'
494
+ ]
495
+ }
496
+ }],
497
+ 'ko': [{
498
+ type: 'ModalAlert',
499
+ options: {
500
+ markdowns: [
501
+ '## Git 브랜치 배우기를 시작합니다!',
502
+ '',
503
+ '이 애플리케이션은 git을 쓸 때 필요한 브랜치에 대한 개념을',
504
+ '탄탄히 잡게끔 도와드리기 위해 만들었습니다. 재밌게 사용해주시기를',
505
+ '바라며, 무언가를 배워가신다면 더 기쁘겠습니다!',
506
+ '',
507
+ '이 애플리케이션은 [Peter Cottle](https://github.io/pcottle)님의 [LearnGitBranching](https://pcottle.github.io/learnGitBranching/)를 번역한 것입니다.',
508
+ '아래 데모를 먼저 보셔도 좋습니다.',
509
+ '',
510
+ '<https://pcottle.github.io/learnGitBranching/?demo&locale=ko>'
511
+ ]
512
+ }
513
+ }, {
514
+ type: 'ModalAlert',
515
+ options: {
516
+ markdowns: [
517
+ '## Git 명령어',
518
+ '',
519
+ '연습 모드에서 쓸 수 있는 다양한 git명령어는 다음과 같습니다',
520
+ '',
521
+ ' * commit',
522
+ ' * branch',
523
+ ' * checkout',
524
+ ' * cherry-pick',
525
+ ' * reset',
526
+ ' * revert',
527
+ ' * rebase',
528
+ ' * merge'
529
+ ]
530
+ }
531
+ }, {
532
+ type: 'ModalAlert',
533
+ options: {
534
+ markdowns: [
535
+ '## 공유해주세요!',
536
+ '',
537
+ '`export tree` 와 `import tree`로 여러분의 친구들에게 트리를 공유해주세요',
538
+ '',
539
+ '훌륭한 학습 자료가 있으신가요? `build level`로 레벨을 만들어 보시거나, 친구의 레벨을 `import level`로 가져와서 실험해보세요',
540
+ '',
541
+ '이제 레슨을 시작해봅시다...'
542
+ ]
543
+ }
544
+ }],
545
+ 'fr_FR': [{
546
+ type: 'ModalAlert',
547
+ options: {
548
+ markdowns: [
549
+ '## Bienvenue sur Learn Git Branching !',
550
+ '',
551
+ 'Cette application a été conçue pour aider les débutants à saisir ',
552
+ 'les puissants concepts derrière les branches en travaillant ',
553
+ 'avec git. Nous espérons que vous apprécierez cette application et ',
554
+ 'que vous apprendrez peut-être quelque chose d\'intéressant !',
555
+ '',
556
+ '# Démo !',
557
+ '',
558
+ 'Si vous n\'avez pas vu la démo, vous pouvez le faire là :',
559
+ '',
560
+ '[https://pcottle.github.io/learnGitBranching/?locale=fr_FR&demo](https://pcottle.github.io/learnGitBranching/?locale=fr_FR&demo)',
561
+ '',
562
+ 'Agacé par ce dialogue ? Ajoutez `NODEMO` à l\'URL pour le supprimer, en lien ci-dessous pour votre commodité :',
563
+ '',
564
+ '[https://pcottle.github.io/learnGitBranching/?locale=fr_FR&NODEMO](https://pcottle.github.io/learnGitBranching/?locale=fr_FR&NODEMO)'
565
+ ]
566
+ }
567
+ }, {
568
+ type: 'ModalAlert',
569
+ options: {
570
+ markdowns: [
571
+ '## Commandes Git',
572
+ '',
573
+ 'Il existe une large variété de commandes git disponibles dans le mode bac à sable. Sont incluses :',
574
+ '',
575
+ ' * commit',
576
+ ' * branch',
577
+ ' * checkout',
578
+ ' * cherry-pick',
579
+ ' * reset',
580
+ ' * revert',
581
+ ' * rebase',
582
+ ' * merge'
583
+ ]
584
+ }
585
+ }, {
586
+ type: 'ModalAlert',
587
+ options: {
588
+ markdowns: [
589
+ '## Partager, c\'est se soucier !',
590
+ '',
591
+ 'Partagez des arbres avec vos amis via `export tree` et `import tree`',
592
+ '',
593
+ 'Vous avez une grande leçon à partager ? Essayez de construire un niveau avec `build level` ou essayez le niveau d\'un ami avec `import level`',
594
+ '',
595
+ 'Pour voir la gamme complète des commandes, tapez `show commands`. Il y a quelques perles telles que `undo` et `reset`',
596
+ '',
597
+ 'Mais tout de suite commencez sur les `levels`…'
598
+ ]
599
+ }
600
+ }],
601
+ 'ru_RU': [{
602
+ type: 'ModalAlert',
603
+ options: {
604
+ markdowns: [
605
+ '## Добро пожаловать в LearnGitBranching!',
606
+ '',
607
+ 'Это приложение создано, чтобы помочь новичкам постичь ',
608
+ 'мощные возможности ветвления и работы ',
609
+ 'с git. Мы надеемся, что вам понравится эта игра ',
610
+ 'и может вы что-то усвоите!',
611
+ '',
612
+ '# Демо!',
613
+ '',
614
+ 'Если ты не видел демонстрацию – посмотри её тут:',
615
+ '',
616
+ '[https://pcottle.github.io/learnGitBranching/?locale=ru_RU&demo](https://pcottle.github.io/learnGitBranching/?locale=ru_RU&demo)',
617
+ '',
618
+ 'Достало это сообщение? Добавь `NODEMO` к адресу и навсегда забудь о нём, ниже ссылка для удобства:',
619
+ '',
620
+ '[https://pcottle.github.io/learnGitBranching/?locale=ru_RU&NODEMO](?locale=ru_RU&NODEMO)'
621
+ ]
622
+ }
623
+ }, {
624
+ type: 'ModalAlert',
625
+ options: {
626
+ markdowns: [
627
+ '## Команды Git',
628
+ '',
629
+ 'В нашей песочнице можно использовать множество команд:',
630
+ '',
631
+ ' * commit',
632
+ ' * branch',
633
+ ' * checkout',
634
+ ' * cherry-pick',
635
+ ' * reset',
636
+ ' * revert',
637
+ ' * rebase',
638
+ ' * merge'
639
+ ]
640
+ }
641
+ }, {
642
+ type: 'ModalAlert',
643
+ options: {
644
+ markdowns: [
645
+ '## Бог велел – делись!',
646
+ '',
647
+ 'Ты можешь делиться результатами с друзьями при помощи `export tree` и `import tree`',
648
+ '',
649
+ 'Хочешь создать классный уровень? Сделай это при помощи `build level` или добавь уровень друга при помощи `import level`',
650
+ '',
651
+ 'Команда `show commands` покажет все доступные инструкции. Там есть очень полезные, например `undo` и `reset`',
652
+ '',
653
+ 'А пока просто начни игру при помощи `levels`...'
654
+ ]
655
+ }
656
+ }],
657
+ 'uk': [{
658
+ type: 'ModalAlert',
659
+ options: {
660
+ markdowns: [
661
+ '## Ласкаво просимо до Learn Git Branching',
662
+ '',
663
+ 'Хочеш вивчити Git? Тоді ти знайшов те, що шукав!',
664
+ '"Learn Git Branching" \u2014 це найбільш візуальний та інтерактивний спосіб вивчення Git в Інтернеті. ',
665
+ 'Ти з��ожеш проходити захопливі рівні, дивитися ',
666
+ 'покрокові інструкції з використання потужних функцій Git, навіть трохи ',
667
+ 'розважитись в процесі навчання.',
668
+ '',
669
+ 'Після цього діалогу побачиш список доступних рівнів. Якщо ти новачок, ',
670
+ 'просто почни з першого рівня. Якщо вже знаєш основи Git, ',
671
+ 'спробуй більш складні рівні в кінці.',
672
+ '',
673
+ 'P.S. Хочеш перейти одразу до пісочниці наступного разу?',
674
+ 'Спробуй ',
675
+ '[це спеціальне посилання.](https://pcottle.github.io/learnGitBranching/?locale=uk&NODEMO)'
676
+ ]
677
+ }
678
+ }],
679
+ 'vi': [{
680
+ type: 'ModalAlert',
681
+ options: {
682
+ markdowns: [
683
+ '## Chào mừng đến với Học Nhánh Git',
684
+ '',
685
+ 'Bạn có hứng thú học Git? Bạn đến đúng nơi rồi đấy! ',
686
+ '"Học Nhánh Git" là cách trực quan và hiệu quả nhất để học Git trên web; ',
687
+ 'thông qua một loạt các thử thách cấp độ thú vị, bạn sẽ từng bước tìm hiểu sức mạnh của git',
688
+ '',
689
+ 'Sau khi hội thoại này đóng lại, bạn sẽ thấy nhiều cấp độ mà chúng tôi cung cấp. ',
690
+ 'Nếu bạn là người mới thì hãy bắt đầu từ bài đầu tiên. Nếu bạn đã có hiểu biết cơ bản về git, ',
691
+ 'hãy thử những bài mang tính thách thức hơn phía sau.',
692
+ '',
693
+ 'Bạn có thể dùng lệnh `show commands` để xem tất cả các lệnh hiện hữu.',
694
+ '',
695
+ 'Ghi chú: Nếu muốn trực tiếp vào hộp cát ở lần sau?',
696
+ 'Hãy dùng',
697
+ '[đường link đặc biệt này của chúng tôi](https://pcottle.github.io/learnGitBranching/?locale=vi&NODEMO)'
698
+ ]
699
+ }
700
+ }],
701
+ 'sl_SI': [{
702
+ type: 'ModalAlert',
703
+ options: {
704
+ markdowns: [
705
+ '## Dobrodošel na učenju Git Branchanja',
706
+ '',
707
+ 'Bi se rad naučil Git? No, prišel si na pravo mesto! ',
708
+ '"Learn Git Branching" je najbolj vizualen in interaktiven način učenja Git-a ',
709
+ 'na spletu; zagrizel boš v zanimive stopnje, po korakih boš spoznaval osupljive ',
710
+ 'funkcije in kaj pa veš, morda ti bo celo zabavno. ;)',
711
+ '',
712
+ 'Za tem oknom boš videl kopico stopenj, ki so na razpolago. Če si ',
713
+ 'začetnik, kar pogumno, začni s prvo. Če pa že poznaš Git osnove, ',
714
+ 'se preizkusi v zahtevnejših stopnjah.',
715
+ '',
716
+ 'Vidiš lahko vse ukaze, ki so na voljo, z ukazom `show commands` v terminalu.',
717
+ '',
718
+ 'PS: Bi šel rad naslednjič naravnost v peskovnik?',
719
+ 'Poizkusi s',
720
+ '[to posebno povezavo](https://pcottle.github.io/learnGitBranching/?locale=sl_SI&NODEMO)'
721
+ ]
722
+ }
723
+ }],
724
+ 'pl': [{
725
+ type: 'ModalAlert',
726
+ options: {
727
+ markdowns: [
728
+ '## Witaj w Learn Git Branching!',
729
+ '',
730
+ 'Jesteś zainteresowany nauką Gita? Cóż, trafiłeś we właściwe miejsce!',
731
+ '"Learn Git Branching" jest najbardziej wizualnym i interaktywnym sposobem na naukę gita w sieci.',
732
+ 'Czekają na Ciebie ekscytujące poziomy, demonstracje zaawansowanych funkcji krok po kroku. Może nawet będziesz się dobrze bawić.',
733
+ '',
734
+ 'Po tym oknie dialogowym zobaczysz różnorodność poziomów, które mamy do zaoferowania.',
735
+ 'Jeśli jesteś początkujący, po prostu zacznij od pierwszego poziomu.',
736
+ 'Jeśli znasz już podstawy gita, wypróbuj niektóre z naszych późniejszych, bardziej wymagających poziomów.',
737
+ '',
738
+ 'Możesz zobaczyć wszystkie komendy wpisując `show commands` w terminalu.',
739
+ '',
740
+ 'Chcesz następnym razem przejść prosto do trybu piaskownicy? Kilknij [tutaj](https://pcottle.github.io/learnGitBranching/?locale=pl&NODEMO)',
741
+ '',
742
+ 'PS. GitHub zaczął nazywać domyślną gałąź `main` zamiast `master`, aby odejść od tendencyjnej terminologii. [(więcej informacji tutaj)](https://github.com/github/renaming)',,
743
+ 'Zgodnie z tym ogólnobranżowym ruchem, zaktualizowaliśmy również "Learn Git Branching", by używać `main` zamiast `master` w naszych zadaniach.',
744
+ 'Ta zmiana nazwy powinna być już w miarę spójna, ale jeśli zauważysz jakieś błędy, nie krępuj się dodać pull request (lub zgłosić issue na githubie - prawy górny róg).'
745
+ ]
746
+ }
747
+ }],
748
+ 'ta_IN': [{
749
+ type: 'ModalAlert',
750
+ options: {
751
+ markdowns: [
752
+ '## Git Branching கற்க வரவேற்கிறோம்',
753
+ '',
754
+ 'கிட் கற்க ஆர்வமா? அப்படியானால் நீங்கள் சரியான இடத்திற்கு வந்துவிட்டீர்கள்! ',
755
+ '"Learn Git Branching" Git ஐக் கற்றுக்கொள்வதற்கான வரைபடம் மற்றும் செயல்முறை ',
756
+ 'பயிற்சியுடன் கூடிய சிரந்த கருவி; ',
757
+ 'உங்களை சோதிக்கும் வகையிலான நிலைகளுடன் மிகுந்த சக்திவாய்ந்த அம்சங்களை ',
758
+ 'படிப்படியாகவும், சில சமையம் விளையாட்டாகவும் கற்றுத்தர கூடியது.',
759
+ '',
760
+ 'இந்த அறிவிற்ப்புக்கு பிறகு, நாங்கள் வழங்க உள்ள பல்வேறு நிலைகளை நீங்கள் காண்பீர்கள். ',
761
+ 'நீங்கள் ஆரம்ம நிலையில் இருந்தால், முதல் கட்டத்தில் இருந்து தொடங்கவும். ',
762
+ 'கிட்டின் சில அடிப்படைகளை நீங்கள் ஏற்கனவே அறிந்திருந்தால், மெலும் உள்ள கடினமான கட்டங்களை முயற்ச்சி செய்யுங்கள்.',
763
+ '',
764
+ 'தேவையானால் `show commands` ஐ பயன்படுத்தி அனைத்து கட்டளைகளையும் முனையத்தில் பார்க்கலாம்.',
765
+ '',
766
+ 'பின்குறிப்பு: அடுத்தமுறை நேராக sandbox செல்ல வேண்டுமா?',
767
+ 'அப்படியானால் பின் வரும் இணைப்பை பயன்பாடித்துக ',
768
+ '[this special link](https://pcottle.github.io/learnGitBranching/?locale=ta_IN&NODEMO)',
769
+ '',
770
+ 'பின்குறிப்பு: GitHub (பெரிய அளவில் பயன்பாட்டில் உள்ள இணையதலம்) `main` என்ற கிளையை `master`-க்கு பதில் ',
771
+ 'முன்னிருப்பு கிளையாக பயன் படுத்த உள்ளது [more details available here](https://github.com/github/renaming). ',
772
+ 'இந்த மாற்றத்தை பின்னோக்கி இணக்கமான வழியில் பொருத்துவதற்காக, `main`-ஐ முதன்மையாக கருதி ',
773
+ 'இந்த இரண்டு பெயர்களும் ஒன்றுக்கொன்று மாற்றுப்பெயர்களாகக் கருதப்படும். ',
774
+ 'இந்த மாற்றத்தை அனைத்து நிலை உள்ளடக்கங்களிலும் புதுப்பிக்க நாங்கள் சிறந்த முயற்சியை ',
775
+ 'மேற்கொண்டோம், ஆயினும் ஏதேனும் விடுபட்டி இருந்தால் PR உருவாக்கி உதவுங்கள்.',
776
+ 'ஒருபக்கச்சார்பான சொற்களிலிருந்து விலகிச் செல்ல உதவியதற்கு நன்றி.'
777
+ ]
778
+ }
779
+ }],
780
+ "it_IT": [
781
+ {
782
+ type: "ModalAlert",
783
+ options: {
784
+ markdowns: [
785
+ "## Benvenuto su Learn Git Branching",
786
+ "",
787
+ "Vorresti imparare Git? Bene, sei nel posto giusto! ",
788
+ '"Learn Git Branching" è il modo più chiaro e interattivo per imparare Git ',
789
+ "su internet; sarai messo alla prova tramite livelli stimolanti, con dimostrazioni ",
790
+ "passo a passo sulle sue potenzialità e, perché no, magari ti divertirai lungo questo percorso.",
791
+ "",
792
+ "Dopo questa finestra vedrai una varietà di livelli che abbiamo da offrire. Se sei alle",
793
+ "prime armi procedi e parti dall'inizio. Se hai delle conoscenze base di Git ",
794
+ "prova con gli ultimi livelli più impegnativi.",
795
+ "",
796
+ "Puoi vedere tutti i comandi disponibili digitando `show commands` sul terminale.",
797
+ "",
798
+ "PS: Preferisci andare direttamente al sandbox?",
799
+ "Prova ",
800
+ "[questo link](https://pcottle.github.io/learnGitBranching/?NODEMO?locale=it_IT).",
801
+ "",
802
+ "PPS: GitHub (e il settore in generale) sta modificando il nome del ramo di default in `main` invece che `master` ",
803
+ "(leggi [qui per ulteriori dettagli](https://github.com/github/renaming)). Per adattarci a questo cambiamento ",
804
+ "mantenendo la retrocompatibilità, questi nomi saranno considerati equivalenti. `main` sarà comunque ",
805
+ "il nome predefinito. Ci siamo impegnati per aggiornare tutti i livelli ma ci sarà ",
806
+ "sicuramente qualcosa che potremmo aver dimenticato. Esegui una PR (o segnala un problema) se ne trovi. ",
807
+ "Grazie per l'aiuto!",
808
+ ],
809
+ },
810
+ },
811
+ ],
812
+ };
src/js/dispatcher/AppDispatcher.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ var AppConstants = require('../constants/AppConstants');
4
+ var Dispatcher = require('flux').Dispatcher;
5
+
6
+ var PayloadSources = AppConstants.PayloadSources;
7
+
8
+ var AppDispatcher = new Dispatcher();
9
+
10
+ AppDispatcher.handleViewAction = function(action) {
11
+ this.dispatch({
12
+ source: PayloadSources.VIEW_ACTION,
13
+ action: action
14
+ });
15
+ };
16
+
17
+ AppDispatcher.handleURIAction = function(action) {
18
+ this.dispatch({
19
+ source: PayloadSources.URI_ACTION,
20
+ action: action
21
+ });
22
+ };
23
+
24
+ module.exports = AppDispatcher;
src/js/git/commands.js ADDED
@@ -0,0 +1,955 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var escapeString = require('../util/escapeString');
2
+ var intl = require('../intl');
3
+
4
+ var Graph = require('../graph');
5
+ var Errors = require('../util/errors');
6
+ var CommandProcessError = Errors.CommandProcessError;
7
+ var GitError = Errors.GitError;
8
+ var Warning = Errors.Warning;
9
+ var CommandResult = Errors.CommandResult;
10
+
11
+ var ORIGIN_PREFIX = 'o/';
12
+
13
+ var crappyUnescape = function(str) {
14
+ return str.replace(/&#x27;/g, "'").replace(/&#x2F;/g, "/");
15
+ };
16
+
17
+ function isColonRefspec(str) {
18
+ return str.indexOf(':') !== -1 && str.split(':').length === 2;
19
+ }
20
+
21
+ var assertIsRef = function(engine, ref) {
22
+ engine.resolveID(ref); // will throw git error if can't resolve
23
+ };
24
+
25
+ var validateBranchName = function(engine, name) {
26
+ return engine.validateBranchName(name);
27
+ };
28
+
29
+ var validateOriginBranchName = function(engine, name) {
30
+ return engine.origin.validateBranchName(name);
31
+ };
32
+
33
+ var validateBranchNameIfNeeded = function(engine, name) {
34
+ if (engine.refs[name]) {
35
+ return name;
36
+ }
37
+ return validateBranchName(engine, name);
38
+ };
39
+
40
+ var assertNotCheckedOut = function(engine, ref) {
41
+ if (!engine.refs[ref]) {
42
+ return;
43
+ }
44
+ if (engine.HEAD.get('target') === engine.refs[ref]) {
45
+ throw new GitError({
46
+ msg: intl.todo(
47
+ 'cannot fetch to ' + ref + ' when checked out on ' + ref
48
+ )
49
+ });
50
+ }
51
+ };
52
+
53
+ var assertIsBranch = function(engine, ref) {
54
+ assertIsRef(engine, ref);
55
+ var obj = engine.resolveID(ref);
56
+ if (!obj || obj.get('type') !== 'branch') {
57
+ throw new GitError({
58
+ msg: intl.todo(
59
+ ref + ' is not a branch'
60
+ )
61
+ });
62
+ }
63
+ };
64
+
65
+ var assertIsRemoteBranch = function(engine, ref) {
66
+ assertIsRef(engine, ref);
67
+ var obj = engine.resolveID(ref);
68
+
69
+ if (obj.get('type') !== 'branch' ||
70
+ !obj.getIsRemote()) {
71
+ throw new GitError({
72
+ msg: intl.todo(
73
+ ref + ' is not a remote branch'
74
+ )
75
+ });
76
+ }
77
+ };
78
+
79
+ var assertOriginSpecified = function(generalArgs) {
80
+ if (!generalArgs.length) {
81
+ return;
82
+ }
83
+ if (generalArgs[0] !== 'origin') {
84
+ throw new GitError({
85
+ msg: intl.todo(
86
+ generalArgs[0] + ' is not a remote in your repository! try adding origin to that argument'
87
+ )
88
+ });
89
+ }
90
+ };
91
+
92
+ var assertBranchIsRemoteTracking = function(engine, branchName) {
93
+ branchName = crappyUnescape(branchName);
94
+ if (!engine.resolveID(branchName)) {
95
+ throw new GitError({
96
+ msg: intl.todo(branchName + ' is not a branch!')
97
+ });
98
+ }
99
+ var branch = engine.resolveID(branchName);
100
+ if (branch.get('type') !== 'branch') {
101
+ throw new GitError({
102
+ msg: intl.todo(branchName + ' is not a branch!')
103
+ });
104
+ }
105
+
106
+ var tracking = branch.getRemoteTrackingBranchID();
107
+ if (!tracking) {
108
+ throw new GitError({
109
+ msg: intl.todo(
110
+ branchName + ' is not a remote tracking branch! I don\'t know where to push'
111
+ )
112
+ });
113
+ }
114
+ return tracking;
115
+ };
116
+
117
+ var commandConfig = {
118
+ commit: {
119
+ sc: /^(gc|git ci)($|\s)/,
120
+ regex: /^git +commit($|\s)/,
121
+ options: [
122
+ '--amend',
123
+ '-a',
124
+ '--all',
125
+ '-am',
126
+ '-m'
127
+ ],
128
+ execute: function(engine, command) {
129
+ var commandOptions = command.getOptionsMap();
130
+ command.acceptNoGeneralArgs();
131
+
132
+ if (commandOptions['-am'] && (
133
+ commandOptions['-a'] || commandOptions['--all'] || commandOptions['-m'])) {
134
+ throw new GitError({
135
+ msg: intl.str('git-error-options')
136
+ });
137
+ }
138
+
139
+ var msg = null;
140
+ var args = null;
141
+ if (commandOptions['-a'] || commandOptions['--all']) {
142
+ command.addWarning(intl.str('git-warning-add'));
143
+ }
144
+
145
+ if (commandOptions['-am']) {
146
+ args = commandOptions['-am'];
147
+ command.validateArgBounds(args, 1, 1, '-am');
148
+ msg = args[0];
149
+ }
150
+
151
+ if (commandOptions['-m']) {
152
+ args = commandOptions['-m'];
153
+ command.validateArgBounds(args, 1, 1, '-m');
154
+ msg = args[0];
155
+ }
156
+
157
+ if (commandOptions['--amend']) {
158
+ args = commandOptions['--amend'];
159
+ command.validateArgBounds(args, 0, 0, '--amend');
160
+ }
161
+
162
+ var newCommit = engine.commit({
163
+ isAmend: !!commandOptions['--amend']
164
+ });
165
+ if (msg) {
166
+ msg = msg
167
+ .replace(/&quot;/g, '"')
168
+ .replace(/^"/g, '')
169
+ .replace(/"$/g, '');
170
+
171
+ newCommit.set('commitMessage', msg);
172
+ }
173
+
174
+ var promise = engine.animationFactory.playCommitBirthPromiseAnimation(
175
+ newCommit,
176
+ engine.gitVisuals
177
+ );
178
+ engine.animationQueue.thenFinish(promise);
179
+ }
180
+ },
181
+
182
+ cherrypick: {
183
+ displayName: 'cherry-pick',
184
+ regex: /^git +cherry-pick($|\s)/,
185
+ execute: function(engine, command) {
186
+ var commandOptions = command.getOptionsMap();
187
+ var generalArgs = command.getGeneralArgs();
188
+
189
+ command.validateArgBounds(generalArgs, 1, Number.MAX_VALUE);
190
+
191
+ var set = Graph.getUpstreamSet(engine, 'HEAD');
192
+ // first resolve all the refs (as an error check)
193
+ var toCherrypick = generalArgs.map(function (arg) {
194
+ var commit = engine.getCommitFromRef(arg);
195
+ // and check that its not upstream
196
+ if (set[commit.get('id')]) {
197
+ throw new GitError({
198
+ msg: intl.str(
199
+ 'git-error-already-exists',
200
+ { commit: commit.get('id') }
201
+ )
202
+ });
203
+ }
204
+ return commit;
205
+ }, this);
206
+
207
+ engine.setupCherrypickChain(toCherrypick);
208
+ }
209
+ },
210
+
211
+ gc: {
212
+ displayName: 'gc',
213
+ regex: /^git +gc($|\s)/,
214
+ execute: function(engine, command) {
215
+ engine.pruneTree(false);
216
+ }
217
+ },
218
+
219
+ pull: {
220
+ regex: /^git +pull($|\s)/,
221
+ options: [
222
+ '--rebase'
223
+ ],
224
+ execute: function(engine, command) {
225
+ if (!engine.hasOrigin()) {
226
+ throw new GitError({
227
+ msg: intl.str('git-error-origin-required')
228
+ });
229
+ }
230
+
231
+ var commandOptions = command.getOptionsMap();
232
+ var generalArgs = command.getGeneralArgs();
233
+ command.twoArgsForOrigin(generalArgs);
234
+ assertOriginSpecified(generalArgs);
235
+ // here is the deal -- git pull is pretty complex with
236
+ // the arguments it wants. You can
237
+ // A) specify the remote branch you want to
238
+ // merge & fetch, in which case it completely
239
+ // ignores the properties of branch you are on, or
240
+ //
241
+ // B) specify no args, in which case it figures out
242
+ // the branch to fetch from the remote tracking
243
+ // and merges those in, or
244
+ //
245
+ // C) specify the colon refspec like fetch, where it does
246
+ // the fetch and then just merges the dest
247
+
248
+ var source;
249
+ var destination;
250
+ var firstArg = generalArgs[1];
251
+ // COPY PASTA validation code from fetch. maybe fix this?
252
+ if (firstArg && isColonRefspec(firstArg)) {
253
+ var refspecParts = firstArg.split(':');
254
+ source = refspecParts[0];
255
+ destination = validateBranchNameIfNeeded(
256
+ engine,
257
+ crappyUnescape(refspecParts[1])
258
+ );
259
+ assertNotCheckedOut(engine, destination);
260
+ } else if (firstArg) {
261
+ source = firstArg;
262
+ assertIsBranch(engine.origin, source);
263
+ // get o/main locally if main is specified
264
+ destination = engine.origin.resolveID(source).getPrefixedID();
265
+ } else {
266
+ // can't be detached
267
+ if (engine.getDetachedHead()) {
268
+ throw new GitError({
269
+ msg: intl.todo('Git pull can not be executed in detached HEAD mode if no remote branch specified!')
270
+ });
271
+ }
272
+ // ok we need to get our currently checked out branch
273
+ // and then specify source and dest
274
+ var branch = engine.getOneBeforeCommit('HEAD');
275
+ var branchName = branch.get('id');
276
+ assertBranchIsRemoteTracking(engine, branchName);
277
+ destination = branch.getRemoteTrackingBranchID();
278
+ source = destination.replace(ORIGIN_PREFIX, '');
279
+ }
280
+
281
+ engine.pull({
282
+ source: source,
283
+ destination: destination,
284
+ isRebase: !!commandOptions['--rebase']
285
+ });
286
+ }
287
+ },
288
+
289
+ fakeTeamwork: {
290
+ regex: /^git +fakeTeamwork($|\s)/,
291
+ execute: function(engine, command) {
292
+ var generalArgs = command.getGeneralArgs();
293
+ if (!engine.hasOrigin()) {
294
+ throw new GitError({
295
+ msg: intl.str('git-error-origin-required')
296
+ });
297
+ }
298
+
299
+ command.validateArgBounds(generalArgs, 0, 2);
300
+ var branch;
301
+ var numToMake;
302
+
303
+ // allow formats of: git fakeTeamwork 2 or git fakeTeamwork side 3
304
+ switch (generalArgs.length) {
305
+ // git fakeTeamwork
306
+ case 0:
307
+ branch = 'main';
308
+ numToMake = 1;
309
+ break;
310
+
311
+ // git fakeTeamwork 10 or git fakeTeamwork foo
312
+ case 1:
313
+ if (isNaN(parseInt(generalArgs[0], 10))) {
314
+ branch = validateOriginBranchName(engine, generalArgs[0]);
315
+ numToMake = 1;
316
+ } else {
317
+ numToMake = parseInt(generalArgs[0], 10);
318
+ branch = 'main';
319
+ }
320
+ break;
321
+
322
+ case 2:
323
+ branch = validateOriginBranchName(engine, generalArgs[0]);
324
+ if (isNaN(parseInt(generalArgs[1], 10))) {
325
+ throw new GitError({
326
+ msg: 'Bad numeric argument: ' + generalArgs[1]
327
+ });
328
+ }
329
+ numToMake = parseInt(generalArgs[1], 10);
330
+ break;
331
+
332
+ }
333
+
334
+ // make sure its a branch and exists
335
+ var destBranch = engine.origin.resolveID(branch);
336
+ if (destBranch.get('type') !== 'branch') {
337
+ throw new GitError({
338
+ msg: intl.str('git-error-options')
339
+ });
340
+ }
341
+
342
+ engine.fakeTeamwork(numToMake, branch);
343
+ }
344
+ },
345
+
346
+ clone: {
347
+ regex: /^git +clone *?$/,
348
+ execute: function(engine, command) {
349
+ command.acceptNoGeneralArgs();
350
+ engine.makeOrigin(engine.printTree());
351
+ }
352
+ },
353
+
354
+ remote: {
355
+ regex: /^git +remote($|\s)/,
356
+ options: [
357
+ '-v'
358
+ ],
359
+ execute: function(engine, command) {
360
+ command.acceptNoGeneralArgs();
361
+ if (!engine.hasOrigin()) {
362
+ throw new CommandResult({
363
+ msg: ''
364
+ });
365
+ }
366
+
367
+ engine.printRemotes({
368
+ verbose: !!command.getOptionsMap()['-v']
369
+ });
370
+ }
371
+ },
372
+
373
+ fetch: {
374
+ regex: /^git +fetch($|\s)/,
375
+ execute: function(engine, command) {
376
+ if (!engine.hasOrigin()) {
377
+ throw new GitError({
378
+ msg: intl.str('git-error-origin-required')
379
+ });
380
+ }
381
+
382
+ var source;
383
+ var destination;
384
+ var generalArgs = command.getGeneralArgs();
385
+ command.twoArgsForOrigin(generalArgs);
386
+ assertOriginSpecified(generalArgs);
387
+
388
+ var firstArg = generalArgs[1];
389
+ if (firstArg && isColonRefspec(firstArg)) {
390
+ var refspecParts = firstArg.split(':');
391
+ source = refspecParts[0];
392
+ destination = validateBranchNameIfNeeded(
393
+ engine,
394
+ crappyUnescape(refspecParts[1])
395
+ );
396
+ assertNotCheckedOut(engine, destination);
397
+ } else if (firstArg) {
398
+ // here is the deal -- its JUST like git push. the first arg
399
+ // is used as both the destination and the source, so we need
400
+ // to make sure it exists as the source on REMOTE. however
401
+ // technically we have a destination here as the remote branch
402
+ source = firstArg;
403
+ assertIsBranch(engine.origin, source);
404
+ // get o/main locally if main is specified
405
+ destination = engine.origin.resolveID(source).getPrefixedID();
406
+ }
407
+ if (source) { // empty string fails this check
408
+ assertIsRef(engine.origin, source);
409
+ }
410
+
411
+ engine.fetch({
412
+ source: source,
413
+ destination: destination
414
+ });
415
+ }
416
+ },
417
+
418
+ branch: {
419
+ sc: /^(gb|git br)($|\s)/,
420
+ regex: /^git +branch($|\s)/,
421
+ options: [
422
+ '-d',
423
+ '-D',
424
+ '-f',
425
+ '--force',
426
+ '-a',
427
+ '-r',
428
+ '-u',
429
+ '--contains'
430
+ ],
431
+ execute: function(engine, command) {
432
+ var commandOptions = command.getOptionsMap();
433
+ var generalArgs = command.getGeneralArgs();
434
+
435
+ var args = null;
436
+ // handle deletion first
437
+ if (commandOptions['-d'] || commandOptions['-D']) {
438
+ var names = commandOptions['-d'] || commandOptions['-D'];
439
+ names = names.concat(generalArgs);
440
+ command.validateArgBounds(names, 1, Number.MAX_VALUE, '-d');
441
+
442
+ names.forEach(function(name) {
443
+ engine.validateAndDeleteBranch(name);
444
+ });
445
+ return;
446
+ }
447
+
448
+ if (commandOptions['-u']) {
449
+ args = commandOptions['-u'].concat(generalArgs);
450
+ command.validateArgBounds(args, 1, 2, '-u');
451
+ var remoteBranch = crappyUnescape(args[0]);
452
+ var branch = args[1] || engine.getOneBeforeCommit('HEAD').get('id');
453
+
454
+ // some assertions, both of these have to exist first
455
+ assertIsRemoteBranch(engine, remoteBranch);
456
+ assertIsBranch(engine, branch);
457
+ engine.setLocalToTrackRemote(
458
+ engine.resolveID(branch),
459
+ engine.resolveID(remoteBranch)
460
+ );
461
+ return;
462
+ }
463
+
464
+ if (commandOptions['--contains']) {
465
+ args = commandOptions['--contains'];
466
+ command.validateArgBounds(args, 1, 1, '--contains');
467
+ engine.printBranchesWithout(args[0]);
468
+ return;
469
+ }
470
+
471
+ if (commandOptions['-f'] || commandOptions['--force']) {
472
+ args = commandOptions['-f'] || commandOptions['--force'];
473
+ args = args.concat(generalArgs);
474
+ command.twoArgsImpliedHead(args, '-f');
475
+
476
+ // we want to force a branch somewhere
477
+ engine.forceBranch(args[0], args[1]);
478
+ return;
479
+ }
480
+
481
+
482
+ if (generalArgs.length === 0) {
483
+ var branches;
484
+ if (commandOptions['-a']) {
485
+ branches = engine.getBranches();
486
+ } else if (commandOptions['-r']) {
487
+ branches = engine.getRemoteBranches();
488
+ } else {
489
+ branches = engine.getLocalBranches();
490
+ }
491
+ engine.printBranches(branches);
492
+ return;
493
+ }
494
+
495
+ command.twoArgsImpliedHead(generalArgs);
496
+ engine.branch(generalArgs[0], generalArgs[1]);
497
+ }
498
+ },
499
+
500
+ add: {
501
+ dontCountForGolf: true,
502
+ sc: /^ga($|\s)/,
503
+ regex: /^git +add($|\s)/,
504
+ execute: function() {
505
+ throw new CommandResult({
506
+ msg: intl.str('git-error-staging')
507
+ });
508
+ }
509
+ },
510
+
511
+ reset: {
512
+ regex: /^git +reset($|\s)/,
513
+ options: [
514
+ '--hard',
515
+ '--soft'
516
+ ],
517
+ execute: function(engine, command) {
518
+ var commandOptions = command.getOptionsMap();
519
+ var generalArgs = command.getGeneralArgs();
520
+
521
+ if (commandOptions['--soft']) {
522
+ throw new GitError({
523
+ msg: intl.str('git-error-staging')
524
+ });
525
+ }
526
+ if (commandOptions['--hard']) {
527
+ command.addWarning(
528
+ intl.str('git-warning-hard')
529
+ );
530
+ // don't absorb the arg off of --hard
531
+ generalArgs = generalArgs.concat(commandOptions['--hard']);
532
+ }
533
+
534
+ command.validateArgBounds(generalArgs, 1, 1);
535
+
536
+ if (engine.getDetachedHead()) {
537
+ throw new GitError({
538
+ msg: intl.str('git-error-reset-detached')
539
+ });
540
+ }
541
+
542
+ engine.reset(generalArgs[0]);
543
+ }
544
+ },
545
+
546
+ revert: {
547
+ regex: /^git +revert($|\s)/,
548
+ execute: function(engine, command) {
549
+ var generalArgs = command.getGeneralArgs();
550
+
551
+ command.validateArgBounds(generalArgs, 1, Number.MAX_VALUE);
552
+ engine.revert(generalArgs);
553
+ }
554
+ },
555
+
556
+ merge: {
557
+ regex: /^git +merge($|\s)/,
558
+ options: [
559
+ '--no-ff'
560
+ ],
561
+ execute: function(engine, command) {
562
+ var commandOptions = command.getOptionsMap();
563
+ var generalArgs = command.getGeneralArgs().concat(commandOptions['--no-ff'] || []);
564
+ command.validateArgBounds(generalArgs, 1, 1);
565
+
566
+ var newCommit = engine.merge(
567
+ generalArgs[0],
568
+ { noFF: !!commandOptions['--no-ff'] }
569
+ );
570
+
571
+ if (newCommit === undefined) {
572
+ // its just a fast forward
573
+ engine.animationFactory.refreshTree(
574
+ engine.animationQueue, engine.gitVisuals
575
+ );
576
+ return;
577
+ }
578
+
579
+ engine.animationFactory.genCommitBirthAnimation(
580
+ engine.animationQueue, newCommit, engine.gitVisuals
581
+ );
582
+ }
583
+ },
584
+
585
+ revlist: {
586
+ dontCountForGolf: true,
587
+ displayName: 'rev-list',
588
+ regex: /^git +rev-list($|\s)/,
589
+ execute: function(engine, command) {
590
+ var generalArgs = command.getGeneralArgs();
591
+ command.validateArgBounds(generalArgs, 1);
592
+
593
+ engine.revlist(generalArgs);
594
+ }
595
+ },
596
+
597
+ log: {
598
+ dontCountForGolf: true,
599
+ regex: /^git +log($|\s)/,
600
+ execute: function(engine, command) {
601
+ var generalArgs = command.getGeneralArgs();
602
+
603
+ command.impliedHead(generalArgs, 0);
604
+ engine.log(generalArgs);
605
+ }
606
+ },
607
+
608
+ show: {
609
+ dontCountForGolf: true,
610
+ regex: /^git +show($|\s)/,
611
+ execute: function(engine, command) {
612
+ var generalArgs = command.getGeneralArgs();
613
+ command.oneArgImpliedHead(generalArgs);
614
+ engine.show(generalArgs[0]);
615
+ }
616
+ },
617
+
618
+ rebase: {
619
+ sc: /^gr($|\s)/,
620
+ options: [
621
+ '-i',
622
+ '--solution-ordering',
623
+ '--interactive-test',
624
+ '--aboveAll',
625
+ '-p',
626
+ '--preserve-merges',
627
+ '--onto'
628
+ ],
629
+ regex: /^git +rebase($|\s)/,
630
+ execute: function(engine, command) {
631
+ var commandOptions = command.getOptionsMap();
632
+ var generalArgs = command.getGeneralArgs();
633
+
634
+ if (commandOptions['-i']) {
635
+ var args = commandOptions['-i'].concat(generalArgs);
636
+ command.twoArgsImpliedHead(args, ' -i');
637
+
638
+ if (commandOptions['--interactive-test']) {
639
+ engine.rebaseInteractiveTest(
640
+ args[0],
641
+ args[1], {
642
+ interactiveTest: commandOptions['--interactive-test']
643
+ }
644
+ );
645
+ } else {
646
+ engine.rebaseInteractive(
647
+ args[0],
648
+ args[1], {
649
+ aboveAll: !!commandOptions['--aboveAll'],
650
+ initialCommitOrdering: commandOptions['--solution-ordering']
651
+ }
652
+ );
653
+ }
654
+ return;
655
+ }
656
+
657
+ if (commandOptions['--onto']) {
658
+ var args = commandOptions['--onto'].concat(generalArgs);
659
+ command.threeArgsImpliedHead(args, ' --onto');
660
+
661
+ engine.rebaseOnto(args[0], args[1], args[2], {
662
+ preserveMerges: commandOptions['-p'] || commandOptions['--preserve-merges']
663
+ });
664
+
665
+ return;
666
+ }
667
+
668
+ command.twoArgsImpliedHead(generalArgs);
669
+ engine.rebase(generalArgs[0], generalArgs[1], {
670
+ preserveMerges: commandOptions['-p'] || commandOptions['--preserve-merges']
671
+ });
672
+ }
673
+ },
674
+
675
+ status: {
676
+ dontCountForGolf: true,
677
+ sc: /^(gst|gs|git st)($|\s)/,
678
+ regex: /^git +status($|\s)/,
679
+ execute: function(engine) {
680
+ // no parsing at all
681
+ engine.status();
682
+ }
683
+ },
684
+
685
+ checkout: {
686
+ sc: /^(go|git co)($|\s)/,
687
+ regex: /^git +checkout($|\s)/,
688
+ options: [
689
+ '-b',
690
+ '-B',
691
+ '-'
692
+ ],
693
+ execute: function(engine, command) {
694
+ var commandOptions = command.getOptionsMap();
695
+ var generalArgs = command.getGeneralArgs();
696
+
697
+ var args = null;
698
+ if (commandOptions['-b']) {
699
+ // the user is really trying to just make a
700
+ // branch and then switch to it. so first:
701
+ args = commandOptions['-b'].concat(generalArgs);
702
+ command.twoArgsImpliedHead(args, '-b');
703
+
704
+ var validId = engine.validateBranchName(args[0]);
705
+ engine.branch(validId, args[1]);
706
+ engine.checkout(validId);
707
+ return;
708
+ }
709
+
710
+ if (commandOptions['-']) {
711
+ // get the heads last location
712
+ var lastPlace = engine.HEAD.get('lastLastTarget');
713
+ if (!lastPlace) {
714
+ throw new GitError({
715
+ msg: intl.str('git-result-nothing')
716
+ });
717
+ }
718
+ engine.HEAD.set('target', lastPlace);
719
+ return;
720
+ }
721
+
722
+ if (commandOptions['-B']) {
723
+ args = commandOptions['-B'].concat(generalArgs);
724
+ command.twoArgsImpliedHead(args, '-B');
725
+
726
+ engine.forceBranch(args[0], args[1]);
727
+ engine.checkout(args[0]);
728
+ return;
729
+ }
730
+
731
+ command.validateArgBounds(generalArgs, 1, 1);
732
+
733
+ engine.checkout(engine.crappyUnescape(generalArgs[0]));
734
+ }
735
+ },
736
+
737
+ push: {
738
+ regex: /^git +push($|\s)/,
739
+ options: [
740
+ '--force'
741
+ ],
742
+ execute: function(engine, command) {
743
+ if (!engine.hasOrigin()) {
744
+ throw new GitError({
745
+ msg: intl.str('git-error-origin-required')
746
+ });
747
+ }
748
+
749
+ var options = {};
750
+ var destination;
751
+ var source;
752
+ var sourceObj;
753
+ var commandOptions = command.getOptionsMap();
754
+
755
+ // git push is pretty complex in terms of
756
+ // the arguments it wants as well... get ready!
757
+ var generalArgs = command.getGeneralArgs();
758
+ command.twoArgsForOrigin(generalArgs);
759
+ assertOriginSpecified(generalArgs);
760
+
761
+ var firstArg = generalArgs[1];
762
+ if (firstArg && isColonRefspec(firstArg)) {
763
+ var refspecParts = firstArg.split(':');
764
+ source = refspecParts[0];
765
+ destination = validateBranchName(engine, refspecParts[1]);
766
+ if (source === "" && !engine.origin.resolveID(destination)) {
767
+ throw new GitError({
768
+ msg: intl.todo(
769
+ 'cannot delete branch ' + options.destination + ' which doesnt exist'
770
+ )
771
+ });
772
+ }
773
+ } else {
774
+ if (firstArg) {
775
+ // we are using this arg as destination AND source. the dest branch
776
+ // can be created on demand but we at least need this to be a source
777
+ // locally otherwise we will fail
778
+ assertIsRef(engine, firstArg);
779
+ sourceObj = engine.resolveID(firstArg);
780
+ } else {
781
+ // since they have not specified a source or destination, then
782
+ // we source from the branch we are on (or HEAD)
783
+ sourceObj = engine.getOneBeforeCommit('HEAD');
784
+ }
785
+ source = sourceObj.get('id');
786
+
787
+ // HOWEVER we push to either the remote tracking branch we have
788
+ // OR a new named branch if we aren't tracking anything
789
+ if (sourceObj.getRemoteTrackingBranchID &&
790
+ sourceObj.getRemoteTrackingBranchID()) {
791
+ assertBranchIsRemoteTracking(engine, source);
792
+ var remoteBranch = sourceObj.getRemoteTrackingBranchID();
793
+ destination = engine.resolveID(remoteBranch).getBaseID();
794
+ } else {
795
+ destination = validateBranchName(engine, source);
796
+ }
797
+ }
798
+ if (source) {
799
+ assertIsRef(engine, source);
800
+ }
801
+
802
+ engine.push({
803
+ // NOTE -- very important! destination and source here
804
+ // are always, always strings. very important :D
805
+ destination: destination,
806
+ source: source,
807
+ force: !!commandOptions['--force']
808
+ });
809
+ }
810
+ },
811
+
812
+ describe: {
813
+ regex: /^git +describe($|\s)/,
814
+ execute: function(engine, command) {
815
+ // first if there are no tags, we cant do anything so just throw
816
+ if (engine.tagCollection.toArray().length === 0) {
817
+ throw new GitError({
818
+ msg: intl.todo(
819
+ 'fatal: No tags found, cannot describe anything.'
820
+ )
821
+ });
822
+ }
823
+
824
+ var generalArgs = command.getGeneralArgs();
825
+ command.oneArgImpliedHead(generalArgs);
826
+ assertIsRef(engine, generalArgs[0]);
827
+
828
+ engine.describe(generalArgs[0]);
829
+ }
830
+ },
831
+
832
+ tag: {
833
+ regex: /^git +tag($|\s)/,
834
+ options: [
835
+ '-d'
836
+ ],
837
+ execute: function(engine, command) {
838
+ var generalArgs = command.getGeneralArgs();
839
+ var commandOptions = command.getOptionsMap();
840
+
841
+ if (commandOptions['-d']) {
842
+ var tagID = commandOptions['-d'];
843
+ var tagToRemove;
844
+
845
+ assertIsRef(engine, tagID);
846
+
847
+ command.oneArgImpliedHead(tagID);
848
+ engine.tagCollection.each(function(tag) {
849
+ if(tag.get('id') == tagID){
850
+ tagToRemove = tag;
851
+ }
852
+ }, true);
853
+
854
+ if(tagToRemove == undefined){
855
+ throw new GitError({
856
+ msg: intl.todo(
857
+ 'No tag found, nothing to remove'
858
+ )
859
+ });
860
+ }
861
+
862
+ engine.tagCollection.remove(tagToRemove);
863
+ delete engine.refs[tagID];
864
+
865
+ engine.gitVisuals.refreshTree();
866
+ return;
867
+ }
868
+
869
+ if (generalArgs.length === 0) {
870
+ var tags = engine.getTags();
871
+ engine.printTags(tags);
872
+ return;
873
+ }
874
+
875
+ command.twoArgsImpliedHead(generalArgs);
876
+ engine.tag(generalArgs[0], generalArgs[1]);
877
+ }
878
+ },
879
+
880
+ switch: {
881
+ sc: /^(gsw|git sw)($|\s)/,
882
+ regex: /^git +switch($|\s)/,
883
+ options: [
884
+ '-c',
885
+ '-'
886
+ ],
887
+ execute: function(engine, command) {
888
+ var generalArgs = command.getGeneralArgs();
889
+ var commandOptions = command.getOptionsMap();
890
+
891
+ var args = null;
892
+ if (commandOptions['-c']) {
893
+ // the user is really trying to just make a
894
+ // branch and then switch to it. so first:
895
+ args = commandOptions['-c'].concat(generalArgs);
896
+ command.twoArgsImpliedHead(args, '-c');
897
+
898
+ var validId = engine.validateBranchName(args[0]);
899
+ engine.branch(validId, args[1]);
900
+ engine.checkout(validId);
901
+ return;
902
+ }
903
+
904
+ if (commandOptions['-']) {
905
+ // get the heads last location
906
+ var lastPlace = engine.HEAD.get('lastLastTarget');
907
+ if (!lastPlace) {
908
+ throw new GitError({
909
+ msg: intl.str('git-result-nothing')
910
+ });
911
+ }
912
+ engine.HEAD.set('target', lastPlace);
913
+ return;
914
+ }
915
+
916
+ command.validateArgBounds(generalArgs, 1, 1);
917
+
918
+ engine.checkout(engine.crappyUnescape(generalArgs[0]));
919
+ }
920
+ }
921
+ };
922
+
923
+ var instantCommands = [
924
+ [/^(git help($|\s)|git$)/, function() {
925
+ var lines = [
926
+ intl.str('git-version'),
927
+ '<br/>',
928
+ intl.str('git-usage'),
929
+ escapeString(intl.str('git-usage-command')),
930
+ '<br/>',
931
+ intl.str('git-supported-commands'),
932
+ '<br/>'
933
+ ];
934
+
935
+ var commands = require('../commands').commands.getOptionMap()['git'];
936
+ // build up a nice display of what we support
937
+ Object.keys(commands).forEach(function(command) {
938
+ var commandOptions = commands[command];
939
+ lines.push('git ' + command);
940
+ Object.keys(commandOptions).forEach(function(optionName) {
941
+ lines.push('\t ' + optionName);
942
+ }, this);
943
+ }, this);
944
+
945
+ // format and throw
946
+ var msg = lines.join('\n');
947
+ msg = msg.replace(/\t/g, '&nbsp;&nbsp;&nbsp;');
948
+ throw new CommandResult({
949
+ msg: msg
950
+ });
951
+ }]
952
+ ];
953
+
954
+ exports.commandConfig = commandConfig;
955
+ exports.instantCommands = instantCommands;
src/js/git/gitShim.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Q = require('q');
2
+
3
+ var Main = require('../app');
4
+ var MultiView = require('../views/multiView').MultiView;
5
+
6
+ function GitShim(options) {
7
+ options = options || {};
8
+
9
+ // these variables are just functions called before / after for
10
+ // simple things (like incrementing a counter)
11
+ this.beforeCB = options.beforeCB || function() {};
12
+ this.afterCB = options.afterCB || function() {};
13
+
14
+ // these guys handle an optional async process before the git
15
+ // command executes or afterwards. If there is none,
16
+ // it just resolves the deferred immediately
17
+ var resolveImmediately = function(deferred) {
18
+ deferred.resolve();
19
+ };
20
+ this.beforeDeferHandler = options.beforeDeferHandler || resolveImmediately;
21
+ this.afterDeferHandler = options.afterDeferHandler || resolveImmediately;
22
+ this.eventBaton = options.eventBaton || Main.getEventBaton();
23
+ }
24
+
25
+ GitShim.prototype.insertShim = function() {
26
+ this.eventBaton.stealBaton('processGitCommand', this.processGitCommand, this);
27
+ };
28
+
29
+ GitShim.prototype.removeShim = function() {
30
+ this.eventBaton.releaseBaton('processGitCommand', this.processGitCommand, this);
31
+ };
32
+
33
+ GitShim.prototype.processGitCommand = function(command, deferred) {
34
+ this.beforeCB(command);
35
+
36
+ // ok we make a NEW deferred that will, upon resolution,
37
+ // call our afterGitCommandProcessed. This inserts the 'after' shim
38
+ // functionality. we give this new deferred to the eventBaton handler
39
+ var newDeferred = Q.defer();
40
+ newDeferred.promise
41
+ .then(function() {
42
+ // give this method the original defer so it can resolve it
43
+ this.afterGitCommandProcessed(command, deferred);
44
+ }.bind(this))
45
+ .done();
46
+
47
+ // now our shim owner might want to launch some kind of deferred beforehand, like
48
+ // a modal or something. in order to do this, we need to defer the passing
49
+ // of the event baton backwards, and either resolve that promise immediately or
50
+ // give it to our shim owner.
51
+ var passBaton = function() {
52
+ // punt to the previous listener
53
+ this.eventBaton.passBatonBack('processGitCommand', this.processGitCommand, this, [command, newDeferred]);
54
+ }.bind(this);
55
+
56
+ var beforeDefer = Q.defer();
57
+ beforeDefer.promise
58
+ .then(passBaton)
59
+ .done();
60
+
61
+ // if we didnt receive a defer handler in the options, this just
62
+ // resolves immediately
63
+ this.beforeDeferHandler(beforeDefer, command);
64
+ };
65
+
66
+ GitShim.prototype.afterGitCommandProcessed = function(command, deferred) {
67
+ this.afterCB(command);
68
+
69
+ // again we can't just resolve this deferred right away... our shim owner might
70
+ // want to insert some promise functionality before that happens. so again
71
+ // we make a defer
72
+ var afterDefer = Q.defer();
73
+ afterDefer.promise
74
+ .then(function() {
75
+ deferred.resolve();
76
+ })
77
+ .done();
78
+
79
+ this.afterDeferHandler(afterDefer, command);
80
+ };
81
+
82
+ exports.GitShim = GitShim;
83
+
src/js/git/headless.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Backbone = require('backbone');
2
+ var Q = require('q');
3
+
4
+ var GitEngine = require('../git').GitEngine;
5
+ var AnimationFactory = require('../visuals/animation/animationFactory').AnimationFactory;
6
+ var GitVisuals = require('../visuals').GitVisuals;
7
+ var TreeCompare = require('../graph/treeCompare');
8
+ var EventBaton = require('../util/eventBaton').EventBaton;
9
+
10
+ var Collections = require('../models/collections');
11
+ var CommitCollection = Collections.CommitCollection;
12
+ var BranchCollection = Collections.BranchCollection;
13
+ var TagCollection = Collections.TagCollection;
14
+ var Command = require('../models/commandModel').Command;
15
+
16
+ var mock = require('../util/mock').mock;
17
+ var util = require('../util');
18
+
19
+ function getMockFactory() {
20
+ var mockFactory = {};
21
+ var mockReturn = function() {
22
+ var d = Q.defer();
23
+ // fall through!
24
+ d.resolve();
25
+ return d.promise;
26
+ };
27
+ for (var key in AnimationFactory) {
28
+ mockFactory[key] = mockReturn;
29
+ }
30
+
31
+ mockFactory.playRefreshAnimationAndFinish = function(gitVisuals, aQueue) {
32
+ aQueue.finish();
33
+ };
34
+ mockFactory.refreshTree = function(aQueue, gitVisuals) {
35
+ aQueue.finish();
36
+ };
37
+
38
+ mockFactory.highlightEachWithPromise = function(chain, toRebase, destBranch) {
39
+ // don't add any steps
40
+ return chain;
41
+ };
42
+
43
+ return mockFactory;
44
+ }
45
+
46
+ function getMockVisualization() {
47
+ return {
48
+ makeOrigin: function(options) {
49
+ var localRepo = options.localRepo;
50
+ var treeString = options.treeString;
51
+
52
+ var headless = new HeadlessGit();
53
+ headless.gitEngine.loadTreeFromString(treeString);
54
+ return {
55
+ customEvents: {
56
+ on: function(key, cb, context) {
57
+ cb.apply(context, []);
58
+ }
59
+ },
60
+ gitEngine: headless.gitEngine
61
+ };
62
+ }
63
+ };
64
+ }
65
+
66
+ var HeadlessGit = function() {
67
+ this.init();
68
+ };
69
+
70
+ HeadlessGit.prototype.init = function() {
71
+ this.commitCollection = new CommitCollection();
72
+ this.branchCollection = new BranchCollection();
73
+ this.tagCollection = new TagCollection();
74
+
75
+ // here we mock visuals and animation factory so the git engine
76
+ // is headless
77
+ var animationFactory = getMockFactory();
78
+ var gitVisuals = mock(GitVisuals);
79
+ // add some stuff for origin making
80
+ var mockVis = getMockVisualization();
81
+ gitVisuals.getVisualization = function() {
82
+ return mockVis;
83
+ };
84
+
85
+ this.gitEngine = new GitEngine({
86
+ collection: this.commitCollection,
87
+ branches: this.branchCollection,
88
+ tags: this.tagCollection,
89
+ gitVisuals: gitVisuals,
90
+ animationFactory: animationFactory,
91
+ eventBaton: new EventBaton()
92
+ });
93
+ this.gitEngine.init();
94
+ };
95
+
96
+ // horrible hack so we can just quickly get a tree string for async git
97
+ // operations, aka for git demonstration views
98
+ var getTreeQuick = function(commandStr, getTreePromise) {
99
+ var deferred = Q.defer();
100
+ var headless = new HeadlessGit();
101
+ headless.sendCommand(commandStr, deferred);
102
+ deferred.promise.then(function() {
103
+ getTreePromise.resolve(headless.gitEngine.exportTree());
104
+ });
105
+ };
106
+
107
+ HeadlessGit.prototype.sendCommand = function(value, entireCommandPromise) {
108
+ var deferred = Q.defer();
109
+ var chain = deferred.promise;
110
+ var startTime = new Date().getTime();
111
+
112
+ var commands = [];
113
+
114
+ util.splitTextCommand(value, function(commandStr) {
115
+ chain = chain.then(function() {
116
+ var commandObj = new Command({
117
+ rawStr: commandStr
118
+ });
119
+
120
+ var thisDeferred = Q.defer();
121
+ this.gitEngine.dispatch(commandObj, thisDeferred);
122
+ commands.push(commandObj);
123
+ return thisDeferred.promise;
124
+ }.bind(this));
125
+ }, this);
126
+
127
+ chain.then(function() {
128
+ var nowTime = new Date().getTime();
129
+ if (entireCommandPromise) {
130
+ entireCommandPromise.resolve(commands);
131
+ }
132
+ });
133
+
134
+ chain.fail(function(err) {
135
+ console.log('!!!!!!!! error !!!!!!!');
136
+ console.log(err);
137
+ console.log(err.stack);
138
+ console.log('!!!!!!!!!!!!!!!!!!!!!!');
139
+ });
140
+ deferred.resolve();
141
+ return chain;
142
+ };
143
+
144
+ exports.HeadlessGit = HeadlessGit;
145
+ exports.getTreeQuick = getTreeQuick;
src/js/git/index.js ADDED
@@ -0,0 +1,3202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Backbone = require('backbone');
2
+ var Q = require('q');
3
+
4
+ var intl = require('../intl');
5
+
6
+ var AnimationFactory = require('../visuals/animation/animationFactory').AnimationFactory;
7
+ var AnimationQueue = require('../visuals/animation').AnimationQueue;
8
+ var TreeCompare = require('../graph/treeCompare');
9
+
10
+ var Graph = require('../graph');
11
+ var Errors = require('../util/errors');
12
+ var Main = require('../app');
13
+ var Commands = require('../commands');
14
+ var GitError = Errors.GitError;
15
+ var CommandResult = Errors.CommandResult;
16
+
17
+ var ORIGIN_PREFIX = 'o/';
18
+ var TAB = '&nbsp;&nbsp;&nbsp;';
19
+ var SHORT_CIRCUIT_CHAIN = 'STAPH';
20
+
21
+ function catchShortCircuit(err) {
22
+ if (err !== SHORT_CIRCUIT_CHAIN) {
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ function GitEngine(options) {
28
+ this.rootCommit = null;
29
+ this.refs = {};
30
+ this.HEAD = null;
31
+ this.origin = null;
32
+ this.mode = 'git';
33
+ this.localRepo = null;
34
+
35
+ this.branchCollection = options.branches;
36
+ this.tagCollection = options.tags;
37
+ this.commitCollection = options.collection;
38
+ this.gitVisuals = options.gitVisuals;
39
+
40
+ this.eventBaton = options.eventBaton;
41
+ this.eventBaton.stealBaton('processGitCommand', this.dispatch, this);
42
+
43
+ // poor man's dependency injection. we can't reassign
44
+ // the module variable because its get clobbered :P
45
+ this.animationFactory = (options.animationFactory) ?
46
+ options.animationFactory : AnimationFactory;
47
+
48
+ this.initUniqueID();
49
+ }
50
+
51
+ GitEngine.prototype.initUniqueID = function() {
52
+ // backbone or something uses _ .uniqueId, so we make our own here
53
+ this.uniqueId = (function() {
54
+ var n = 0;
55
+ return function(prepend) {
56
+ return prepend ? prepend + n++ : n++;
57
+ };
58
+ })();
59
+ };
60
+
61
+ GitEngine.prototype.handleModeChange = function(vcs, callback) {
62
+ if (this.mode === vcs) {
63
+ // don't fire event aggressively
64
+ callback();
65
+ return;
66
+ }
67
+ Main.getEvents().trigger('vcsModeChange', {mode: vcs});
68
+ var chain = this.setMode(vcs);
69
+ if (this.origin) {
70
+ this.origin.setMode(vcs, function() {});
71
+ }
72
+
73
+ if (!chain) {
74
+ callback();
75
+ return;
76
+ }
77
+ // we have to do it async
78
+ chain.then(callback);
79
+ };
80
+
81
+ GitEngine.prototype.getIsHg = function() {
82
+ return this.mode === 'hg';
83
+ };
84
+
85
+ GitEngine.prototype.setMode = function(vcs) {
86
+ var switchedToHg = (this.mode === 'git' && vcs === 'hg');
87
+ this.mode = vcs;
88
+ if (!switchedToHg) {
89
+ return;
90
+ }
91
+ // if we are switching to mercurial then we have some
92
+ // garbage collection and other tidying up to do. this
93
+ // may or may not require a refresh so lets check.
94
+ var deferred = Q.defer();
95
+ deferred.resolve();
96
+ var chain = deferred.promise;
97
+
98
+ // this stuff is tricky because we don't animate when
99
+ // we didn't do anything, but we DO animate when
100
+ // either of the operations happen. so a lot of
101
+ // branching ahead...
102
+ var neededUpdate = this.updateAllBranchesForHg();
103
+ if (neededUpdate) {
104
+ chain = chain.then(function() {
105
+ return this.animationFactory.playRefreshAnimationSlow(this.gitVisuals);
106
+ }.bind(this));
107
+
108
+ // ok we need to refresh anyways, so do the prune after
109
+ chain = chain.then(function() {
110
+ var neededPrune = this.pruneTree();
111
+ if (!neededPrune) {
112
+ return;
113
+ }
114
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
115
+ }.bind(this));
116
+
117
+ return chain;
118
+ }
119
+
120
+ // ok might need prune though
121
+ var pruned = this.pruneTree();
122
+ if (!pruned) {
123
+ // do sync
124
+ return;
125
+ }
126
+
127
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
128
+ };
129
+
130
+ GitEngine.prototype.assignLocalRepo = function(repo) {
131
+ this.localRepo = repo;
132
+ };
133
+
134
+ GitEngine.prototype.defaultInit = function() {
135
+ var defaultTree = Graph.getDefaultTree();
136
+ this.loadTree(defaultTree);
137
+ };
138
+
139
+ GitEngine.prototype.init = function() {
140
+ // make an initial commit and a main branch
141
+ this.rootCommit = this.makeCommit(null, null, {rootCommit: true});
142
+ this.commitCollection.add(this.rootCommit);
143
+
144
+ var main = this.makeBranch('main', this.rootCommit);
145
+ this.HEAD = new Ref({
146
+ id: 'HEAD',
147
+ target: main
148
+ });
149
+ this.refs[this.HEAD.get('id')] = this.HEAD;
150
+
151
+ // commit once to get things going
152
+ this.commit();
153
+ };
154
+
155
+ GitEngine.prototype.hasOrigin = function() {
156
+ return !!this.origin;
157
+ };
158
+
159
+ GitEngine.prototype.isOrigin = function() {
160
+ return !!this.localRepo;
161
+ };
162
+
163
+ GitEngine.prototype.exportTreeForBranch = function(branchName) {
164
+ // this method exports the tree and then prunes everything that
165
+ // is not connected to branchname
166
+ var tree = this.exportTree();
167
+ // get the upstream set
168
+ var set = Graph.getUpstreamSet(this, branchName);
169
+ // now loop through and delete commits
170
+ var commitsToLoop = tree.commits;
171
+ tree.commits = {};
172
+ commitsToLoop.forEach(function(commit, id) {
173
+ if (set[id]) {
174
+ // if included in target branch
175
+ tree.commits[id] = commit;
176
+ }
177
+ });
178
+
179
+ var branchesToLoop = tree.branches;
180
+ tree.branches = {};
181
+ branchesToLoop.forEach(function(branch, id) {
182
+ if (id === branchName) {
183
+ tree.branches[id] = branch;
184
+ }
185
+ });
186
+
187
+ tree.HEAD.target = branchName;
188
+ return tree;
189
+ };
190
+
191
+ GitEngine.prototype.exportTree = function() {
192
+ // need to export all commits, their connectivity / messages, branches, and state of head.
193
+ // this would be simple if didn't have circular structures.... :P
194
+ // thus, we need to loop through and "flatten" our graph of objects referencing one another
195
+ var totalExport = {
196
+ branches: {},
197
+ commits: {},
198
+ tags: {},
199
+ HEAD: null
200
+ };
201
+
202
+ this.branchCollection.toJSON().forEach(function(branch) {
203
+ branch.target = branch.target.get('id');
204
+ delete branch.visBranch;
205
+
206
+ totalExport.branches[branch.id] = branch;
207
+ });
208
+
209
+ this.commitCollection.toJSON().forEach(function(commit) {
210
+ // clear out the fields that reference objects and create circular structure
211
+ Commit.prototype.constants.circularFields.forEach(function(field) {
212
+ delete commit[field];
213
+ });
214
+
215
+ // convert parents
216
+ commit.parents = (commit.parents || []).map(function(par) {
217
+ return par.get('id');
218
+ });
219
+
220
+ totalExport.commits[commit.id] = commit;
221
+ }, this);
222
+
223
+ this.tagCollection.toJSON().forEach(function(tag) {
224
+ delete tag.visTag;
225
+ tag.target = tag.target.get('id');
226
+
227
+ totalExport.tags[tag.id] = tag;
228
+ }, this);
229
+
230
+ var HEAD = this.HEAD.toJSON();
231
+ HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = HEAD.visTag = undefined;
232
+ HEAD.target = HEAD.target.get('id');
233
+ totalExport.HEAD = HEAD;
234
+
235
+ if (this.hasOrigin()) {
236
+ totalExport.originTree = this.origin.exportTree();
237
+ }
238
+
239
+ return totalExport;
240
+ };
241
+
242
+ GitEngine.prototype.printTree = function(tree) {
243
+ tree = tree || this.exportTree();
244
+ TreeCompare.reduceTreeFields([tree]);
245
+
246
+ var str = JSON.stringify(tree);
247
+ if (/'/.test(str)) {
248
+ // escape it to make it more copy paste friendly
249
+ str = escape(str);
250
+ }
251
+ return str;
252
+ };
253
+
254
+ GitEngine.prototype.printAndCopyTree = function() {
255
+ window.prompt(
256
+ intl.str('Copy the tree string below'),
257
+ this.printTree()
258
+ );
259
+ };
260
+
261
+ GitEngine.prototype.loadTree = function(tree) {
262
+ // deep copy in case we use it a bunch. lol awesome copy method
263
+ tree = JSON.parse(JSON.stringify(tree));
264
+
265
+ // first clear everything
266
+ this.removeAll();
267
+
268
+ this.instantiateFromTree(tree);
269
+
270
+ this.reloadGraphics();
271
+ this.initUniqueID();
272
+ };
273
+
274
+ GitEngine.prototype.loadTreeFromString = function(treeString) {
275
+ this.loadTree(JSON.parse(unescape(this.crappyUnescape(treeString))));
276
+ };
277
+
278
+ GitEngine.prototype.instantiateFromTree = function(tree) {
279
+ // now we do the loading part
280
+ var createdSoFar = {};
281
+
282
+ Object.values(tree.commits).forEach(function(commitJSON) {
283
+ var commit = this.getOrMakeRecursive(tree, createdSoFar, commitJSON.id, this.gitVisuals);
284
+ this.commitCollection.add(commit);
285
+ }, this);
286
+
287
+ Object.values(tree.branches).forEach(function(branchJSON) {
288
+ var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id, this.gitVisuals);
289
+
290
+ this.branchCollection.add(branch, {silent: true});
291
+ }, this);
292
+
293
+ Object.values(tree.tags || {}).forEach(function(tagJSON) {
294
+ var tag = this.getOrMakeRecursive(tree, createdSoFar, tagJSON.id, this.gitVisuals);
295
+
296
+ this.tagCollection.add(tag, {silent: true});
297
+ }, this);
298
+
299
+ var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id, this.gitVisuals);
300
+ this.HEAD = HEAD;
301
+
302
+ this.rootCommit = createdSoFar['C0'];
303
+ if (!this.rootCommit) {
304
+ throw new Error('Need root commit of C0 for calculations');
305
+ }
306
+ this.refs = createdSoFar;
307
+
308
+ this.gitVisuals.gitReady = false;
309
+ this.branchCollection.each(function(branch) {
310
+ this.gitVisuals.addBranch(branch);
311
+ }, this);
312
+ this.tagCollection.each(function(tag) {
313
+ this.gitVisuals.addTag(tag);
314
+ }, this);
315
+
316
+ if (tree.originTree) {
317
+ var treeString = JSON.stringify(tree.originTree);
318
+ // if we don't have an animation queue (like when loading
319
+ // right away), just go ahead and make an empty one
320
+ this.animationQueue = this.animationQueue || new AnimationQueue({
321
+ callback: function() {}
322
+ });
323
+ this.makeOrigin(treeString);
324
+ }
325
+ };
326
+
327
+ GitEngine.prototype.makeOrigin = function(treeString) {
328
+ if (this.hasOrigin()) {
329
+ throw new GitError({
330
+ msg: intl.str('git-error-origin-exists')
331
+ });
332
+ }
333
+ treeString = treeString || this.printTree(this.exportTreeForBranch('main'));
334
+
335
+ // this is super super ugly but a necessary hack because of the way LGB was
336
+ // originally designed. We need to get to the top level visualization from
337
+ // the git engine -- aka we need to access our own visuals, then the
338
+ // visualization and ask the main vis to create a new vis/git pair. Then
339
+ // we grab the gitengine out of that and assign that as our origin repo
340
+ // which connects the two. epic
341
+ var mainVis = this.gitVisuals.getVisualization();
342
+ var originVis = mainVis.makeOrigin({
343
+ localRepo: this,
344
+ treeString: treeString
345
+ });
346
+
347
+ // defer the starting of our animation until origin has been created
348
+ this.animationQueue.set('promiseBased', true);
349
+ originVis.customEvents.on('gitEngineReady', function() {
350
+ this.origin = originVis.gitEngine;
351
+ originVis.gitEngine.assignLocalRepo(this);
352
+ this.syncRemoteBranchFills();
353
+ // and then here is the crazy part -- we need the ORIGIN to refresh
354
+ // itself in a separate animation. @_____@
355
+ this.origin.externalRefresh();
356
+ this.animationFactory.playRefreshAnimationAndFinish(this.gitVisuals, this.animationQueue);
357
+ }, this);
358
+
359
+ var originTree = JSON.parse(unescape(treeString));
360
+ // make an origin branch for each branch mentioned in the tree if its
361
+ // not made already...
362
+ Object.keys(originTree.branches).forEach(function(branchName) {
363
+ var branchJSON = originTree.branches[branchName];
364
+ if (this.refs[ORIGIN_PREFIX + branchName]) {
365
+ // we already have this branch
366
+ return;
367
+ }
368
+
369
+ var originTarget = this.findCommonAncestorWithRemote(
370
+ branchJSON.target
371
+ );
372
+
373
+ // now we have something in common, lets make the tracking branch
374
+ var remoteBranch = this.makeBranch(
375
+ ORIGIN_PREFIX + branchName,
376
+ this.getCommitFromRef(originTarget)
377
+ );
378
+
379
+ this.setLocalToTrackRemote(this.refs[branchJSON.id], remoteBranch);
380
+ }, this);
381
+ };
382
+
383
+ GitEngine.prototype.makeRemoteBranchIfNeeded = function(branchName) {
384
+ if (this.doesRefExist(ORIGIN_PREFIX + branchName)) {
385
+ return;
386
+ }
387
+ // if its not a branch on origin then bounce
388
+ var source = this.origin.resolveID(branchName);
389
+ if (source.get('type') !== 'branch') {
390
+ return;
391
+ }
392
+
393
+ return this.makeRemoteBranchForRemote(branchName);
394
+ };
395
+
396
+ GitEngine.prototype.makeBranchIfNeeded = function(branchName) {
397
+ if (this.doesRefExist(branchName)) {
398
+ return;
399
+ }
400
+ var where = this.findCommonAncestorForRemote(
401
+ this.getCommitFromRef('HEAD').get('id')
402
+ );
403
+
404
+ return this.validateAndMakeBranch(branchName, this.getCommitFromRef(where));
405
+ };
406
+
407
+ GitEngine.prototype.makeRemoteBranchForRemote = function(branchName) {
408
+ var target = this.origin.resolveID(branchName).get('target');
409
+ var originTarget = this.findCommonAncestorWithRemote(
410
+ target.get('id')
411
+ );
412
+ return this.makeBranch(
413
+ ORIGIN_PREFIX + branchName,
414
+ this.getCommitFromRef(originTarget)
415
+ );
416
+ };
417
+
418
+ GitEngine.prototype.findCommonAncestorForRemote = function(myTarget) {
419
+ if (this.origin.refs[myTarget]) {
420
+ return myTarget;
421
+ }
422
+ var parents = this.refs[myTarget].get('parents');
423
+ if (parents.length === 1) {
424
+ // Easy, we only have one parent. lets just go upwards
425
+ myTarget = parents[0].get('id');
426
+ // Recurse upwards to find where our remote has a commit.
427
+ return this.findCommonAncestorForRemote(myTarget);
428
+ }
429
+ // We have multiple parents so find out where these two meet.
430
+ var leftTarget = this.findCommonAncestorForRemote(parents[0].get('id'));
431
+ var rightTarget = this.findCommonAncestorForRemote(parents[1].get('id'));
432
+ return this.getCommonAncestor(
433
+ leftTarget,
434
+ rightTarget,
435
+ true // don't throw since we don't know the order here.
436
+ ).get('id');
437
+ };
438
+
439
+ GitEngine.prototype.findCommonAncestorWithRemote = function(originTarget) {
440
+ if (this.refs[originTarget]) {
441
+ return originTarget;
442
+ }
443
+ // now this is tricky -- our remote could have commits that we do
444
+ // not have. so lets go upwards until we find one that we have
445
+ var parents = this.origin.refs[originTarget].get('parents');
446
+ if (parents.length === 1) {
447
+ return this.findCommonAncestorWithRemote(parents[0].get('id'));
448
+ }
449
+ // Like above, could have two parents
450
+ var leftTarget = this.findCommonAncestorWithRemote(parents[0].get('id'));
451
+ var rightTarget = this.findCommonAncestorWithRemote(parents[1].get('id'));
452
+ return this.getCommonAncestor(leftTarget, rightTarget, true /* don't throw */).get('id');
453
+ };
454
+
455
+ GitEngine.prototype.makeBranchOnOriginAndTrack = function(branchName, target) {
456
+ var remoteBranch = this.makeBranch(
457
+ ORIGIN_PREFIX + branchName,
458
+ this.getCommitFromRef(target)
459
+ );
460
+
461
+ if (this.refs[branchName]) { // not all remote branches have tracking ones
462
+ this.setLocalToTrackRemote(this.refs[branchName], remoteBranch);
463
+ }
464
+
465
+ var originTarget = this.findCommonAncestorForRemote(
466
+ this.getCommitFromRef(target).get('id')
467
+ );
468
+ this.origin.makeBranch(
469
+ branchName,
470
+ this.origin.getCommitFromRef(originTarget)
471
+ );
472
+ };
473
+
474
+ GitEngine.prototype.setLocalToTrackRemote = function(localBranch, remoteBranch) {
475
+ localBranch.setRemoteTrackingBranchID(remoteBranch.get('id'));
476
+
477
+ if (!this.command) {
478
+ // during init we have no command
479
+ return;
480
+ }
481
+
482
+ var msg = 'local branch "' +
483
+ localBranch.get('id') +
484
+ '" set to track remote branch "' +
485
+ remoteBranch.get('id') +
486
+ '"';
487
+ this.command.addWarning(intl.todo(msg));
488
+ };
489
+
490
+ GitEngine.prototype.getOrMakeRecursive = function(
491
+ tree,
492
+ createdSoFar,
493
+ objID,
494
+ gitVisuals
495
+ ) {
496
+ if (createdSoFar[objID]) {
497
+ // base case
498
+ return createdSoFar[objID];
499
+ }
500
+
501
+ var getType = function(tree, id) {
502
+ if (tree.commits[id]) {
503
+ return 'commit';
504
+ } else if (tree.branches[id]) {
505
+ return 'branch';
506
+ } else if (id == 'HEAD') {
507
+ return 'HEAD';
508
+ } else if (tree.tags[id]) {
509
+ return 'tag';
510
+ }
511
+ throw new Error("bad type for " + id);
512
+ };
513
+
514
+ // figure out what type
515
+ var type = getType(tree, objID);
516
+
517
+ if (type == 'HEAD') {
518
+ var headJSON = tree.HEAD;
519
+ var HEAD = new Ref(Object.assign(
520
+ tree.HEAD,
521
+ {
522
+ target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target)
523
+ }
524
+ ));
525
+ createdSoFar[objID] = HEAD;
526
+ return HEAD;
527
+ }
528
+
529
+ if (type == 'branch') {
530
+ var branchJSON = tree.branches[objID];
531
+
532
+ var branch = new Branch(Object.assign(
533
+ tree.branches[objID],
534
+ {
535
+ target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target)
536
+ }
537
+ ));
538
+ createdSoFar[objID] = branch;
539
+ return branch;
540
+ }
541
+
542
+ if (type == 'tag') {
543
+ var tagJSON = tree.tags[objID];
544
+
545
+ var tag = new Tag(Object.assign(
546
+ tree.tags[objID],
547
+ {
548
+ target: this.getOrMakeRecursive(tree, createdSoFar, tagJSON.target)
549
+ }
550
+ ));
551
+ createdSoFar[objID] = tag;
552
+ return tag;
553
+ }
554
+
555
+ if (type == 'commit') {
556
+ // for commits, we need to grab all the parents
557
+ var commitJSON = tree.commits[objID];
558
+
559
+ var parentObjs = commitJSON.parents.map(function(parentID) {
560
+ return this.getOrMakeRecursive(tree, createdSoFar, parentID);
561
+ }, this);
562
+
563
+ var commit = new Commit(Object.assign(
564
+ commitJSON,
565
+ {
566
+ parents: parentObjs,
567
+ gitVisuals: this.gitVisuals
568
+ }
569
+ ));
570
+ createdSoFar[objID] = commit;
571
+ return commit;
572
+ }
573
+
574
+ throw new Error('ruh rho!! unsupported type for ' + objID);
575
+ };
576
+
577
+ GitEngine.prototype.tearDown = function() {
578
+ if (this.tornDown) {
579
+ return;
580
+ }
581
+ this.eventBaton.releaseBaton('processGitCommand', this.dispatch, this);
582
+ this.removeAll();
583
+ this.tornDown = true;
584
+ };
585
+
586
+ GitEngine.prototype.reloadGraphics = function() {
587
+ // get the root commit
588
+ this.gitVisuals.rootCommit = this.refs['C0'];
589
+ // this just basically makes the HEAD branch. the head branch really should have been
590
+ // a member of a collection and not this annoying edge case stuff... one day
591
+ this.gitVisuals.initHeadBranch();
592
+
593
+ // when the paper is ready
594
+ this.gitVisuals.drawTreeFromReload();
595
+
596
+ this.gitVisuals.refreshTreeHarsh();
597
+ };
598
+
599
+ GitEngine.prototype.removeAll = function() {
600
+ this.branchCollection.reset();
601
+ this.tagCollection.reset();
602
+ this.commitCollection.reset();
603
+ this.refs = {};
604
+ this.HEAD = null;
605
+ this.rootCommit = null;
606
+
607
+ if (this.origin) {
608
+ // we will restart all this jazz during init from tree
609
+ this.origin.gitVisuals.getVisualization().tearDown();
610
+ delete this.origin;
611
+ this.gitVisuals.getVisualization().clearOrigin();
612
+ }
613
+
614
+ this.gitVisuals.resetAll();
615
+ };
616
+
617
+ GitEngine.prototype.getDetachedHead = function() {
618
+ // detached head is if HEAD points to a commit instead of a branch...
619
+ var target = this.HEAD.get('target');
620
+ var targetType = target.get('type');
621
+ return targetType !== 'branch';
622
+ };
623
+
624
+ GitEngine.prototype.validateBranchName = function(name) {
625
+ // Lets escape some of the nasty characters
626
+ name = name.replace(/&#x2F;/g,"\/");
627
+ name = name.replace(/\s/g, '');
628
+ // And then just make sure it starts with alpha-numeric,
629
+ // can contain a slash or dash, and then ends with alpha
630
+ if (
631
+ !/^(\w+[.\/\-]?)+\w+$/.test(name) ||
632
+ name.search('o/') === 0
633
+ ) {
634
+ throw new GitError({
635
+ msg: intl.str(
636
+ 'bad-branch-name',
637
+ { branch: name }
638
+ )
639
+ });
640
+ }
641
+ if (/^[cC]\d+$/.test(name)) {
642
+ throw new GitError({
643
+ msg: intl.str(
644
+ 'bad-branch-name',
645
+ { branch: name }
646
+ )
647
+ });
648
+ }
649
+ if (/[hH][eE][aA][dD]/.test(name)) {
650
+ throw new GitError({
651
+ msg: intl.str(
652
+ 'bad-branch-name',
653
+ { branch: name }
654
+ )
655
+ });
656
+ }
657
+ if (name.length > 9) {
658
+ name = name.slice(0, 9);
659
+ this.command.addWarning(
660
+ intl.str(
661
+ 'branch-name-short',
662
+ { branch: name }
663
+ )
664
+ );
665
+ }
666
+ return name;
667
+ };
668
+
669
+ GitEngine.prototype.validateAndMakeBranch = function(id, target) {
670
+ id = this.validateBranchName(id);
671
+ if (this.doesRefExist(id)) {
672
+ throw new GitError({
673
+ msg: intl.str(
674
+ 'bad-branch-name',
675
+ { branch: id }
676
+ )
677
+ });
678
+ }
679
+
680
+ return this.makeBranch(id, target);
681
+ };
682
+
683
+ GitEngine.prototype.validateAndMakeTag = function(id, target) {
684
+ id = this.validateBranchName(id);
685
+ if (this.refs[id]) {
686
+ throw new GitError({
687
+ msg: intl.str(
688
+ 'bad-tag-name',
689
+ { tag: id }
690
+ )
691
+ });
692
+ }
693
+
694
+ this.makeTag(id, target);
695
+ };
696
+
697
+ GitEngine.prototype.makeBranch = function(id, target) {
698
+ if (this.refs[id]) {
699
+ var err = new Error();
700
+ throw new Error('woah already have that ref ' + id + ' ' + err.stack);
701
+ }
702
+
703
+ var branch = new Branch({
704
+ target: target,
705
+ id: id
706
+ });
707
+ this.branchCollection.add(branch);
708
+ this.refs[branch.get('id')] = branch;
709
+ return branch;
710
+ };
711
+
712
+ GitEngine.prototype.makeTag = function(id, target) {
713
+ if (this.refs[id]) {
714
+ throw new Error('woah already have that');
715
+ }
716
+
717
+ var tag = new Tag({
718
+ target: target,
719
+ id: id
720
+ });
721
+ this.tagCollection.add(tag);
722
+ this.refs[tag.get('id')] = tag;
723
+ return tag;
724
+ };
725
+
726
+ GitEngine.prototype.getHead = function() {
727
+ return Object.assign({}, this.HEAD);
728
+ };
729
+
730
+ GitEngine.prototype.getTags = function() {
731
+ var toReturn = [];
732
+ this.tagCollection.each(function(tag) {
733
+ toReturn.push({
734
+ id: tag.get('id'),
735
+ target: tag.get('target'),
736
+ remote: tag.getIsRemote(),
737
+ obj: tag
738
+ });
739
+ }, this);
740
+ return toReturn;
741
+ };
742
+
743
+ GitEngine.prototype.getBranches = function() {
744
+ var toReturn = [];
745
+ this.branchCollection.each(function(branch) {
746
+ toReturn.push({
747
+ id: branch.get('id'),
748
+ selected: this.HEAD.get('target') === branch,
749
+ target: branch.get('target'),
750
+ remote: branch.getIsRemote(),
751
+ obj: branch
752
+ });
753
+ }, this);
754
+ return toReturn;
755
+ };
756
+
757
+ GitEngine.prototype.getRemoteBranches = function() {
758
+ var all = this.getBranches();
759
+ return all.filter(function(branchJSON) {
760
+ return branchJSON.remote === true;
761
+ });
762
+ };
763
+
764
+ GitEngine.prototype.getLocalBranches = function() {
765
+ var all = this.getBranches();
766
+ return all.filter(function(branchJSON) {
767
+ return branchJSON.remote === false;
768
+ });
769
+ };
770
+
771
+ GitEngine.prototype.printBranchesWithout = function(without) {
772
+ var commitToBranches = this.getUpstreamBranchSet();
773
+ var commitID = this.getCommitFromRef(without).get('id');
774
+
775
+ var toPrint = commitToBranches[commitID].map(function (branchJSON) {
776
+ branchJSON.selected = this.HEAD.get('target').get('id') == branchJSON.id;
777
+ return branchJSON;
778
+ }, this);
779
+ this.printBranches(toPrint);
780
+ };
781
+
782
+ GitEngine.prototype.printBranches = function(branches) {
783
+ var result = '';
784
+ branches.forEach(branch => {
785
+ result += (branch.selected ? '* ' : '') + this.resolveName(branch.id).split('"')[1] + '\n';
786
+ });
787
+ throw new CommandResult({
788
+ msg: result
789
+ });
790
+ };
791
+
792
+ GitEngine.prototype.printTags = function(tags) {
793
+ var result = '';
794
+ tags.forEach(function (tag) {
795
+ result += tag.id + '\n';
796
+ });
797
+ throw new CommandResult({
798
+ msg: result
799
+ });
800
+ };
801
+
802
+ GitEngine.prototype.printRemotes = function(options) {
803
+ var result = '';
804
+ if (options.verbose) {
805
+ result += 'origin (fetch)\n';
806
+ result += TAB + 'git@github.com:pcottle/foo.git' + '\n\n';
807
+ result += 'origin (push)\n';
808
+ result += TAB + 'git@github.com:pcottle/foo.git';
809
+ } else {
810
+ result += 'origin';
811
+ }
812
+ throw new CommandResult({
813
+ msg: result
814
+ });
815
+ };
816
+
817
+ GitEngine.prototype.getUniqueID = function() {
818
+ var id = this.uniqueId('C');
819
+
820
+ var hasID = function(idToCheck) {
821
+ // loop through and see if we have it locally or
822
+ // remotely
823
+ if (this.refs[idToCheck]) {
824
+ return true;
825
+ }
826
+ if (this.origin && this.origin.refs[idToCheck]) {
827
+ return true;
828
+ }
829
+ return false;
830
+ }.bind(this);
831
+
832
+ while (hasID(id)) {
833
+ id = this.uniqueId('C');
834
+ }
835
+ return id;
836
+ };
837
+
838
+ GitEngine.prototype.makeCommit = function(parents, id, options) {
839
+ // ok we need to actually manually create commit IDs now because
840
+ // people like nikita (thanks for finding this!) could
841
+ // make branches named C2 before creating the commit C2
842
+ if (!id) {
843
+ id = this.getUniqueID();
844
+ }
845
+
846
+ var commit = new Commit(Object.assign({
847
+ parents: parents,
848
+ id: id,
849
+ gitVisuals: this.gitVisuals
850
+ },
851
+ options || {}
852
+ ));
853
+
854
+ this.refs[commit.get('id')] = commit;
855
+ this.commitCollection.add(commit);
856
+ return commit;
857
+ };
858
+
859
+ GitEngine.prototype.revert = function(whichCommits) {
860
+ // resolve the commits we will rebase
861
+ var toRevert = whichCommits.map(function(stringRef) {
862
+ return this.getCommitFromRef(stringRef);
863
+ }, this);
864
+
865
+ var deferred = Q.defer();
866
+ var chain = deferred.promise;
867
+ var destBranch = this.resolveID('HEAD');
868
+
869
+ chain = this.animationFactory.highlightEachWithPromise(
870
+ chain,
871
+ toRevert,
872
+ destBranch
873
+ );
874
+
875
+ var base = this.getCommitFromRef('HEAD');
876
+ // each step makes a new commit
877
+ var chainStep = function(oldCommit) {
878
+ var newId = this.rebaseAltID(oldCommit.get('id'));
879
+ var commitMessage = intl.str('git-revert-msg', {
880
+ oldCommit: this.resolveName(oldCommit),
881
+ oldMsg: oldCommit.get('commitMessage')
882
+ });
883
+ var newCommit = this.makeCommit([base], newId, {
884
+ commitMessage: commitMessage
885
+ });
886
+ base = newCommit;
887
+
888
+ return this.animationFactory.playCommitBirthPromiseAnimation(
889
+ newCommit,
890
+ this.gitVisuals
891
+ );
892
+ }.bind(this);
893
+
894
+ // set up the promise chain
895
+ toRevert.forEach(function (commit) {
896
+ chain = chain.then(function() {
897
+ return chainStep(commit);
898
+ });
899
+ });
900
+
901
+ // done! update our location
902
+ chain = chain.then(function() {
903
+ this.setTargetLocation('HEAD', base);
904
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
905
+ }.bind(this));
906
+
907
+ this.animationQueue.thenFinish(chain, deferred);
908
+ };
909
+
910
+ GitEngine.prototype.reset = function(target) {
911
+ this.setTargetLocation('HEAD', this.getCommitFromRef(target));
912
+ };
913
+
914
+ GitEngine.prototype.setupCherrypickChain = function(toCherrypick) {
915
+ // error checks are all good, lets go!
916
+ var deferred = Q.defer();
917
+ var chain = deferred.promise;
918
+ var destinationBranch = this.resolveID('HEAD');
919
+
920
+ chain = this.animationFactory.highlightEachWithPromise(
921
+ chain,
922
+ toCherrypick,
923
+ destinationBranch
924
+ );
925
+
926
+ var chainStep = function(commit) {
927
+ var newCommit = this.cherrypick(commit);
928
+ return this.animationFactory.playCommitBirthPromiseAnimation(
929
+ newCommit,
930
+ this.gitVisuals
931
+ );
932
+ }.bind(this);
933
+
934
+ toCherrypick.forEach(function (arg) {
935
+ chain = chain.then(function() {
936
+ return chainStep(arg);
937
+ });
938
+ }, this);
939
+
940
+ this.animationQueue.thenFinish(chain, deferred);
941
+ };
942
+
943
+ /*************************************
944
+ * Origin stuff!
945
+ ************************************/
946
+
947
+ GitEngine.prototype.checkUpstreamOfSource = function(
948
+ target,
949
+ source,
950
+ targetBranch,
951
+ sourceBranch,
952
+ errorMsg
953
+ ) {
954
+ // here we are downloading some X number of commits from source onto
955
+ // target. Hence target should be strictly upstream of source
956
+
957
+ // lets first get the upstream set from source's dest branch
958
+ var upstream = Graph.getUpstreamSet(source, sourceBranch);
959
+
960
+ var targetLocationID = target.getCommitFromRef(targetBranch).get('id');
961
+ if (!upstream[targetLocationID]) {
962
+ throw new GitError({
963
+ msg: errorMsg || intl.str('git-error-origin-fetch-no-ff')
964
+ });
965
+ }
966
+ };
967
+
968
+ GitEngine.prototype.getTargetGraphDifference = function(
969
+ target,
970
+ source,
971
+ targetBranch,
972
+ sourceBranch,
973
+ options
974
+ ) {
975
+ options = options || {};
976
+ sourceBranch = source.resolveID(sourceBranch);
977
+
978
+ var targetSet = Graph.getUpstreamSet(target, targetBranch);
979
+ var sourceStartCommit = source.getCommitFromRef(sourceBranch);
980
+
981
+ var sourceTree = source.exportTree();
982
+ var sourceStartCommitJSON = sourceTree.commits[sourceStartCommit.get('id')];
983
+
984
+ if (targetSet[sourceStartCommitJSON.id]) {
985
+ // either we throw since theres no work to be done, or we return an empty array
986
+ if (options.dontThrowOnNoFetch) {
987
+ return [];
988
+ } else {
989
+ throw new GitError({
990
+ msg: intl.str('git-error-origin-fetch-uptodate')
991
+ });
992
+ }
993
+ }
994
+
995
+ // ok great, we have our starting point and our stopping set. lets go ahead
996
+ // and traverse upwards and keep track of depth manually
997
+ sourceStartCommitJSON.depth = 0;
998
+ var difference = [];
999
+ var toExplore = [sourceStartCommitJSON];
1000
+
1001
+ var pushParent = function(parentID) {
1002
+ if (targetSet[parentID]) {
1003
+ // we already have that commit, lets bounce
1004
+ return;
1005
+ }
1006
+
1007
+ var parentJSON = sourceTree.commits[parentID];
1008
+ parentJSON.depth = here.depth + 1;
1009
+ toExplore.push(parentJSON);
1010
+ };
1011
+
1012
+ while (toExplore.length) {
1013
+ var here = toExplore.pop();
1014
+ difference.push(here);
1015
+ here.parents.forEach(pushParent);
1016
+ }
1017
+
1018
+ // filter because we weren't doing graph search
1019
+ var differenceUnique = Graph.getUniqueObjects(difference);
1020
+ /**
1021
+ * Ok now we have to determine the order in which to make these commits.
1022
+ * We used to just sort by depth because we were lazy but that is incorrect
1023
+ * since it doesn't represent the actual dependency tree of the commits.
1024
+ *
1025
+ * So here is what we are going to do -- loop through the differenceUnique
1026
+ * set and find a commit that has _all_ its parents in the targetSet. Then
1027
+ * decide to make that commit first, expand targetSet, and then rinse & repeat
1028
+ */
1029
+ var inOrder = [];
1030
+ var allParentsMade = function(node) {
1031
+ var allParents = true;
1032
+ node.parents.forEach(function(parent) {
1033
+ allParents = allParents && targetSet[parent];
1034
+ });
1035
+ return allParents;
1036
+ };
1037
+
1038
+ while (differenceUnique.length) {
1039
+ for (var i = 0; i < differenceUnique.length; i++) {
1040
+ if (!allParentsMade(differenceUnique[i])) {
1041
+ // This commit cannot be made since not all of its dependencies are
1042
+ // satisfied.
1043
+ continue;
1044
+ }
1045
+
1046
+ var makeThis = differenceUnique[i];
1047
+ inOrder.push(makeThis);
1048
+ // remove the commit
1049
+ differenceUnique.splice(i, 1);
1050
+ // expand target set
1051
+ targetSet[makeThis.id] = true;
1052
+ }
1053
+ }
1054
+ return inOrder;
1055
+ };
1056
+
1057
+ GitEngine.prototype.push = function(options) {
1058
+ options = options || {};
1059
+
1060
+ if (options.source === "") {
1061
+ // delete case
1062
+ this.pushDeleteRemoteBranch(
1063
+ this.refs[ORIGIN_PREFIX + options.destination],
1064
+ this.origin.refs[options.destination]
1065
+ );
1066
+ return;
1067
+ }
1068
+
1069
+ var sourceBranch = this.resolveID(options.source);
1070
+ if (sourceBranch && sourceBranch.attributes.type === 'tag') {
1071
+ throw new GitError({
1072
+ msg: intl.todo('Tags are not allowed as sources for pushing'),
1073
+ });
1074
+ }
1075
+
1076
+ if (!this.origin.doesRefExist(options.destination)) {
1077
+ console.warn('ref', options.destination);
1078
+ this.makeBranchOnOriginAndTrack(
1079
+ options.destination,
1080
+ this.getCommitFromRef(sourceBranch)
1081
+ );
1082
+ // play an animation now since we might not have to fast forward
1083
+ // anything... this is weird because we are punting an animation
1084
+ // and not resolving the promise but whatever
1085
+ this.animationFactory.playRefreshAnimation(this.origin.gitVisuals);
1086
+ this.animationFactory.playRefreshAnimation(this.gitVisuals);
1087
+ }
1088
+ var branchOnRemote = this.origin.resolveID(options.destination);
1089
+ var sourceLocation = this.resolveID(options.source || 'HEAD');
1090
+
1091
+ // first check if this is even allowed by checking the sync between
1092
+ if (!options.force) {
1093
+ this.checkUpstreamOfSource(
1094
+ this,
1095
+ this.origin,
1096
+ branchOnRemote,
1097
+ sourceLocation,
1098
+ intl.str('git-error-origin-push-no-ff')
1099
+ );
1100
+ }
1101
+
1102
+ var commitsToMake = this.getTargetGraphDifference(
1103
+ this.origin,
1104
+ this,
1105
+ branchOnRemote,
1106
+ sourceLocation,
1107
+ /* options */ {
1108
+ dontThrowOnNoFetch: true,
1109
+ }
1110
+ );
1111
+ if (!commitsToMake.length) {
1112
+ if (!options.force) {
1113
+ // We are already up to date, and we can't be deleting
1114
+ // either since we don't have --force
1115
+ throw new GitError({
1116
+ msg: intl.str('git-error-origin-fetch-uptodate')
1117
+ });
1118
+ } else {
1119
+ var sourceCommit = this.getCommitFromRef(sourceBranch);
1120
+ var originCommit = this.getCommitFromRef(branchOnRemote);
1121
+ if (sourceCommit.id === originCommit.id) {
1122
+ // This is essentially also being up to date
1123
+ throw new GitError({
1124
+ msg: intl.str('git-error-origin-fetch-uptodate')
1125
+ });
1126
+ }
1127
+ // Otherwise fall through! We will update origin
1128
+ // and essentially delete the commit
1129
+ }
1130
+ }
1131
+
1132
+ // now here is the tricky part -- the difference between local main
1133
+ // and remote main might be commits C2, C3, and C4, but the remote
1134
+ // might already have those commits. In this case, we don't need to
1135
+ // make them, so filter these out
1136
+ commitsToMake = commitsToMake.filter(function(commitJSON) {
1137
+ return !this.origin.refs[commitJSON.id];
1138
+ }, this);
1139
+
1140
+ var makeCommit = function(id, parentIDs) {
1141
+ // need to get the parents first. since we order by depth, we know
1142
+ // the dependencies are there already
1143
+ var parents = parentIDs.map(function(parentID) {
1144
+ return this.origin.refs[parentID];
1145
+ }, this);
1146
+ return this.origin.makeCommit(parents, id);
1147
+ }.bind(this);
1148
+
1149
+ // now make the promise chain to make each commit
1150
+ var chainStep = function(id, parents) {
1151
+ var newCommit = makeCommit(id, parents);
1152
+ return this.animationFactory.playCommitBirthPromiseAnimation(
1153
+ newCommit,
1154
+ this.origin.gitVisuals
1155
+ );
1156
+ }.bind(this);
1157
+
1158
+ var deferred = Q.defer();
1159
+ var chain = deferred.promise;
1160
+
1161
+ commitsToMake.forEach(function(commitJSON) {
1162
+ chain = chain.then(function() {
1163
+ return this.animationFactory.playHighlightPromiseAnimation(
1164
+ this.refs[commitJSON.id],
1165
+ branchOnRemote
1166
+ );
1167
+ }.bind(this));
1168
+
1169
+ chain = chain.then(function() {
1170
+ return chainStep(
1171
+ commitJSON.id,
1172
+ commitJSON.parents
1173
+ );
1174
+ });
1175
+ }, this);
1176
+
1177
+ chain = chain.then(function() {
1178
+ var localLocationID = this.getCommitFromRef(sourceLocation).get('id');
1179
+ var remoteCommit = this.origin.refs[localLocationID];
1180
+ this.origin.setTargetLocation(branchOnRemote, remoteCommit);
1181
+ // unhighlight local
1182
+ this.animationFactory.playRefreshAnimation(this.gitVisuals);
1183
+ return this.animationFactory.playRefreshAnimation(this.origin.gitVisuals);
1184
+ }.bind(this));
1185
+
1186
+ // HAX HAX update main and remote tracking for main
1187
+ chain = chain.then(function() {
1188
+ var localCommit = this.getCommitFromRef(sourceLocation);
1189
+ this.setTargetLocation(this.resolveID(ORIGIN_PREFIX + options.destination), localCommit);
1190
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
1191
+ }.bind(this));
1192
+
1193
+ if (!options.dontResolvePromise) {
1194
+ this.animationQueue.thenFinish(chain, deferred);
1195
+ }
1196
+ };
1197
+
1198
+ GitEngine.prototype.pushDeleteRemoteBranch = function(
1199
+ remoteBranch,
1200
+ branchOnRemote
1201
+ ) {
1202
+ if (branchOnRemote.get('id') === 'main') {
1203
+ throw new GitError({
1204
+ msg: intl.todo('You cannot delete main branch on remote!')
1205
+ });
1206
+ }
1207
+ // ok so this isn't too bad -- we basically just:
1208
+ // 1) instruct the remote to delete the branch
1209
+ // 2) kill off the remote branch locally
1210
+ // 3) find any branches tracking this remote branch and set them to not track
1211
+ var id = remoteBranch.get('id');
1212
+ this.origin.deleteBranch(branchOnRemote);
1213
+ this.deleteBranch(remoteBranch);
1214
+ this.branchCollection.each(function(branch) {
1215
+ if (branch.getRemoteTrackingBranchID() === id) {
1216
+ branch.setRemoteTrackingBranchID(null);
1217
+ }
1218
+ }, this);
1219
+
1220
+ // animation needs to be triggered on origin directly
1221
+ this.origin.pruneTree();
1222
+ this.origin.externalRefresh();
1223
+ };
1224
+
1225
+ GitEngine.prototype.fetch = function(options) {
1226
+ options = options || {};
1227
+ var didMakeBranch;
1228
+
1229
+ // first check for super stupid case where we are just making
1230
+ // a branch with fetch...
1231
+ if (options.destination && options.source === '') {
1232
+ this.validateAndMakeBranch(
1233
+ options.destination,
1234
+ this.getCommitFromRef('HEAD')
1235
+ );
1236
+ return;
1237
+ } else if (options.destination && options.source) {
1238
+ didMakeBranch = didMakeBranch || this.makeRemoteBranchIfNeeded(options.source);
1239
+ didMakeBranch = didMakeBranch || this.makeBranchIfNeeded(options.destination);
1240
+ options.didMakeBranch = didMakeBranch;
1241
+
1242
+ return this.fetchCore([{
1243
+ destination: options.destination,
1244
+ source: options.source
1245
+ }],
1246
+ options
1247
+ );
1248
+ }
1249
+ // get all remote branches and specify the dest / source pairs
1250
+ var allBranchesOnRemote = this.origin.branchCollection.toArray();
1251
+ var sourceDestPairs = allBranchesOnRemote.map(function(branch) {
1252
+ var branchName = branch.get('id');
1253
+ didMakeBranch = didMakeBranch || this.makeRemoteBranchIfNeeded(branchName);
1254
+
1255
+ return {
1256
+ destination: branch.getPrefixedID(),
1257
+ source: branchName
1258
+ };
1259
+ }, this);
1260
+ options.didMakeBranch = didMakeBranch;
1261
+ return this.fetchCore(sourceDestPairs, options);
1262
+ };
1263
+
1264
+ GitEngine.prototype.fetchCore = function(sourceDestPairs, options) {
1265
+ // first check if our local remote branch is upstream of the origin branch set.
1266
+ // this check essentially pretends the local remote branch is in origin and
1267
+ // could be fast forwarded (basic sanity check)
1268
+ sourceDestPairs.forEach(function (pair) {
1269
+ this.checkUpstreamOfSource(
1270
+ this,
1271
+ this.origin,
1272
+ pair.destination,
1273
+ pair.source
1274
+ );
1275
+ }, this);
1276
+
1277
+ // then we get the difference in commits between these two graphs
1278
+ var commitsToMake = [];
1279
+ sourceDestPairs.forEach(function (pair) {
1280
+ commitsToMake = commitsToMake.concat(this.getTargetGraphDifference(
1281
+ this,
1282
+ this.origin,
1283
+ pair.destination,
1284
+ pair.source,
1285
+ Object.assign(
1286
+ {},
1287
+ options,
1288
+ {dontThrowOnNoFetch: true}
1289
+ )
1290
+ ));
1291
+ }, this);
1292
+
1293
+ if (!commitsToMake.length && !options.dontThrowOnNoFetch) {
1294
+ throw new GitError({
1295
+ msg: intl.str('git-error-origin-fetch-uptodate')
1296
+ });
1297
+ }
1298
+
1299
+ // we did this for each remote branch, but we still need to reduce to unique
1300
+ // and sort. in this particular app we can never have unfected remote
1301
+ // commits that are upstream of multiple branches (since the fakeTeamwork
1302
+ // command simply commits), but we are doing it anyways for correctness
1303
+ commitsToMake = Graph.getUniqueObjects(commitsToMake);
1304
+ commitsToMake = Graph.descendSortDepth(commitsToMake);
1305
+
1306
+ // now here is the tricky part -- the difference between local main
1307
+ // and remote main might be commits C2, C3, and C4, but we
1308
+ // might already have those commits. In this case, we don't need to
1309
+ // make them, so filter these out
1310
+ commitsToMake = commitsToMake.filter(function(commitJSON) {
1311
+ return !this.refs[commitJSON.id];
1312
+ }, this);
1313
+
1314
+ var makeCommit = function(id, parentIDs) {
1315
+ // need to get the parents first. since we order by depth, we know
1316
+ // the dependencies are there already
1317
+ var parents = parentIDs.map(function(parentID) {
1318
+ return this.resolveID(parentID);
1319
+ }, this);
1320
+ return this.makeCommit(parents, id);
1321
+ }.bind(this);
1322
+
1323
+ // now make the promise chain to make each commit
1324
+ var chainStep = function(id, parents) {
1325
+ var newCommit = makeCommit(id, parents);
1326
+ return this.animationFactory.playCommitBirthPromiseAnimation(
1327
+ newCommit,
1328
+ this.gitVisuals
1329
+ );
1330
+ }.bind(this);
1331
+
1332
+ var deferred = Q.defer();
1333
+ var chain = deferred.promise;
1334
+ if (options.didMakeBranch) {
1335
+ chain = chain.then(function() {
1336
+ this.animationFactory.playRefreshAnimation(this.origin.gitVisuals);
1337
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
1338
+ }.bind(this));
1339
+ }
1340
+
1341
+ var originBranchSet = this.origin.getUpstreamBranchSet();
1342
+ commitsToMake.forEach(function (commitJSON) {
1343
+ // technically we could grab the wrong one here
1344
+ // but this works for now
1345
+ var originBranch = originBranchSet[commitJSON.id][0].obj;
1346
+ var localBranch = this.refs[originBranch.getPrefixedID()];
1347
+
1348
+ chain = chain.then(function() {
1349
+ return this.animationFactory.playHighlightPromiseAnimation(
1350
+ this.origin.resolveID(commitJSON.id),
1351
+ localBranch
1352
+ );
1353
+ }.bind(this));
1354
+
1355
+ chain = chain.then(function() {
1356
+ return chainStep(
1357
+ commitJSON.id,
1358
+ commitJSON.parents
1359
+ );
1360
+ });
1361
+ }, this);
1362
+
1363
+ chain = chain.then(function() {
1364
+ // update all the destinations
1365
+ sourceDestPairs.forEach(function (pair) {
1366
+ var ours = this.resolveID(pair.destination);
1367
+ var theirCommitID = this.origin.getCommitFromRef(pair.source).get('id');
1368
+ // by definition we just made the commit with this id,
1369
+ // so we can grab it now
1370
+ var localCommit = this.refs[theirCommitID];
1371
+ this.setTargetLocation(ours, localCommit);
1372
+ }, this);
1373
+
1374
+ // unhighlight origin by refreshing
1375
+ this.animationFactory.playRefreshAnimation(this.origin.gitVisuals);
1376
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
1377
+ }.bind(this));
1378
+
1379
+ if (!options.dontResolvePromise) {
1380
+ this.animationQueue.thenFinish(chain, deferred);
1381
+ }
1382
+ return {
1383
+ chain: chain,
1384
+ deferred: deferred
1385
+ };
1386
+ };
1387
+
1388
+ GitEngine.prototype.pull = function(options) {
1389
+ options = options || {};
1390
+ var localBranch = this.getOneBeforeCommit('HEAD');
1391
+
1392
+ // no matter what fetch
1393
+ var pendingFetch = this.fetch({
1394
+ dontResolvePromise: true,
1395
+ dontThrowOnNoFetch: true,
1396
+ source: options.source,
1397
+ destination: options.destination
1398
+ });
1399
+
1400
+ if (!pendingFetch) {
1401
+ // short circuited for some reason
1402
+ return;
1403
+ }
1404
+
1405
+ var destBranch = this.resolveID(options.destination);
1406
+ // then either rebase or merge
1407
+ if (options.isRebase) {
1408
+ this.pullFinishWithRebase(pendingFetch, localBranch, destBranch);
1409
+ } else {
1410
+ this.pullFinishWithMerge(pendingFetch, localBranch, destBranch);
1411
+ }
1412
+ };
1413
+
1414
+ GitEngine.prototype.pullFinishWithRebase = function(
1415
+ pendingFetch,
1416
+ localBranch,
1417
+ remoteBranch
1418
+ ) {
1419
+ var chain = pendingFetch.chain;
1420
+ var deferred = pendingFetch.deferred;
1421
+ chain = chain.then(function() {
1422
+ if (this.isUpstreamOf(remoteBranch, localBranch)) {
1423
+ this.command.set('error', new CommandResult({
1424
+ msg: intl.str('git-result-uptodate')
1425
+ }));
1426
+ throw SHORT_CIRCUIT_CHAIN;
1427
+ }
1428
+ }.bind(this));
1429
+
1430
+ // delay a bit after the intense refresh animation from
1431
+ // fetch
1432
+ chain = chain.then(function() {
1433
+ return this.animationFactory.getDelayedPromise(300);
1434
+ }.bind(this));
1435
+
1436
+ chain = chain.then(function() {
1437
+ // highlight last commit on o/main to color of
1438
+ // local branch
1439
+ return this.animationFactory.playHighlightPromiseAnimation(
1440
+ this.getCommitFromRef(remoteBranch),
1441
+ localBranch
1442
+ );
1443
+ }.bind(this));
1444
+
1445
+ chain = chain.then(function() {
1446
+ pendingFetch.dontResolvePromise = true;
1447
+
1448
+ // Lets move the git pull --rebase check up here.
1449
+ if (this.isUpstreamOf(localBranch, remoteBranch)) {
1450
+ this.setTargetLocation(
1451
+ localBranch,
1452
+ this.getCommitFromRef(remoteBranch)
1453
+ );
1454
+ this.checkout(localBranch);
1455
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
1456
+ }
1457
+
1458
+ try {
1459
+ return this.rebase(remoteBranch, localBranch, pendingFetch);
1460
+ } catch (err) {
1461
+ this.filterError(err);
1462
+ if (err.getMsg() !== intl.str('git-error-rebase-none')) {
1463
+ throw err;
1464
+ }
1465
+ this.setTargetLocation(
1466
+ localBranch,
1467
+ this.getCommitFromRef(remoteBranch)
1468
+ );
1469
+ this.checkout(localBranch);
1470
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
1471
+ }
1472
+ }.bind(this));
1473
+ chain = chain.fail(catchShortCircuit);
1474
+
1475
+ this.animationQueue.thenFinish(chain, deferred);
1476
+ };
1477
+
1478
+ GitEngine.prototype.pullFinishWithMerge = function(
1479
+ pendingFetch,
1480
+ localBranch,
1481
+ remoteBranch
1482
+ ) {
1483
+ var chain = pendingFetch.chain;
1484
+ var deferred = pendingFetch.deferred;
1485
+
1486
+ chain = chain.then(function() {
1487
+ if (this.mergeCheck(remoteBranch, localBranch)) {
1488
+ this.command.set('error', new CommandResult({
1489
+ msg: intl.str('git-result-uptodate')
1490
+ }));
1491
+ throw SHORT_CIRCUIT_CHAIN;
1492
+ }
1493
+ }.bind(this));
1494
+
1495
+ // delay a bit after the intense refresh animation from
1496
+ // fetch
1497
+ chain = chain.then(function() {
1498
+ return this.animationFactory.getDelayedPromise(300);
1499
+ }.bind(this));
1500
+
1501
+ chain = chain.then(function() {
1502
+ // highlight last commit on o/main to color of
1503
+ // local branch
1504
+ return this.animationFactory.playHighlightPromiseAnimation(
1505
+ this.getCommitFromRef(remoteBranch),
1506
+ localBranch
1507
+ );
1508
+ }.bind(this));
1509
+
1510
+ chain = chain.then(function() {
1511
+ // highlight commit on main to color of remote
1512
+ return this.animationFactory.playHighlightPromiseAnimation(
1513
+ this.getCommitFromRef(localBranch),
1514
+ remoteBranch
1515
+ );
1516
+ }.bind(this));
1517
+
1518
+ // delay and merge
1519
+ chain = chain.then(function() {
1520
+ return this.animationFactory.getDelayedPromise(700);
1521
+ }.bind(this));
1522
+ chain = chain.then(function() {
1523
+ var newCommit = this.merge(remoteBranch);
1524
+ if (!newCommit) {
1525
+ // it is a fast forward
1526
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
1527
+ }
1528
+
1529
+ return this.animationFactory.playCommitBirthPromiseAnimation(
1530
+ newCommit,
1531
+ this.gitVisuals
1532
+ );
1533
+ }.bind(this));
1534
+ chain = chain.fail(catchShortCircuit);
1535
+
1536
+ this.animationQueue.thenFinish(chain, deferred);
1537
+ };
1538
+
1539
+ GitEngine.prototype.fakeTeamwork = function(numToMake, branch) {
1540
+ var makeOriginCommit = function() {
1541
+ var id = this.getUniqueID();
1542
+ return this.origin.receiveTeamwork(id, branch, this.animationQueue);
1543
+ }.bind(this);
1544
+
1545
+ var chainStep = function() {
1546
+ var newCommit = makeOriginCommit();
1547
+ return this.animationFactory.playCommitBirthPromiseAnimation(
1548
+ newCommit,
1549
+ this.origin.gitVisuals
1550
+ );
1551
+ }.bind(this);
1552
+
1553
+ var deferred = Q.defer();
1554
+ var chain = deferred.promise;
1555
+
1556
+ for(var i = 0; i < numToMake; i++) {
1557
+ chain = chain.then(chainStep);
1558
+ }
1559
+ this.animationQueue.thenFinish(chain, deferred);
1560
+ };
1561
+
1562
+ GitEngine.prototype.receiveTeamwork = function(id, branch, animationQueue) {
1563
+ this.checkout(this.resolveID(branch));
1564
+ var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id);
1565
+ this.setTargetLocation(this.HEAD, newCommit);
1566
+
1567
+ return newCommit;
1568
+ };
1569
+
1570
+ GitEngine.prototype.cherrypick = function(commit) {
1571
+ // alter the ID slightly
1572
+ var id = this.rebaseAltID(commit.get('id'));
1573
+
1574
+ // now commit with that id onto HEAD
1575
+ var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id);
1576
+ this.setTargetLocation(this.HEAD, newCommit);
1577
+
1578
+ return newCommit;
1579
+ };
1580
+
1581
+ GitEngine.prototype.commit = function(options) {
1582
+ options = options || {};
1583
+ var targetCommit = this.getCommitFromRef(this.HEAD);
1584
+ var id = null;
1585
+
1586
+ // if we want to amend, go one above
1587
+ if (options.isAmend) {
1588
+ targetCommit = this.resolveID('HEAD~1');
1589
+ id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
1590
+ }
1591
+
1592
+ var newCommit = this.makeCommit([targetCommit], id);
1593
+ if (this.getDetachedHead() && this.mode === 'git') {
1594
+ this.command.addWarning(intl.str('git-warning-detached'));
1595
+ }
1596
+
1597
+ this.setTargetLocation(this.HEAD, newCommit);
1598
+ return newCommit;
1599
+ };
1600
+
1601
+ GitEngine.prototype.resolveNameNoPrefix = function(someRef) {
1602
+ // first get the obj
1603
+ var obj = this.resolveID(someRef);
1604
+ if (obj.get('type') == 'commit') {
1605
+ return obj.get('id');
1606
+ }
1607
+ if (obj.get('type') == 'branch') {
1608
+ return obj.get('id');
1609
+ }
1610
+ // we are dealing with HEAD
1611
+ return this.resolveNameNoPrefix(obj.get('target'));
1612
+ };
1613
+
1614
+ GitEngine.prototype.resolveName = function(someRef) {
1615
+ // first get the obj
1616
+ var obj = this.resolveID(someRef);
1617
+ if (obj.get('type') == 'commit') {
1618
+ return 'commit ' + obj.get('id');
1619
+ }
1620
+ if (obj.get('type') == 'branch') {
1621
+ return 'branch "' + obj.get('id') + '"';
1622
+ }
1623
+ // we are dealing with HEAD
1624
+ return this.resolveName(obj.get('target'));
1625
+ };
1626
+
1627
+ GitEngine.prototype.resolveID = function(idOrTarget) {
1628
+ if (idOrTarget === null || idOrTarget === undefined) {
1629
+ var err = new Error();
1630
+ throw new Error('Don\'t call this with null / undefined: ' + err.stack);
1631
+ }
1632
+
1633
+ if (typeof idOrTarget !== 'string') {
1634
+ return idOrTarget;
1635
+ }
1636
+ return this.resolveStringRef(idOrTarget);
1637
+ };
1638
+
1639
+ GitEngine.prototype.resolveRelativeRef = function(commit, relative) {
1640
+ var regex = /([~\^])(\d*)/g;
1641
+ var matches;
1642
+
1643
+ while (matches = regex.exec(relative)) {
1644
+ var next = commit;
1645
+ var num = matches[2] ? parseInt(matches[2], 10) : 1;
1646
+
1647
+ if (matches[1] == '^') {
1648
+ next = commit.getParent(num-1);
1649
+ } else {
1650
+ while (next && num--) {
1651
+ next = next.getParent(0);
1652
+ }
1653
+ }
1654
+
1655
+ if (!next) {
1656
+ var msg = intl.str('git-error-relative-ref', {
1657
+ commit: commit.id,
1658
+ match: matches[0]
1659
+ });
1660
+ throw new GitError({
1661
+ msg: msg
1662
+ });
1663
+ }
1664
+
1665
+ commit = next;
1666
+ }
1667
+
1668
+ return commit;
1669
+ };
1670
+
1671
+ GitEngine.prototype.doesRefExist = function(ref) {
1672
+ return !!this.refs[ref]
1673
+ };
1674
+
1675
+ GitEngine.prototype.resolveStringRef = function(ref) {
1676
+ ref = this.crappyUnescape(ref);
1677
+
1678
+ if (this.refs[ref]) {
1679
+ return this.refs[ref];
1680
+ }
1681
+ // Commit hashes like C4 are case insensitive
1682
+ if (ref.match(/^c\d+'*/) && this.refs[ref.toUpperCase()]) {
1683
+ return this.refs[ref.toUpperCase()];
1684
+ }
1685
+
1686
+ // Attempt to split ref string into a reference and a string of ~ and ^ modifiers.
1687
+ var startRef = null;
1688
+ var relative = null;
1689
+ var regex = /^([a-zA-Z0-9]+)(([~\^]\d*)*)$/;
1690
+ var matches = regex.exec(ref);
1691
+ if (matches) {
1692
+ startRef = matches[1];
1693
+ relative = matches[2];
1694
+ } else {
1695
+ throw new GitError({
1696
+ msg: intl.str('git-error-exist', {ref: ref})
1697
+ });
1698
+ }
1699
+
1700
+ if (!this.refs[startRef]) {
1701
+ throw new GitError({
1702
+ msg: intl.str('git-error-exist', {ref: ref})
1703
+ });
1704
+ }
1705
+ var commit = this.getCommitFromRef(startRef);
1706
+
1707
+ if (relative) {
1708
+ commit = this.resolveRelativeRef( commit, relative );
1709
+ }
1710
+
1711
+ return commit;
1712
+ };
1713
+
1714
+ GitEngine.prototype.getCommitFromRef = function(ref) {
1715
+ var start = this.resolveID(ref);
1716
+
1717
+ // works for both HEAD and just a single layer. aka branch
1718
+ while (start.get('type') !== 'commit') {
1719
+ start = start.get('target');
1720
+ }
1721
+ return start;
1722
+ };
1723
+
1724
+ GitEngine.prototype.getType = function(ref) {
1725
+ return this.resolveID(ref).get('type');
1726
+ };
1727
+
1728
+ GitEngine.prototype.setTargetLocation = function(ref, target) {
1729
+ if (this.getType(ref) == 'commit') {
1730
+ // nothing to do
1731
+ return;
1732
+ }
1733
+
1734
+ // sets whatever ref is (branch, HEAD, etc) to a target. so if
1735
+ // you pass in HEAD, and HEAD is pointing to a branch, it will update
1736
+ // the branch to that commit, not the HEAD
1737
+ ref = this.getOneBeforeCommit(ref);
1738
+ ref.set('target', target);
1739
+ };
1740
+
1741
+ GitEngine.prototype.updateBranchesFromSet = function(commitSet) {
1742
+ if (!commitSet) {
1743
+ throw new Error('need commit set here');
1744
+ }
1745
+ // commitSet is the set of commits that are stale or moved or whatever.
1746
+ // any branches POINTING to these commits need to be moved!
1747
+
1748
+ // first get a list of what branches influence what commits
1749
+ var upstreamSet = this.getUpstreamBranchSet();
1750
+
1751
+ var branchesToUpdate = {};
1752
+ // now loop over the set we got passed in and find which branches
1753
+ // that means (aka intersection)
1754
+ commitSet.forEach(function (val, id) {
1755
+ upstreamSet[id].forEach(function (branchJSON) {
1756
+ branchesToUpdate[branchJSON.id] = true;
1757
+ });
1758
+ }, this);
1759
+
1760
+ var branchList = branchesToUpdate.map(function(val, id) {
1761
+ return id;
1762
+ });
1763
+ return this.updateBranchesForHg(branchList);
1764
+ };
1765
+
1766
+ GitEngine.prototype.updateAllBranchesForHgAndPlay = function(branchList) {
1767
+ return this.updateBranchesForHg(branchList) &&
1768
+ this.animationFactory.playRefreshAnimationSlow(this.gitVisuals);
1769
+ };
1770
+
1771
+ GitEngine.prototype.updateAllBranchesForHg = function() {
1772
+ var branchList = this.branchCollection.map(function(branch) {
1773
+ return branch.get('id');
1774
+ });
1775
+ return this.updateBranchesForHg(branchList);
1776
+ };
1777
+
1778
+ GitEngine.prototype.syncRemoteBranchFills = function() {
1779
+ this.branchCollection.each(function(branch) {
1780
+ if (!branch.getIsRemote()) {
1781
+ return;
1782
+ }
1783
+ var originBranch = this.origin.refs[branch.getBaseID()];
1784
+ if (!originBranch.get('visBranch')) {
1785
+ // testing mode doesn't get this
1786
+ return;
1787
+ }
1788
+ var originFill = originBranch.get('visBranch').get('fill');
1789
+ branch.get('visBranch').set('fill', originFill);
1790
+ }, this);
1791
+ };
1792
+
1793
+ GitEngine.prototype.updateBranchesForHg = function(branchList) {
1794
+ var hasUpdated = false;
1795
+ branchList.forEach(function (branchID) {
1796
+ // ok now just check if this branch has a more recent commit available.
1797
+ // that mapping is easy because we always do rebase alt id --
1798
+ // theres no way to have C3' and C3''' but no C3''. so just
1799
+ // bump the ID once -- if thats not filled in we are updated,
1800
+ // otherwise loop until you find undefined
1801
+ var commitID = this.getCommitFromRef(branchID).get('id');
1802
+ var altID = this.getBumpedID(commitID);
1803
+ if (!this.refs[altID]) {
1804
+ return;
1805
+ }
1806
+ hasUpdated = true;
1807
+
1808
+ var lastID;
1809
+ while (this.refs[altID]) {
1810
+ lastID = altID;
1811
+ altID = this.rebaseAltID(altID);
1812
+ }
1813
+
1814
+ // last ID is the one we want to update to
1815
+ this.setTargetLocation(this.refs[branchID], this.refs[lastID]);
1816
+ }, this);
1817
+
1818
+ if (!hasUpdated) {
1819
+ return false;
1820
+ }
1821
+ return true;
1822
+ };
1823
+
1824
+ GitEngine.prototype.updateCommitParentsForHgRebase = function(commitSet) {
1825
+ var anyChange = false;
1826
+ Object.keys(commitSet).forEach(function(commitID) {
1827
+ var commit = this.refs[commitID];
1828
+ var thisUpdated = commit.checkForUpdatedParent(this);
1829
+ anyChange = anyChange || thisUpdated;
1830
+ }, this);
1831
+ return anyChange;
1832
+ };
1833
+
1834
+ GitEngine.prototype.pruneTreeAndPlay = function() {
1835
+ return this.pruneTree() &&
1836
+ this.animationFactory.playRefreshAnimationSlow(this.gitVisuals);
1837
+ };
1838
+
1839
+ GitEngine.prototype.pruneTree = function(doPrintWarning = true) {
1840
+ var set = this.getUpstreamBranchSet();
1841
+ // don't prune commits that HEAD depends on
1842
+ var headSet = Graph.getUpstreamSet(this, 'HEAD');
1843
+ Object.keys(headSet).forEach(function(commitID) {
1844
+ set[commitID] = true;
1845
+ });
1846
+ Object.keys(this.getUpstreamTagSet()).forEach(commitID => set[commitID] = true);
1847
+
1848
+ var toDelete = [];
1849
+ this.commitCollection.each(function(commit) {
1850
+ // nothing cares about this commit :(
1851
+ if (!set[commit.get('id')]) {
1852
+ toDelete.push(commit);
1853
+ }
1854
+ }, this);
1855
+
1856
+ if (!toDelete.length) {
1857
+ // returning nothing will perform
1858
+ // the switch sync
1859
+ return;
1860
+ }
1861
+ if (this.command && doPrintWarning) {
1862
+ this.command.addWarning(intl.str('hg-prune-tree'));
1863
+ }
1864
+
1865
+ toDelete.forEach(function (commit) {
1866
+ commit.removeFromParents();
1867
+ this.commitCollection.remove(commit);
1868
+
1869
+ var ID = commit.get('id');
1870
+ this.refs[ID] = undefined;
1871
+ delete this.refs[ID];
1872
+
1873
+ var visNode = commit.get('visNode');
1874
+ if (visNode) {
1875
+ visNode.removeAll();
1876
+ }
1877
+ }, this);
1878
+
1879
+ return true;
1880
+ };
1881
+ GitEngine.prototype.getUpstreamBranchSet = function() {
1882
+ return this.getUpstreamCollectionSet(this.branchCollection);
1883
+ };
1884
+
1885
+ GitEngine.prototype.getUpstreamTagSet = function() {
1886
+ return this.getUpstreamCollectionSet(this.tagCollection);
1887
+ };
1888
+
1889
+ GitEngine.prototype.getUpstreamCollectionSet = function(collection) {
1890
+ // this is expensive!! so only call once in a while
1891
+ var commitToSet = {};
1892
+
1893
+ var inArray = function(arr, id) {
1894
+ var found = false;
1895
+ arr.forEach(function (wrapper) {
1896
+ if (wrapper.id == id) {
1897
+ found = true;
1898
+ }
1899
+ });
1900
+
1901
+ return found;
1902
+ };
1903
+
1904
+ var bfsSearch = function(commit) {
1905
+ var set = [];
1906
+ var pQueue = [commit];
1907
+ while (pQueue.length) {
1908
+ var popped = pQueue.pop();
1909
+ set.push(popped.get('id'));
1910
+
1911
+ if (popped.get('parents') && popped.get('parents').length) {
1912
+ pQueue = pQueue.concat(popped.get('parents'));
1913
+ }
1914
+ }
1915
+ return set;
1916
+ };
1917
+
1918
+ collection.each(function(ref) {
1919
+ var set = bfsSearch(ref.get('target'));
1920
+ set.forEach(function (id) {
1921
+ commitToSet[id] = commitToSet[id] || [];
1922
+
1923
+ // only add it if it's not there, so hue blending is ok
1924
+ if (!inArray(commitToSet[id], ref.get('id'))) {
1925
+ commitToSet[id].push({
1926
+ obj: ref,
1927
+ id: ref.get('id')
1928
+ });
1929
+ }
1930
+ });
1931
+ });
1932
+
1933
+ return commitToSet;
1934
+ };
1935
+
1936
+ GitEngine.prototype.getUpstreamHeadSet = function() {
1937
+ var set = Graph.getUpstreamSet(this, 'HEAD');
1938
+ var including = this.getCommitFromRef('HEAD').get('id');
1939
+
1940
+ set[including] = true;
1941
+ return set;
1942
+ };
1943
+
1944
+ GitEngine.prototype.getOneBeforeCommit = function(ref) {
1945
+ // you can call this command on HEAD in detached, HEAD, or on a branch
1946
+ // and it will return the ref that is one above a commit. aka
1947
+ // it resolves HEAD to something that we can move the ref with
1948
+ var start = this.resolveID(ref);
1949
+ if (start === this.HEAD && !this.getDetachedHead()) {
1950
+ start = start.get('target');
1951
+ }
1952
+ return start;
1953
+ };
1954
+
1955
+ GitEngine.prototype.scrapeBaseID = function(id) {
1956
+ var results = /^C(\d+)/.exec(id);
1957
+
1958
+ if (!results) {
1959
+ throw new Error('regex failed on ' + id);
1960
+ }
1961
+
1962
+ return 'C' + results[1];
1963
+ };
1964
+
1965
+ /*
1966
+ * grabs a bumped ID that is NOT currently reserved
1967
+ */
1968
+ GitEngine.prototype.rebaseAltID = function(id) {
1969
+ var newID = this.getBumpedID(id);
1970
+ while (this.refs[newID]) {
1971
+ newID = this.getBumpedID(newID);
1972
+ }
1973
+ return newID;
1974
+ };
1975
+
1976
+ GitEngine.prototype.getMostRecentBumpedID = function(id) {
1977
+ var newID = id;
1978
+ var lastID;
1979
+ while (this.refs[newID]) {
1980
+ lastID = newID;
1981
+ newID = this.getBumpedID(newID);
1982
+ }
1983
+ return lastID;
1984
+ };
1985
+
1986
+ GitEngine.prototype.getBumpedID = function(id) {
1987
+ // this function alters an ID to add a quote to the end,
1988
+ // indicating that it was rebased.
1989
+ var regexMap = [
1990
+ [/^C(\d+)[']{0,2}$/, function(bits) {
1991
+ // this id can use another quote, so just add it
1992
+ return bits[0] + "'";
1993
+ }],
1994
+ [/^C(\d+)[']{3}$/, function(bits) {
1995
+ // here we switch from C''' to C'^4
1996
+ return bits[0].slice(0, -3) + "'^4";
1997
+ }],
1998
+ [/^C(\d+)['][\^](\d+)$/, function(bits) {
1999
+ return 'C' + String(bits[1]) + "'^" + String(Number(bits[2]) + 1);
2000
+ }]
2001
+ ];
2002
+
2003
+ // for loop for early return (instead of _.each)
2004
+ for (var i = 0; i < regexMap.length; i++) {
2005
+ var regex = regexMap[i][0];
2006
+ var func = regexMap[i][1];
2007
+ var results = regex.exec(id);
2008
+ if (results) {
2009
+ return func(results);
2010
+ }
2011
+ }
2012
+ throw new Error('could not modify the id ' + id);
2013
+ };
2014
+
2015
+ GitEngine.prototype.idSortFunc = function(cA, cB) {
2016
+ // commit IDs can come in many forms:
2017
+ // C4
2018
+ // C4' (from a rebase)
2019
+ // C4'' (from multiple rebases)
2020
+ // C4'^3 (from a BUNCH of rebases)
2021
+
2022
+ var scale = 1000;
2023
+
2024
+ var regexMap = [
2025
+ [/^C(\d+)$/, function(bits) {
2026
+ // return the 4 from C4
2027
+ return scale * bits[1];
2028
+ }],
2029
+ [/^C(\d+)([']+)$/, function(bits) {
2030
+ // return the 4 from C4, plus the length of the quotes
2031
+ return scale * bits[1] + bits[2].length;
2032
+ }],
2033
+ [/^C(\d+)['][\^](\d+)$/, function(bits) {
2034
+ return scale * bits[1] + Number(bits[2]);
2035
+ }]
2036
+ ];
2037
+
2038
+ var getNumToSort = function(id) {
2039
+ for (var i = 0; i < regexMap.length; i++) {
2040
+ var regex = regexMap[i][0];
2041
+ var func = regexMap[i][1];
2042
+ var results = regex.exec(id);
2043
+ if (results) {
2044
+ return func(results);
2045
+ }
2046
+ }
2047
+ throw new Error('Could not parse commit ID ' + id);
2048
+ };
2049
+
2050
+ // We usually want to sort by reverse chronological order, aka the
2051
+ // "latest" commits have the highest values. When we did this
2052
+ // with date sorting, that means the commit C1 at t=0 should have
2053
+ // a lower value than the commit C2 at t=1. We do this by doing
2054
+ // t0 - t1 and get a negative number. Same goes for ID sorting,
2055
+ // which means C1 - C2 = -1
2056
+ return getNumToSort(cA.get('id')) - getNumToSort(cB.get('id'));
2057
+ };
2058
+
2059
+ GitEngine.prototype.dateSortFunc = function(cA, cB) {
2060
+ // We used to use date sorting, but its hacky so lets switch to ID sorting
2061
+ // to eliminate non-determinism
2062
+ return GitEngine.prototype.idSortFunc(cA, cB);
2063
+ };
2064
+
2065
+ GitEngine.prototype.hgRebase = function(destination, base) {
2066
+ var deferred = Q.defer();
2067
+ var chain = this.rebase(destination, base, {
2068
+ dontResolvePromise: true,
2069
+ deferred: deferred
2070
+ });
2071
+
2072
+ // was upstream or something
2073
+ if (!chain) {
2074
+ return;
2075
+ }
2076
+
2077
+ // ok lets grab the merge base first
2078
+ var commonAncestor = this.getCommonAncestor(destination, base);
2079
+ var baseCommit = this.getCommitFromRef(base);
2080
+ // we need everything BELOW ourselves...
2081
+ var downstream = this.getDownstreamSet(base);
2082
+ // and we need to go upwards to the stop set
2083
+ var stopSet = Graph.getUpstreamSet(this, destination);
2084
+ var upstream = this.getUpstreamDiffSetFromSet(stopSet, base);
2085
+
2086
+ // and NOWWWwwww get all the descendants of this set
2087
+ var moreSets = [];
2088
+ Object.keys(upstream).forEach(function(id) {
2089
+ moreSets.push(this.getDownstreamSet(id));
2090
+ }, this);
2091
+
2092
+ var mainSet = {};
2093
+ mainSet[baseCommit.get('id')] = true;
2094
+ [upstream, downstream].concat(moreSets).forEach(function(set) {
2095
+ Object.keys(set).forEach(function(id) {
2096
+ mainSet[id] = true;
2097
+ });
2098
+ });
2099
+
2100
+ // we also need the branches POINTING to main set
2101
+ var branchMap = {};
2102
+ var upstreamSet = this.getUpstreamBranchSet();
2103
+ Object.keys(mainSet).forEach(function(commitID) {
2104
+ // now loop over that commits branches
2105
+ upstreamSet[commitID].forEach(function(branchJSON) {
2106
+ branchMap[branchJSON.id] = true;
2107
+ });
2108
+ });
2109
+
2110
+ var branchList = Object.keys(branchMap);
2111
+
2112
+ chain = chain.then(function() {
2113
+ // now we just moved a bunch of commits, but we haven't updated the
2114
+ // dangling guys. lets do that and then prune
2115
+ var anyChange = this.updateCommitParentsForHgRebase(mainSet);
2116
+ if (!anyChange) {
2117
+ return;
2118
+ }
2119
+ return this.animationFactory.playRefreshAnimationSlow(this.gitVisuals);
2120
+ }.bind(this));
2121
+
2122
+ chain = chain.then(function() {
2123
+ return this.updateAllBranchesForHgAndPlay(branchList);
2124
+ }.bind(this));
2125
+
2126
+ chain = chain.then(function() {
2127
+ // now that we have moved branches, lets prune
2128
+ return this.pruneTreeAndPlay();
2129
+ }.bind(this));
2130
+
2131
+ this.animationQueue.thenFinish(chain, deferred);
2132
+ };
2133
+
2134
+ GitEngine.prototype.rebase = function(targetSource, currentLocation, options) {
2135
+ // first some conditions
2136
+ if (this.isUpstreamOf(targetSource, currentLocation)) {
2137
+ this.command.setResult(intl.str('git-result-uptodate'));
2138
+
2139
+ // git for some reason always checks out the branch you are rebasing,
2140
+ // no matter the result of the rebase
2141
+ this.checkout(currentLocation);
2142
+
2143
+ // returning instead of throwing makes a tree refresh
2144
+ return;
2145
+ }
2146
+
2147
+ if (this.isUpstreamOf(currentLocation, targetSource)) {
2148
+ // just set the target of this current location to the source
2149
+ this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
2150
+ // we need the refresh tree animation to happen, so set the result directly
2151
+ // instead of throwing
2152
+ this.command.setResult(intl.str('git-result-fastforward'));
2153
+
2154
+ this.checkout(currentLocation);
2155
+ return;
2156
+ }
2157
+
2158
+ // now the part of actually rebasing.
2159
+ // We need to get the downstream set of targetSource first.
2160
+ // then we BFS from currentLocation, using the downstream set as our stopping point.
2161
+ // we need to BFS because we need to include all commits below
2162
+ // pop these commits on top of targetSource and modify their ids with quotes
2163
+ var stopSet = Graph.getUpstreamSet(this, targetSource);
2164
+ var toRebaseRough = this.getUpstreamDiffFromSet(stopSet, currentLocation);
2165
+ return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation, options);
2166
+ };
2167
+
2168
+ GitEngine.prototype.rebaseOnto = function(targetSource, oldSource, unit, options) {
2169
+ if (this.isUpstreamOf(unit, targetSource)) {
2170
+ this.setTargetLocation(unit, this.getCommitFromRef(targetSource));
2171
+ this.command.setResult(intl.str('git-result-fastforward'));
2172
+
2173
+ this.checkout(unit);
2174
+ return;
2175
+ }
2176
+
2177
+ var stopSet = Graph.getUpstreamSet(this, targetSource);
2178
+ var oldBranchSet = Graph.getUpstreamSet(this, oldSource);
2179
+ var toRebaseRough = this.getUpstreamDiffFromSet(oldBranchSet, unit);
2180
+ return this.rebaseFinish(toRebaseRough, stopSet, targetSource, unit, options);
2181
+ };
2182
+
2183
+ GitEngine.prototype.getUpstreamDiffSetFromSet = function(stopSet, location) {
2184
+ var set = {};
2185
+ this.getUpstreamDiffFromSet(stopSet, location).forEach(function (commit) {
2186
+ set[commit.get('id')] = true;
2187
+ });
2188
+ return set;
2189
+ };
2190
+
2191
+ GitEngine.prototype.getUpstreamDiffFromSet = function(stopSet, location) {
2192
+ var result = Graph.bfsFromLocationWithSet(this, location, stopSet);
2193
+ result.sort(this.dateSortFunc);
2194
+ return result;
2195
+ };
2196
+
2197
+ GitEngine.prototype.getInteractiveRebaseCommits = function(targetSource, currentLocation) {
2198
+ var stopSet = Graph.getUpstreamSet(this, targetSource);
2199
+ var toRebaseRough = [];
2200
+
2201
+ // standard BFS
2202
+ var pQueue = [this.getCommitFromRef(currentLocation)];
2203
+
2204
+ while (pQueue.length) {
2205
+ var popped = pQueue.pop();
2206
+
2207
+ if (stopSet[popped.get('id')]) {
2208
+ continue;
2209
+ }
2210
+
2211
+ toRebaseRough.push(popped);
2212
+ pQueue = pQueue.concat(popped.get('parents'));
2213
+ pQueue.sort(this.dateSortFunc);
2214
+ }
2215
+
2216
+ // throw out merge's real fast and see if we have anything to do
2217
+ var toRebase = [];
2218
+ toRebaseRough.forEach(function (commit) {
2219
+ if (commit.get('parents').length == 1) {
2220
+ toRebase.push(commit);
2221
+ }
2222
+ });
2223
+
2224
+ if (!toRebase.length) {
2225
+ throw new GitError({
2226
+ msg: intl.str('git-error-rebase-none')
2227
+ });
2228
+ }
2229
+
2230
+ return toRebase;
2231
+ };
2232
+
2233
+ GitEngine.prototype.rebaseInteractiveTest = function(targetSource, currentLocation, options) {
2234
+ options = options || {};
2235
+
2236
+ // Get the list of commits that would be displayed to the user
2237
+ var toRebase = this.getInteractiveRebaseCommits(targetSource, currentLocation);
2238
+
2239
+ var rebaseMap = {};
2240
+ toRebase.forEach(function (commit) {
2241
+ var id = commit.get('id');
2242
+ rebaseMap[id] = commit;
2243
+ });
2244
+
2245
+ var rebaseOrder;
2246
+ if (options['interactiveTest'].length === 0) {
2247
+ // If no commits were explicitly specified for the rebase, act like the user didn't change anything
2248
+ // in the rebase dialog and hit confirm
2249
+ rebaseOrder = toRebase;
2250
+ } else {
2251
+ // Get the list and order of commits specified
2252
+ var idsToRebase = options['interactiveTest'][0].split(',');
2253
+
2254
+ // Verify each chosen commit exists in the list of commits given to the user
2255
+ var extraCommits = [];
2256
+ rebaseOrder = [];
2257
+ idsToRebase.forEach(function (id) {
2258
+ if (id in rebaseMap) {
2259
+ rebaseOrder.push(rebaseMap[id]);
2260
+ } else {
2261
+ extraCommits.push(id);
2262
+ }
2263
+ });
2264
+
2265
+ if (extraCommits.length > 0) {
2266
+ throw new GitError({
2267
+ msg: intl.todo('Hey those commits don\'t exist in the set!')
2268
+ });
2269
+ }
2270
+ }
2271
+
2272
+ this.rebaseFinish(rebaseOrder, {}, targetSource, currentLocation);
2273
+ };
2274
+
2275
+ GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation, options) {
2276
+ options = options || {};
2277
+
2278
+ // there are a reduced set of checks now, so we can't exactly use parts of the rebase function
2279
+ // but it will look similar.
2280
+ var toRebase = this.getInteractiveRebaseCommits(targetSource, currentLocation);
2281
+
2282
+ // now do stuff :D since all our validation checks have passed, we are going to defer animation
2283
+ // and actually launch the dialog
2284
+ this.animationQueue.set('defer', true);
2285
+
2286
+ var deferred = Q.defer();
2287
+ deferred.promise
2288
+ .then(function(userSpecifiedRebase) {
2289
+ // first, they might have dropped everything (annoying)
2290
+ if (!userSpecifiedRebase.length) {
2291
+ throw new CommandResult({
2292
+ msg: intl.str('git-result-nothing')
2293
+ });
2294
+ }
2295
+
2296
+ // finish the rebase crap and animate!
2297
+ this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation);
2298
+ }.bind(this))
2299
+ .fail(function(err) {
2300
+ this.filterError(err);
2301
+ this.command.set('error', err);
2302
+ this.animationQueue.start();
2303
+ }.bind(this))
2304
+ .done();
2305
+
2306
+ // If we have a solution provided, set up the GUI to display it by default
2307
+ var initialCommitOrdering;
2308
+ if (options.initialCommitOrdering && options.initialCommitOrdering.length > 0) {
2309
+ var rebaseMap = {};
2310
+ toRebase.forEach(function (commit) {
2311
+ rebaseMap[commit.get('id')] = true;
2312
+ });
2313
+
2314
+ // Verify each chosen commit exists in the list of commits given to the user
2315
+ initialCommitOrdering = [];
2316
+ options.initialCommitOrdering[0].split(',').forEach(function (id) {
2317
+ if (!rebaseMap[id]) {
2318
+ throw new GitError({
2319
+ msg: intl.todo('Hey those commits don\'t exist in the set!')
2320
+ });
2321
+ }
2322
+ initialCommitOrdering.push(id);
2323
+ });
2324
+ }
2325
+
2326
+ var InteractiveRebaseView = require('../views/rebaseView').InteractiveRebaseView;
2327
+ // interactive rebase view will reject or resolve our promise
2328
+ new InteractiveRebaseView({
2329
+ deferred: deferred,
2330
+ toRebase: toRebase,
2331
+ initialCommitOrdering: initialCommitOrdering,
2332
+ aboveAll: options.aboveAll
2333
+ });
2334
+ };
2335
+
2336
+ GitEngine.prototype.filterRebaseCommits = function(
2337
+ toRebaseRough,
2338
+ stopSet,
2339
+ options
2340
+ ) {
2341
+ var changesAlreadyMade = {};
2342
+ Object.keys(stopSet).forEach(function(key) {
2343
+ changesAlreadyMade[this.scrapeBaseID(key)] = true;
2344
+ }, this);
2345
+ var uniqueIDs = {};
2346
+
2347
+ // resolve the commits we will rebase
2348
+ return toRebaseRough.filter(function(commit) {
2349
+ // no merge commits, unless we preserve
2350
+ if (commit.get('parents').length !== 1 && !options.preserveMerges) {
2351
+ return false;
2352
+ }
2353
+
2354
+ // we ALSO need to throw out commits that will do the same changes. like
2355
+ // if the upstream set has a commit C4 and we have C4', we don't rebase the C4' again.
2356
+ var baseID = this.scrapeBaseID(commit.get('id'));
2357
+ if (changesAlreadyMade[baseID]) {
2358
+ return false;
2359
+ }
2360
+
2361
+ // make unique
2362
+ if (uniqueIDs[commit.get('id')]) {
2363
+ return false;
2364
+ }
2365
+
2366
+ uniqueIDs[commit.get('id')] = true;
2367
+ return true;
2368
+ }, this);
2369
+ };
2370
+
2371
+ GitEngine.prototype.getRebasePreserveMergesParents = function(oldCommit) {
2372
+ var oldParents = oldCommit.get('parents');
2373
+ return oldParents.map(function(parent) {
2374
+ var oldID = parent.get('id');
2375
+ var newID = this.getMostRecentBumpedID(oldID);
2376
+ return this.refs[newID];
2377
+ }, this);
2378
+ };
2379
+
2380
+ GitEngine.prototype.rebaseFinish = function(
2381
+ toRebaseRough,
2382
+ stopSet,
2383
+ targetSource,
2384
+ currentLocation,
2385
+ options
2386
+ ) {
2387
+ options = options || {};
2388
+ // now we have the all the commits between currentLocation and the set of target to rebase.
2389
+ var destinationBranch = this.resolveID(targetSource);
2390
+ var deferred = options.deferred || Q.defer();
2391
+ var chain = options.chain || deferred.promise;
2392
+
2393
+ var toRebase = this.filterRebaseCommits(toRebaseRough, stopSet, options);
2394
+ if (!toRebase.length) {
2395
+ throw new GitError({
2396
+ msg: intl.str('git-error-rebase-none')
2397
+ });
2398
+ }
2399
+
2400
+ chain = this.animationFactory.highlightEachWithPromise(
2401
+ chain,
2402
+ toRebase,
2403
+ destinationBranch
2404
+ );
2405
+
2406
+ // now pop all of these commits onto targetLocation
2407
+ var base = this.getCommitFromRef(targetSource);
2408
+ var hasStartedChain = false;
2409
+ // each step makes a new commit
2410
+ var chainStep = function(oldCommit) {
2411
+ var newId = this.rebaseAltID(oldCommit.get('id'));
2412
+ var parents;
2413
+ if (!options.preserveMerges || !hasStartedChain) {
2414
+ // easy logic since we just have a straight line
2415
+ parents = [base];
2416
+ } else { // preserving merges
2417
+ // we always define the parent for the first commit to plop,
2418
+ // otherwise search for most recent parents
2419
+ parents = (hasStartedChain) ?
2420
+ this.getRebasePreserveMergesParents(oldCommit) :
2421
+ [base];
2422
+ }
2423
+
2424
+ var newCommit = this.makeCommit(parents, newId);
2425
+ base = newCommit;
2426
+ hasStartedChain = true;
2427
+
2428
+ return this.animationFactory.playCommitBirthPromiseAnimation(
2429
+ newCommit,
2430
+ this.gitVisuals
2431
+ );
2432
+ }.bind(this);
2433
+
2434
+ // set up the promise chain
2435
+ toRebase.forEach(function (commit) {
2436
+ chain = chain.then(function() {
2437
+ return chainStep(commit);
2438
+ });
2439
+ }, this);
2440
+
2441
+ chain = chain.then(function() {
2442
+ if (this.resolveID(currentLocation).get('type') == 'commit') {
2443
+ // we referenced a commit like git rebase C2 C1, so we have
2444
+ // to manually check out C1'
2445
+ this.checkout(base);
2446
+ } else {
2447
+ // now we just need to update the rebased branch is
2448
+ this.setTargetLocation(currentLocation, base);
2449
+ this.checkout(currentLocation);
2450
+ }
2451
+ return this.animationFactory.playRefreshAnimation(this.gitVisuals);
2452
+ }.bind(this));
2453
+
2454
+ if (!options.dontResolvePromise) {
2455
+ this.animationQueue.thenFinish(chain, deferred);
2456
+ }
2457
+ return chain;
2458
+ };
2459
+
2460
+ GitEngine.prototype.mergeCheck = function(targetSource, currentLocation) {
2461
+ var sameCommit = this.getCommitFromRef(targetSource) ===
2462
+ this.getCommitFromRef(currentLocation);
2463
+ return this.isUpstreamOf(targetSource, currentLocation) || sameCommit;
2464
+ };
2465
+
2466
+ GitEngine.prototype.merge = function(targetSource, options) {
2467
+ options = options || {};
2468
+ var currentLocation = 'HEAD';
2469
+
2470
+ // first some conditions
2471
+ if (this.mergeCheck(targetSource, currentLocation)) {
2472
+ throw new CommandResult({
2473
+ msg: intl.str('git-result-uptodate')
2474
+ });
2475
+ }
2476
+
2477
+ if (this.isUpstreamOf(currentLocation, targetSource) && !options.noFF) {
2478
+ // just set the target of this current location to the source
2479
+ this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
2480
+ // get fresh animation to happen
2481
+ this.command.setResult(intl.str('git-result-fastforward'));
2482
+ return;
2483
+ }
2484
+
2485
+ // now the part of making a merge commit
2486
+ var parent1 = this.getCommitFromRef(currentLocation);
2487
+ var parent2 = this.getCommitFromRef(targetSource);
2488
+
2489
+ // we need a fancy commit message
2490
+ var msg = intl.str(
2491
+ 'git-merge-msg',
2492
+ {
2493
+ target: this.resolveName(targetSource),
2494
+ current: this.resolveName(currentLocation)
2495
+ }
2496
+ );
2497
+ // since we specify parent 1 as the first parent, it is the "main" parent
2498
+ // and the node will be displayed below that branch / commit / whatever
2499
+ var mergeCommit = this.makeCommit(
2500
+ [parent1, parent2],
2501
+ null,
2502
+ {
2503
+ commitMessage: msg
2504
+ }
2505
+ );
2506
+
2507
+ this.setTargetLocation(currentLocation, mergeCommit);
2508
+ return mergeCommit;
2509
+ };
2510
+
2511
+ GitEngine.prototype.checkout = function(idOrTarget) {
2512
+ var target = this.resolveID(idOrTarget);
2513
+ if (target.get('id') === 'HEAD') {
2514
+ // git checkout HEAD is a
2515
+ // meaningless command but i used to do this back in the day
2516
+ return;
2517
+ }
2518
+
2519
+ var type = target.get('type');
2520
+ // check if this is an origin branch, and if so go to the commit referenced
2521
+ if (type === 'branch' && target.getIsRemote()) {
2522
+ target = this.getCommitFromRef(target.get('id'));
2523
+ }
2524
+
2525
+ if (type !== 'branch' && type !== 'tag' && type !== 'commit') {
2526
+ throw new GitError({
2527
+ msg: intl.str('git-error-options')
2528
+ });
2529
+ }
2530
+ if (type === 'tag') {
2531
+ target = target.get('target');
2532
+ }
2533
+
2534
+ this.HEAD.set('target', target);
2535
+ };
2536
+
2537
+ GitEngine.prototype.forceBranch = function(branchName, where) {
2538
+ branchName = this.crappyUnescape(branchName);
2539
+ // if branchname doesn't exist...
2540
+ if (!this.doesRefExist(branchName)) {
2541
+ this.branch(branchName, where);
2542
+ }
2543
+
2544
+ var branch = this.resolveID(branchName);
2545
+
2546
+ if (branch.get('type') !== 'branch') {
2547
+ throw new GitError({
2548
+ msg: intl.str('git-error-options')
2549
+ });
2550
+ }
2551
+ if (branch.getIsRemote()) {
2552
+ throw new GitError({
2553
+ msg: intl.str('git-error-remote-branch')
2554
+ });
2555
+ }
2556
+
2557
+ var whereCommit = this.getCommitFromRef(where);
2558
+
2559
+ this.setTargetLocation(branch, whereCommit);
2560
+ };
2561
+
2562
+ GitEngine.prototype.branch = function(name, ref) {
2563
+ var target = this.getCommitFromRef(ref);
2564
+ var newBranch = this.validateAndMakeBranch(name, target);
2565
+
2566
+ ref = this.resolveID(ref);
2567
+ if (this.isRemoteBranchRef(ref)) {
2568
+ this.setLocalToTrackRemote(newBranch, ref);
2569
+ }
2570
+ };
2571
+
2572
+ GitEngine.prototype.isRemoteBranchRef = function(ref) {
2573
+ var resolved = this.resolveID(ref);
2574
+ if (resolved.get('type') !== 'branch') {
2575
+ return false;
2576
+ }
2577
+ return resolved.getIsRemote();
2578
+ };
2579
+
2580
+ GitEngine.prototype.tag = function(name, ref) {
2581
+ var target = this.getCommitFromRef(ref);
2582
+ this.validateAndMakeTag(name, target);
2583
+ };
2584
+
2585
+ GitEngine.prototype.describe = function(ref) {
2586
+ var startCommit = this.getCommitFromRef(ref);
2587
+ // ok we need to BFS from start upwards until we hit a tag. but
2588
+ // first we need to get a reverse mapping from tag to commit
2589
+ var tagMap = {};
2590
+ this.tagCollection.toJSON().forEach(function (tag) {
2591
+ tagMap[tag.target.get('id')] = tag.id;
2592
+ });
2593
+
2594
+ var pQueue = [startCommit];
2595
+ var foundTag;
2596
+ var numAway = [];
2597
+ while (pQueue.length) {
2598
+ var popped = pQueue.pop();
2599
+ var thisID = popped.get('id');
2600
+ if (tagMap[thisID]) {
2601
+ foundTag = tagMap[thisID];
2602
+ break;
2603
+ }
2604
+ // ok keep going
2605
+ numAway.push(popped.get('id'));
2606
+
2607
+ var parents = popped.get('parents');
2608
+ if (parents && parents.length) {
2609
+ pQueue = pQueue.concat(parents);
2610
+ pQueue.sort(this.dateSortFunc);
2611
+ }
2612
+ }
2613
+
2614
+ if (!foundTag) {
2615
+ throw new GitError({
2616
+ msg: intl.todo('Fatal: no tags found upstream')
2617
+ });
2618
+ }
2619
+
2620
+ if (numAway.length === 0) {
2621
+ throw new CommandResult({
2622
+ msg: foundTag
2623
+ });
2624
+ }
2625
+
2626
+ // then join
2627
+ throw new CommandResult({
2628
+ msg: foundTag + '_' + numAway.length + '_g' + startCommit.get('id')
2629
+ });
2630
+ };
2631
+
2632
+ GitEngine.prototype.validateAndDeleteBranch = function(name) {
2633
+ // trying to delete, lets check our refs
2634
+ var target = this.resolveID(name);
2635
+
2636
+ if (target.get('type') !== 'branch' ||
2637
+ target.get('id') == 'main' ||
2638
+ this.HEAD.get('target') === target) {
2639
+ throw new GitError({
2640
+ msg: intl.str('git-error-branch')
2641
+ });
2642
+ }
2643
+
2644
+ // now we know it's a branch
2645
+ var branch = target;
2646
+ // if its remote
2647
+ if (target.getIsRemote()) {
2648
+ throw new GitError({
2649
+ msg: intl.str('git-error-remote-branch')
2650
+ });
2651
+ }
2652
+ this.deleteBranch(branch);
2653
+ };
2654
+
2655
+ GitEngine.prototype.deleteBranch = function(branch) {
2656
+ this.branchCollection.remove(branch);
2657
+ this.refs[branch.get('id')] = undefined;
2658
+ delete this.refs[branch.get('id')];
2659
+ // also in some cases external engines call our delete, so
2660
+ // verify integrity of HEAD here
2661
+ if (this.HEAD.get('target') === branch) {
2662
+ this.HEAD.set('target', this.refs['main']);
2663
+ }
2664
+
2665
+ if (branch.get('visBranch')) {
2666
+ branch.get('visBranch').remove();
2667
+ }
2668
+ };
2669
+
2670
+ GitEngine.prototype.crappyUnescape = function(str) {
2671
+ return str.replace(/&#x27;/g, "'").replace(/&#x2F;/g, "/");
2672
+ };
2673
+
2674
+ GitEngine.prototype.filterError = function(err) {
2675
+ if (!(err instanceof GitError ||
2676
+ err instanceof CommandResult)) {
2677
+ throw err;
2678
+ }
2679
+ };
2680
+
2681
+ // called on a origin repo from a local -- simply refresh immediately with
2682
+ // an animation
2683
+ GitEngine.prototype.externalRefresh = function() {
2684
+ this.animationQueue = new AnimationQueue({
2685
+ callback: function() {}
2686
+ });
2687
+ this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
2688
+ this.animationQueue.start();
2689
+ };
2690
+
2691
+ GitEngine.prototype.dispatch = function(command, deferred) {
2692
+ this.command = command;
2693
+ var vcs = command.get('vcs');
2694
+ var executeCommand = function() {
2695
+ this.dispatchProcess(command, deferred);
2696
+ }.bind(this);
2697
+ // handle mode change will either execute sync or
2698
+ // animate during tree pruning / etc
2699
+ this.handleModeChange(vcs, executeCommand);
2700
+ };
2701
+
2702
+ GitEngine.prototype.dispatchProcess = function(command, deferred) {
2703
+ // set up the animation queue
2704
+ var whenDone = function() {
2705
+ command.finishWith(deferred);
2706
+ }.bind(this);
2707
+ this.animationQueue = new AnimationQueue({
2708
+ callback: whenDone
2709
+ });
2710
+
2711
+ var vcs = command.get('vcs');
2712
+ var methodName = command.get('method').replace(/-/g, '');
2713
+
2714
+ try {
2715
+ Commands.commands.execute(vcs, methodName, this, this.command);
2716
+ } catch (err) {
2717
+ this.filterError(err);
2718
+ // short circuit animation by just setting error and returning
2719
+ command.set('error', err);
2720
+ deferred.resolve();
2721
+ return;
2722
+ }
2723
+
2724
+ var willStartAuto = this.animationQueue.get('defer') ||
2725
+ this.animationQueue.get('promiseBased');
2726
+
2727
+ // only add the refresh if we didn't do manual animations
2728
+ if (!this.animationQueue.get('animations').length && !willStartAuto) {
2729
+ this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
2730
+ }
2731
+
2732
+ // animation queue will call the callback when its done
2733
+ if (!willStartAuto) {
2734
+ this.animationQueue.start();
2735
+ }
2736
+ };
2737
+
2738
+ GitEngine.prototype.show = function(ref) {
2739
+ var commit = this.getCommitFromRef(ref);
2740
+
2741
+ throw new CommandResult({
2742
+ msg: commit.getShowEntry()
2743
+ });
2744
+ };
2745
+
2746
+ GitEngine.prototype.status = function() {
2747
+ // UGLY todo
2748
+ var lines = [];
2749
+ if (this.getDetachedHead()) {
2750
+ lines.push(intl.str('git-status-detached'));
2751
+ } else {
2752
+ var branchName = this.resolveNameNoPrefix('HEAD');
2753
+ lines.push(intl.str('git-status-onbranch', {branch: branchName}));
2754
+ }
2755
+ lines.push('Changes to be committed:');
2756
+ lines.push('');
2757
+ lines.push(TAB + 'modified: cal/OskiCostume.stl');
2758
+ lines.push('');
2759
+ lines.push(intl.str('git-status-readytocommit'));
2760
+
2761
+ var msg = '';
2762
+ lines.forEach(function (line) {
2763
+ msg += '# ' + line + '\n';
2764
+ });
2765
+
2766
+ throw new CommandResult({
2767
+ msg: msg
2768
+ });
2769
+ };
2770
+
2771
+ GitEngine.prototype.logWithout = function(ref, omitBranch) {
2772
+ // slice off the ^branch
2773
+ omitBranch = omitBranch.slice(1);
2774
+ this.log(ref, Graph.getUpstreamSet(this, omitBranch));
2775
+ };
2776
+
2777
+ GitEngine.prototype.revlist = function(refs) {
2778
+ var range = new RevisionRange(this, refs);
2779
+
2780
+ // now go through and collect ids
2781
+ var bigLogStr = range.formatRevisions(function(c) {
2782
+ return c.id + '\n';
2783
+ });
2784
+
2785
+ throw new CommandResult({
2786
+ msg: bigLogStr
2787
+ });
2788
+ };
2789
+
2790
+ GitEngine.prototype.log = function(refs) {
2791
+ var range = new RevisionRange(this, refs);
2792
+
2793
+ // now go through and collect logs
2794
+ var bigLogStr = range.formatRevisions(function(c) {
2795
+ return c.getLogEntry();
2796
+ });
2797
+
2798
+ throw new CommandResult({
2799
+ msg: bigLogStr
2800
+ });
2801
+ };
2802
+
2803
+ GitEngine.prototype.getCommonAncestor = function(ancestor, cousin, dontThrow) {
2804
+ if (this.isUpstreamOf(cousin, ancestor) && !dontThrow) {
2805
+ throw new Error('Don\'t use common ancestor if we are upstream!');
2806
+ }
2807
+
2808
+ var upstreamSet = Graph.getUpstreamSet(this, ancestor);
2809
+ // now BFS off of cousin until you find something
2810
+
2811
+ var queue = [this.getCommitFromRef(cousin)];
2812
+ while (queue.length) {
2813
+ var here = queue.pop();
2814
+ if (upstreamSet[here.get('id')]) {
2815
+ return here;
2816
+ }
2817
+ queue = queue.concat(here.get('parents'));
2818
+ }
2819
+ throw new Error('something has gone very wrong... two nodes aren\'t connected!');
2820
+ };
2821
+
2822
+ GitEngine.prototype.isUpstreamOf = function(child, ancestor) {
2823
+ child = this.getCommitFromRef(child);
2824
+
2825
+ // basically just do a completely BFS search on ancestor to the root, then
2826
+ // check for membership of child in that set of explored nodes
2827
+ var upstream = Graph.getUpstreamSet(this, ancestor);
2828
+ return upstream[child.get('id')] !== undefined;
2829
+ };
2830
+
2831
+ GitEngine.prototype.getDownstreamSet = function(ancestor) {
2832
+ var commit = this.getCommitFromRef(ancestor);
2833
+
2834
+ var ancestorID = commit.get('id');
2835
+ var queue = [commit];
2836
+
2837
+ var exploredSet = {};
2838
+ exploredSet[ancestorID] = true;
2839
+
2840
+ var addToExplored = function(child) {
2841
+ exploredSet[child.get('id')] = true;
2842
+ queue.push(child);
2843
+ };
2844
+
2845
+ while (queue.length) {
2846
+ var here = queue.pop();
2847
+ var children = here.get('children');
2848
+
2849
+ children.forEach(addToExplored);
2850
+ }
2851
+ return exploredSet;
2852
+ };
2853
+
2854
+ var Ref = Backbone.Model.extend({
2855
+ initialize: function() {
2856
+ if (!this.get('target')) {
2857
+ throw new Error('must be initialized with target');
2858
+ }
2859
+ if (!this.get('id')) {
2860
+ throw new Error('must be given an id');
2861
+ }
2862
+ this.set('type', 'general ref');
2863
+
2864
+ if (this.get('id') == 'HEAD') {
2865
+ this.set('lastLastTarget', null);
2866
+ this.set('lastTarget', this.get('target'));
2867
+ // have HEAD remember where it is for checkout -
2868
+ this.on('change:target', this.targetChanged, this);
2869
+ }
2870
+ },
2871
+
2872
+ getIsRemote: function() {
2873
+ return false;
2874
+ },
2875
+
2876
+ getName: function() {
2877
+ return this.get('id');
2878
+ },
2879
+
2880
+ targetChanged: function(model, targetValue, ev) {
2881
+ // push our little 3 stack back. we need to do this because
2882
+ // backbone doesn't give you what the value WAS, only what it was changed
2883
+ // TO
2884
+ this.set('lastLastTarget', this.get('lastTarget'));
2885
+ this.set('lastTarget', targetValue);
2886
+ },
2887
+
2888
+ toString: function() {
2889
+ return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target'));
2890
+ }
2891
+ });
2892
+
2893
+ var Branch = Ref.extend({
2894
+ defaults: {
2895
+ visBranch: null,
2896
+ remoteTrackingBranchID: null,
2897
+ remote: false
2898
+ },
2899
+
2900
+ initialize: function() {
2901
+ Ref.prototype.initialize.call(this);
2902
+ this.set('type', 'branch');
2903
+ },
2904
+
2905
+ /**
2906
+ * Here is the deal -- there are essentially three types of branches
2907
+ * we deal with:
2908
+ * 1) Normal local branches (that may track a remote branch)
2909
+ * 2) Local remote branches (o/main) that track an origin branch
2910
+ * 3) Origin branches (main) that exist in origin
2911
+ *
2912
+ * With that in mind, we change our branch model to support the following
2913
+ */
2914
+ setRemoteTrackingBranchID: function(id) {
2915
+ this.set('remoteTrackingBranchID', id);
2916
+ },
2917
+
2918
+ getRemoteTrackingBranchID: function() {
2919
+ return this.get('remoteTrackingBranchID');
2920
+ },
2921
+
2922
+ getPrefixedID: function() {
2923
+ if (this.getIsRemote()) {
2924
+ throw new Error('im already remote');
2925
+ }
2926
+ return ORIGIN_PREFIX + this.get('id');
2927
+ },
2928
+
2929
+ getBaseID: function() {
2930
+ if (!this.getIsRemote()) {
2931
+ throw new Error('im not remote so can\'t get base');
2932
+ }
2933
+ return this.get('id').replace(ORIGIN_PREFIX, '');
2934
+ },
2935
+
2936
+ getIsRemote: function() {
2937
+ if (typeof this.get('id') !== 'string') {
2938
+ debugger;
2939
+ }
2940
+ return this.get('id').slice(0, 2) === ORIGIN_PREFIX;
2941
+ }
2942
+ });
2943
+
2944
+ var Commit = Backbone.Model.extend({
2945
+ defaults: {
2946
+ type: 'commit',
2947
+ children: null,
2948
+ parents: null,
2949
+ author: 'Peter Cottle',
2950
+ createTime: null,
2951
+ commitMessage: null,
2952
+ visNode: null,
2953
+ gitVisuals: null
2954
+ },
2955
+
2956
+ constants: {
2957
+ circularFields: ['gitVisuals', 'visNode', 'children']
2958
+ },
2959
+
2960
+ getLogEntry: function() {
2961
+ return [
2962
+ 'Author: ' + this.get('author'),
2963
+ 'Date: ' + this.get('createTime'),
2964
+ '',
2965
+ this.get('commitMessage'),
2966
+ '',
2967
+ 'Commit: ' + this.get('id')
2968
+ ].join('<br/>') + '\n';
2969
+ },
2970
+
2971
+ getShowEntry: function() {
2972
+ // same deal as above, show log entry and some fake changes
2973
+ return [
2974
+ this.getLogEntry().replace('\n', ''),
2975
+ 'diff --git a/bigGameResults.html b/bigGameResults.html',
2976
+ '--- bigGameResults.html',
2977
+ '+++ bigGameResults.html',
2978
+ '@@ 13,27 @@ Winner, Score',
2979
+ '- Stanfurd, 14-7',
2980
+ '+ Cal, 21-14'
2981
+ ].join('<br/>') + '\n';
2982
+ },
2983
+
2984
+ validateAtInit: function() {
2985
+ if (!this.get('id')) {
2986
+ throw new Error('Need ID!!');
2987
+ }
2988
+
2989
+ if (!this.get('createTime')) {
2990
+ this.set('createTime', new Date().toString());
2991
+ }
2992
+ if (!this.get('commitMessage')) {
2993
+ this.set('commitMessage', intl.str('git-dummy-msg'));
2994
+ }
2995
+
2996
+ this.set('children', []);
2997
+
2998
+ // root commits have no parents
2999
+ if (!this.get('rootCommit')) {
3000
+ if (!this.get('parents') || !this.get('parents').length) {
3001
+ throw new Error('needs parents');
3002
+ }
3003
+ }
3004
+ },
3005
+
3006
+ addNodeToVisuals: function() {
3007
+ var visNode = this.get('gitVisuals').addNode(this.get('id'), this);
3008
+ this.set('visNode', visNode);
3009
+ },
3010
+
3011
+ addEdgeToVisuals: function(parent) {
3012
+ this.get('gitVisuals').addEdge(this.get('id'), parent.get('id'));
3013
+ },
3014
+
3015
+ getParent: function(parentNum) {
3016
+ if (this && this.attributes && this.attributes.parents) {
3017
+ return this.attributes.parents[parentNum];
3018
+ } else {
3019
+ return null;
3020
+ }
3021
+ },
3022
+
3023
+ removeFromParents: function() {
3024
+ this.get('parents').forEach(function (parent) {
3025
+ parent.removeChild(this);
3026
+ }, this);
3027
+ },
3028
+
3029
+ checkForUpdatedParent: function(engine) {
3030
+ var parents = this.get('parents');
3031
+ if (parents.length > 1) {
3032
+ return;
3033
+ }
3034
+ var parent = parents[0];
3035
+ var parentID = parent.get('id');
3036
+ var newestID = engine.getMostRecentBumpedID(parentID);
3037
+
3038
+ if (parentID === newestID) {
3039
+ // BOOM done, its already updated
3040
+ return;
3041
+ }
3042
+
3043
+ // crap we have to switch
3044
+ var newParent = engine.refs[newestID];
3045
+
3046
+ this.removeFromParents();
3047
+ this.set('parents', [newParent]);
3048
+ newParent.get('children').push(this);
3049
+
3050
+ // when we run in test mode, our visnode and
3051
+ // visuals will be undefined so we need to check for their existence
3052
+ var visNode = this.get('visNode');
3053
+ if (visNode) {
3054
+ visNode.removeAllEdges();
3055
+ }
3056
+
3057
+ var gitVisuals = this.get('gitVisuals');
3058
+ if (gitVisuals) {
3059
+ gitVisuals.addEdge(this.get('id'), newestID);
3060
+ }
3061
+
3062
+ return true;
3063
+ },
3064
+
3065
+ removeChild: function(childToRemove) {
3066
+ var newChildren = [];
3067
+ this.get('children').forEach(function (child) {
3068
+ if (child !== childToRemove) {
3069
+ newChildren.push(child);
3070
+ }
3071
+ });
3072
+ this.set('children', newChildren);
3073
+ },
3074
+
3075
+ isMainParent: function(parent) {
3076
+ var index = this.get('parents').indexOf(parent);
3077
+ return index === 0;
3078
+ },
3079
+
3080
+ initialize: function(options) {
3081
+ this.validateAtInit();
3082
+ this.addNodeToVisuals();
3083
+
3084
+ (this.get('parents') || []).forEach(function (parent) {
3085
+ parent.get('children').push(this);
3086
+ this.addEdgeToVisuals(parent);
3087
+ }, this);
3088
+ }
3089
+ });
3090
+
3091
+ var Tag = Ref.extend({
3092
+ defaults: {
3093
+ visTag: null
3094
+ },
3095
+
3096
+ initialize: function() {
3097
+ Ref.prototype.initialize.call(this);
3098
+ this.set('type', 'tag');
3099
+ }
3100
+ });
3101
+
3102
+ function RevisionRange(engine, specifiers) {
3103
+ this.engine = engine;
3104
+ this.tipsToInclude = [];
3105
+ this.tipsToExclude = [];
3106
+ this.includedRefs = {};
3107
+ this.excludedRefs = {};
3108
+ this.revisions = [];
3109
+
3110
+ this.processSpecifiers(specifiers);
3111
+ }
3112
+
3113
+ var rangeRegex = /^(.*)\.\.(.*)$/;
3114
+
3115
+ RevisionRange.prototype.processAsRange = function(specifier) {
3116
+ var match = specifier.match(rangeRegex);
3117
+ if(!match) {
3118
+ return false;
3119
+ }
3120
+ this.tipsToExclude.push(match[1]);
3121
+ this.tipsToInclude.push(match[2]);
3122
+ return true;
3123
+ };
3124
+
3125
+ RevisionRange.prototype.processAsExclusion = function(specifier) {
3126
+ if(!specifier.startsWith('^')) {
3127
+ return false;
3128
+ }
3129
+ this.tipsToExclude.push(specifier.slice(1));
3130
+ return true;
3131
+ };
3132
+
3133
+ RevisionRange.prototype.processAsInclusion = function(specifier) {
3134
+ this.tipsToInclude.push(specifier);
3135
+ return true;
3136
+ };
3137
+
3138
+ RevisionRange.prototype.processSpecifiers = function(specifiers) {
3139
+ var self = this;
3140
+ var processors = [
3141
+ this.processAsRange,
3142
+ this.processAsExclusion
3143
+ ];
3144
+
3145
+ specifiers.forEach(function(specifier) {
3146
+ if(!processors.some(function(processor) { return processor.bind(self)(specifier); })) {
3147
+ self.processAsInclusion(specifier);
3148
+ }
3149
+ });
3150
+
3151
+ this.tipsToExclude.forEach(function(exclusion) {
3152
+ self.addExcluded(Graph.getUpstreamSet(self.engine, exclusion));
3153
+ });
3154
+
3155
+ this.tipsToInclude.forEach(function(inclusion) {
3156
+ self.addIncluded(Graph.getUpstreamSet(self.engine, inclusion));
3157
+ });
3158
+
3159
+ var includedKeys = Array.from(Object.keys(self.includedRefs));
3160
+
3161
+ self.revisions = includedKeys.map(function(revision) {
3162
+ return self.engine.resolveStringRef(revision);
3163
+ });
3164
+ self.revisions.sort(self.engine.dateSortFunc);
3165
+ self.revisions.reverse();
3166
+ };
3167
+
3168
+ RevisionRange.prototype.isExcluded = function(revision) {
3169
+ return this.excludedRefs.hasOwnProperty(revision);
3170
+ };
3171
+
3172
+ RevisionRange.prototype.addExcluded = function(setToExclude) {
3173
+ var self = this;
3174
+ Object.keys(setToExclude).forEach(function(toExclude) {
3175
+ if(!self.isExcluded(toExclude)) {
3176
+ self.excludedRefs[toExclude] = true;
3177
+ }
3178
+ });
3179
+ };
3180
+
3181
+ RevisionRange.prototype.addIncluded = function(setToInclude) {
3182
+ var self = this;
3183
+ Object.keys(setToInclude).forEach(function(toInclude) {
3184
+ if(!self.isExcluded(toInclude)) {
3185
+ self.includedRefs[toInclude] = true;
3186
+ }
3187
+ });
3188
+ };
3189
+
3190
+ RevisionRange.prototype.formatRevisions = function(revisionFormatter) {
3191
+ var output = "";
3192
+ this.revisions.forEach(function(c) {
3193
+ output += revisionFormatter(c);
3194
+ });
3195
+ return output;
3196
+ };
3197
+
3198
+ exports.GitEngine = GitEngine;
3199
+ exports.Commit = Commit;
3200
+ exports.Branch = Branch;
3201
+ exports.Tag = Tag;
3202
+ exports.Ref = Ref;
src/js/graph/index.js ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function invariant(truthy, reason) {
2
+ if (!truthy) {
3
+ throw new Error(reason);
4
+ }
5
+ }
6
+
7
+ var Graph = {
8
+
9
+ getOrMakeRecursive: function(
10
+ tree,
11
+ createdSoFar,
12
+ objID,
13
+ gitVisuals
14
+ ) {
15
+ // circular dependency, should move these base models OUT of
16
+ // the git class to resolve this
17
+ var Git = require('../git');
18
+ var Commit = Git.Commit;
19
+ var Ref = Git.Ref;
20
+ var Branch = Git.Branch;
21
+ var Tag = Git.Tag;
22
+ if (createdSoFar[objID]) {
23
+ // base case
24
+ return createdSoFar[objID];
25
+ }
26
+
27
+ var getType = function(tree, id) {
28
+ if (tree.commits[id]) {
29
+ return 'commit';
30
+ } else if (tree.branches[id]) {
31
+ return 'branch';
32
+ } else if (id == 'HEAD') {
33
+ return 'HEAD';
34
+ } else if (tree.tags[id]) {
35
+ return 'tag';
36
+ }
37
+ throw new Error("bad type for " + id);
38
+ };
39
+
40
+ // figure out what type
41
+ var type = getType(tree, objID);
42
+
43
+ if (type == 'HEAD') {
44
+ var headJSON = tree.HEAD;
45
+ var HEAD = new Ref(Object.assign(
46
+ tree.HEAD,
47
+ {
48
+ target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target)
49
+ }
50
+ ));
51
+ createdSoFar[objID] = HEAD;
52
+ return HEAD;
53
+ }
54
+
55
+ if (type == 'branch') {
56
+ var branchJSON = tree.branches[objID];
57
+
58
+ var branch = new Branch(Object.assign(
59
+ tree.branches[objID],
60
+ {
61
+ target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target)
62
+ }
63
+ ));
64
+ createdSoFar[objID] = branch;
65
+ return branch;
66
+ }
67
+
68
+ if (type == 'tag') {
69
+ var tagJSON = tree.tags[objID];
70
+
71
+ var tag = new Tag(Object.assign(
72
+ tree.tags[objID],
73
+ {
74
+ target: this.getOrMakeRecursive(tree, createdSoFar, tagJSON.target)
75
+ }
76
+ ));
77
+ createdSoFar[objID] = tag;
78
+ return tag;
79
+ }
80
+
81
+ if (type == 'commit') {
82
+ // for commits, we need to grab all the parents
83
+ var commitJSON = tree.commits[objID];
84
+
85
+ var parentObjs = [];
86
+ commitJSON.parents.forEach(function(parentID) {
87
+ parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID));
88
+ }, this);
89
+
90
+ var commit = new Commit(Object.assign(
91
+ commitJSON,
92
+ {
93
+ parents: parentObjs,
94
+ gitVisuals: this.gitVisuals
95
+ }
96
+ ));
97
+ createdSoFar[objID] = commit;
98
+ return commit;
99
+ }
100
+
101
+ throw new Error('ruh rho!! unsupported type for ' + objID);
102
+ },
103
+
104
+ descendSortDepth: function(objects) {
105
+ return objects.sort(function(oA, oB) {
106
+ return oB.depth - oA.depth;
107
+ });
108
+ },
109
+
110
+ bfsFromLocationWithSet: function(engine, location, set) {
111
+ var result = [];
112
+ var pQueue = [engine.getCommitFromRef(location)];
113
+
114
+ while (pQueue.length) {
115
+ var popped = pQueue.pop();
116
+ if (set[popped.get('id')]) {
117
+ continue;
118
+ }
119
+
120
+ result.push(popped);
121
+ // keep searching
122
+ pQueue = pQueue.concat(popped.get('parents'));
123
+ }
124
+ return result;
125
+ },
126
+
127
+ getUpstreamSet: function(engine, ancestor) {
128
+ var commit = engine.getCommitFromRef(ancestor);
129
+ var ancestorID = commit.get('id');
130
+ var queue = [commit];
131
+
132
+ var exploredSet = {};
133
+ exploredSet[ancestorID] = true;
134
+
135
+ var addToExplored = function(rent) {
136
+ exploredSet[rent.get('id')] = true;
137
+ queue.push(rent);
138
+ };
139
+
140
+ while (queue.length) {
141
+ var here = queue.pop();
142
+ var rents = here.get('parents');
143
+
144
+ (rents || []).forEach(addToExplored);
145
+ }
146
+ return exploredSet;
147
+ },
148
+
149
+ getUniqueObjects: function(objects) {
150
+ var unique = {};
151
+ var result = [];
152
+ objects.forEach(function(object) {
153
+ if (unique[object.id]) {
154
+ return;
155
+ }
156
+ unique[object.id] = true;
157
+ result.push(object);
158
+ });
159
+ return result;
160
+ },
161
+
162
+ getDefaultTree: function() {
163
+ return JSON.parse(unescape("%7B%22branches%22%3A%7B%22main%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22main%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22main%22%2C%22type%22%3A%22general%20ref%22%7D%7D"));
164
+ }
165
+ };
166
+
167
+ module.exports = Graph;
src/js/graph/treeCompare.js ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var _ = require('underscore');
2
+
3
+ // static class...
4
+ var TreeCompare = {};
5
+
6
+ TreeCompare.dispatchFromLevel = function(levelBlob, treeToCompare) {
7
+ var goalTreeString = levelBlob.goalTreeString;
8
+ if (typeof treeToCompare !== 'string') {
9
+ console.warn('NEED to pass in string!! gah');
10
+ }
11
+ return TreeCompare.dispatch(levelBlob, goalTreeString, treeToCompare);
12
+ };
13
+
14
+ TreeCompare.onlyMainCompared = function(levelBlob) {
15
+ var getAroundLintTrue = true;
16
+ switch (getAroundLintTrue) {
17
+ case !!levelBlob.compareOnlyMain:
18
+ case !!levelBlob.compareOnlyMainHashAgnostic:
19
+ case !!levelBlob.compareOnlyMainHashAgnosticWithAsserts:
20
+ return true;
21
+ default:
22
+ return false;
23
+ }
24
+ };
25
+
26
+ TreeCompare.dispatch = function(levelBlob, goalTreeString, treeToCompare) {
27
+ var goalTree = this.convertTreeSafe(goalTreeString);
28
+ treeToCompare = this.convertTreeSafe(treeToCompare);
29
+ if (typeof goalTree.originTree !== typeof treeToCompare.originTree) {
30
+ // origin status does not match
31
+ return false;
32
+ }
33
+ var shallowResult = this.dispatchShallow(
34
+ levelBlob, goalTree, treeToCompare
35
+ );
36
+ if (!shallowResult || !goalTree.originTree) {
37
+ // we only have one level (or failed on shallow), punt
38
+ return shallowResult;
39
+ }
40
+
41
+ var originBlob = (levelBlob.originCompare) ?
42
+ levelBlob.originCompare : levelBlob;
43
+ // compare origin trees
44
+ return shallowResult && this.dispatchShallow(
45
+ originBlob, goalTree.originTree, treeToCompare.originTree
46
+ );
47
+ };
48
+
49
+ TreeCompare.dispatchShallow = function(levelBlob, goalTreeString, treeToCompare) {
50
+ var getAroundLintTrue = true;
51
+ // i actually prefer this to else if
52
+ switch (getAroundLintTrue) {
53
+ case !!levelBlob.compareOnlyMain:
54
+ return TreeCompare.compareBranchWithinTrees(
55
+ treeToCompare, goalTreeString, 'main'
56
+ );
57
+ case !!levelBlob.compareAllBranchesAndEnforceBranchCleanup:
58
+ return TreeCompare.compareAllBranchesAndEnforceBranchCleanup(
59
+ treeToCompare, goalTreeString
60
+ );
61
+ case !!levelBlob.compareOnlyBranches:
62
+ return TreeCompare.compareAllBranchesWithinTrees(
63
+ treeToCompare, goalTreeString
64
+ );
65
+ case !!levelBlob.compareAllBranchesHashAgnostic:
66
+ return TreeCompare.compareAllBranchesWithinTreesHashAgnostic(
67
+ treeToCompare, goalTreeString
68
+ );
69
+ case !!levelBlob.compareOnlyMainHashAgnostic:
70
+ return TreeCompare.compareBranchesWithinTreesHashAgnostic(
71
+ treeToCompare, goalTreeString, ['main']
72
+ );
73
+ case !!levelBlob.compareOnlyMainHashAgnosticWithAsserts:
74
+ return TreeCompare.compareBranchesWithinTreesHashAgnostic(
75
+ treeToCompare, goalTreeString, ['main']
76
+ ) && TreeCompare.evalAsserts(treeToCompare, levelBlob.goalAsserts);
77
+ default:
78
+ return TreeCompare.compareAllBranchesWithinTreesAndHEAD(
79
+ treeToCompare, goalTreeString
80
+ );
81
+ }
82
+ };
83
+
84
+ // would love to have copy properties here.. :(
85
+ TreeCompare.compareAllBranchesWithinTreesAndHEAD = function(treeToCompare, goalTree) {
86
+ treeToCompare = this.convertTreeSafe(treeToCompare);
87
+ goalTree = this.convertTreeSafe(goalTree);
88
+
89
+ // also compare tags!! for just one level
90
+ return treeToCompare.HEAD.target === goalTree.HEAD.target &&
91
+ this.compareAllBranchesWithinTrees(treeToCompare, goalTree) &&
92
+ this.compareAllTagsWithinTrees(treeToCompare, goalTree);
93
+ };
94
+
95
+ TreeCompare.compareAllBranchesAndEnforceBranchCleanup = function(treeToCompare, goalTree) {
96
+ treeToCompare = this.convertTreeSafe(treeToCompare);
97
+ goalTree = this.convertTreeSafe(goalTree);
98
+
99
+ // Unlike compareAllBranchesWithinTrees, here we consider both the branches
100
+ // in the goalTree and the branches in the treeToCompare. This means that
101
+ // we enforce that you clean up any branches that you have locally that
102
+ // the goal does not have. this is helpful when we want to verify that you
103
+ // have deleted branch, for instance.
104
+ var allBranches = Object.assign(
105
+ {},
106
+ treeToCompare.branches,
107
+ goalTree.branches
108
+ );
109
+ return Object.keys(allBranches).every(function(branch) {
110
+ return this.compareBranchWithinTrees(treeToCompare, goalTree, branch);
111
+ }.bind(this));
112
+ };
113
+
114
+
115
+ TreeCompare.compareAllBranchesWithinTrees = function(treeToCompare, goalTree) {
116
+ treeToCompare = this.convertTreeSafe(treeToCompare);
117
+ goalTree = this.convertTreeSafe(goalTree);
118
+
119
+ /**
120
+ * Disclaimer / reminder!! We only care about branches in the goal tree;
121
+ * if you have extra branches in your source tree thats ok. but that means
122
+ * the arguments here are important -- always call this function with
123
+ * goalTree being the latter argument, since we will discard extra branches
124
+ * from treeToCompare (the first argument).
125
+ */
126
+ return Object.keys(goalTree.branches).every(function(branch) {
127
+ return this.compareBranchWithinTrees(treeToCompare, goalTree, branch);
128
+ }.bind(this));
129
+ };
130
+
131
+ TreeCompare.compareAllTagsWithinTrees = function(treeToCompare, goalTree) {
132
+ treeToCompare = this.convertTreeSafe(treeToCompare);
133
+ goalTree = this.convertTreeSafe(goalTree);
134
+ this.reduceTreeFields([treeToCompare, goalTree]);
135
+
136
+ return _.isEqual(treeToCompare.tags, goalTree.tags);
137
+ };
138
+
139
+ TreeCompare.compareBranchesWithinTrees = function(treeToCompare, goalTree, branches) {
140
+ var result = true;
141
+ branches.forEach(function(branchName) {
142
+ result = result && this.compareBranchWithinTrees(treeToCompare, goalTree, branchName);
143
+ }, this);
144
+
145
+ return result;
146
+ };
147
+
148
+ TreeCompare.compareBranchWithinTrees = function(treeToCompare, goalTree, branchName) {
149
+ treeToCompare = this.convertTreeSafe(treeToCompare);
150
+ goalTree = this.convertTreeSafe(goalTree);
151
+ this.reduceTreeFields([treeToCompare, goalTree]);
152
+
153
+ var recurseCompare = this.getRecurseCompare(treeToCompare, goalTree);
154
+ var branchA = treeToCompare.branches[branchName];
155
+ var branchB = goalTree.branches[branchName];
156
+
157
+ return _.isEqual(branchA, branchB) &&
158
+ recurseCompare(treeToCompare.commits[branchA.target], goalTree.commits[branchB.target]);
159
+ };
160
+
161
+ TreeCompare.compareAllBranchesWithinTreesHashAgnostic = function(treeToCompare, goalTree) {
162
+ treeToCompare = this.convertTreeSafe(treeToCompare);
163
+ goalTree = this.convertTreeSafe(goalTree);
164
+ this.reduceTreeFields([treeToCompare, goalTree]);
165
+
166
+ var allBranches = Object.assign(
167
+ {},
168
+ treeToCompare.branches,
169
+ goalTree.branches
170
+ );
171
+ var branchNames = Object.keys(allBranches || {});
172
+
173
+ return this.compareBranchesWithinTreesHashAgnostic(treeToCompare, goalTree, branchNames);
174
+ };
175
+
176
+ TreeCompare.compareBranchesWithinTreesHashAgnostic = function(treeToCompare, goalTree, branches) {
177
+ // we can't DRY unfortunately here because we need a special _.isEqual function
178
+ // for both the recursive compare and the branch compare
179
+ treeToCompare = this.convertTreeSafe(treeToCompare);
180
+ goalTree = this.convertTreeSafe(goalTree);
181
+ this.reduceTreeFields([treeToCompare, goalTree]);
182
+
183
+ // get a function to compare branch objects without hashes
184
+ var compareBranchObjs = function(branchA, branchB) {
185
+ if (!branchA || !branchB) {
186
+ return false;
187
+ }
188
+
189
+ // don't mess up the rest of comparison
190
+ branchA = Object.assign({}, branchA);
191
+ branchB = Object.assign({}, branchB);
192
+ branchA.target = this.getBaseRef(branchA.target);
193
+ branchB.target = this.getBaseRef(branchB.target);
194
+
195
+ return _.isEqual(branchA, branchB);
196
+ }.bind(this);
197
+ // and a function to compare recursively without worrying about hashes
198
+ var recurseCompare = this.getRecurseCompareHashAgnostic(treeToCompare, goalTree);
199
+
200
+ var result = true;
201
+ branches.forEach(function(branchName) {
202
+ var branchA = treeToCompare.branches[branchName];
203
+ var branchB = goalTree.branches[branchName];
204
+
205
+ result = result && compareBranchObjs(branchA, branchB) &&
206
+ recurseCompare(treeToCompare.commits[branchA.target], goalTree.commits[branchB.target]);
207
+ }, this);
208
+ return result;
209
+ };
210
+
211
+ TreeCompare.evalAsserts = function(tree, assertsPerBranch) {
212
+ var result = true;
213
+ Object.keys(assertsPerBranch).forEach(function(branchName) {
214
+ var asserts = assertsPerBranch[branchName];
215
+ result = result && this.evalAssertsOnBranch(tree, branchName, asserts);
216
+ }, this);
217
+ return result;
218
+ };
219
+
220
+ TreeCompare.evalAssertsOnBranch = function(tree, branchName, asserts) {
221
+ tree = this.convertTreeSafe(tree);
222
+
223
+ // here is the outline:
224
+ // * make a data object
225
+ // * go to the branch given by the key
226
+ // * traverse upwards, storing the amount of hashes on each in the data object
227
+ // * then come back and perform functions on data
228
+
229
+ if (!tree.branches[branchName]) {
230
+ return false;
231
+ }
232
+
233
+ var branch = tree.branches[branchName];
234
+ var queue = [branch.target];
235
+ var data = {};
236
+ while (queue.length) {
237
+ var commitRef = queue.pop();
238
+ data[this.getBaseRef(commitRef)] = this.getNumHashes(commitRef);
239
+ queue = queue.concat(tree.commits[commitRef].parents);
240
+ }
241
+
242
+ var result = true;
243
+ asserts.forEach(function(assert) {
244
+ try {
245
+ result = result && assert(data);
246
+ } catch (err) {
247
+ console.warn('error during assert', err);
248
+ console.log(err);
249
+ result = false;
250
+ }
251
+ });
252
+
253
+ return result;
254
+ };
255
+
256
+ TreeCompare.getNumHashes = function(ref) {
257
+ var regexMap = [
258
+ [/^C(\d+)([']{0,3})$/, function(bits) {
259
+ if (!bits[2]) {
260
+ return 0;
261
+ }
262
+ return bits[2].length;
263
+ }],
264
+ [/^C(\d+)['][\^](\d+)$/, function(bits) {
265
+ return Number(bits[2]);
266
+ }]
267
+ ];
268
+
269
+ for (var i = 0; i < regexMap.length; i++) {
270
+ var regex = regexMap[i][0];
271
+ var func = regexMap[i][1];
272
+ var results = regex.exec(ref);
273
+ if (results) {
274
+ return func(results);
275
+ }
276
+ }
277
+ throw new Error('couldn\'t parse ref ' + ref);
278
+ };
279
+
280
+ TreeCompare.getBaseRef = function(ref) {
281
+ var idRegex = /^C(\d+)/;
282
+ var bits = idRegex.exec(ref);
283
+ if (!bits) { throw new Error('no regex matchy for ' + ref); }
284
+ // no matter what hash this is (aka C1', C1'', C1'^3, etc) we
285
+ // return C1
286
+ return 'C' + bits[1];
287
+ };
288
+
289
+ TreeCompare.getRecurseCompareHashAgnostic = function(treeToCompare, goalTree) {
290
+ // here we pass in a special comparison function to pass into the base
291
+ // recursive compare.
292
+
293
+ // some buildup functions
294
+ var getStrippedCommitCopy = function(commit) {
295
+ if (!commit) { return {}; }
296
+ return Object.assign(
297
+ {},
298
+ commit,
299
+ {
300
+ id: this.getBaseRef(commit.id),
301
+ parents: null
302
+ }
303
+ );
304
+ }.bind(this);
305
+
306
+ var isEqual = function(commitA, commitB) {
307
+ return _.isEqual(
308
+ getStrippedCommitCopy(commitA),
309
+ getStrippedCommitCopy(commitB)
310
+ );
311
+ };
312
+ return this.getRecurseCompare(treeToCompare, goalTree, {isEqual: isEqual});
313
+ };
314
+
315
+ TreeCompare.getRecurseCompare = function(treeToCompare, goalTree, options) {
316
+ options = options || {};
317
+
318
+ // we need a recursive comparison function to bubble up the branch
319
+ var recurseCompare = function(commitA, commitB) {
320
+ // this is the short-circuit base case
321
+ var result = options.isEqual ?
322
+ options.isEqual(commitA, commitB) : _.isEqual(commitA, commitB);
323
+ if (!result) {
324
+ return false;
325
+ }
326
+
327
+ // we loop through each parent ID. we sort the parent ID's beforehand
328
+ // so the index lookup is valid. for merge commits this will duplicate some of the
329
+ // checking (because we aren't doing graph search) but it's not a huge deal
330
+ var maxNumParents = Math.max(commitA.parents.length, commitB.parents.length);
331
+ for (var index = 0; index < maxNumParents; index++) {
332
+ var pAid = commitA.parents[index];
333
+ var pBid = commitB.parents[index];
334
+
335
+ // if treeToCompare or goalTree doesn't have this parent,
336
+ // then we get an undefined child which is fine when we pass into _.isEqual
337
+ var childA = treeToCompare.commits[pAid];
338
+ var childB = goalTree.commits[pBid];
339
+
340
+ result = result && recurseCompare(childA, childB);
341
+ }
342
+ // if each of our children recursively are equal, we are good
343
+ return result;
344
+ };
345
+ return recurseCompare;
346
+ };
347
+
348
+ TreeCompare.lowercaseTree = function(tree) {
349
+ if (tree.HEAD) {
350
+ tree.HEAD.target = tree.HEAD.target.toLocaleLowerCase();
351
+ }
352
+
353
+ var branches = tree.branches || {};
354
+ tree.branches = {};
355
+ Object.keys(branches).forEach(function(name) {
356
+ var obj = branches[name];
357
+ obj.id = obj.id.toLocaleLowerCase();
358
+ tree.branches[name.toLocaleLowerCase()] = obj;
359
+ });
360
+ return tree;
361
+ };
362
+
363
+ TreeCompare.convertTreeSafe = function(tree) {
364
+ if (typeof tree !== 'string') {
365
+ return tree;
366
+ }
367
+ tree = JSON.parse(unescape(tree));
368
+ // ok we are almost done -- but we need to case insensitive
369
+ // certain fields. so go ahead and do that.
370
+ // handle HEAD target first
371
+ this.lowercaseTree(tree);
372
+ if (tree.originTree) {
373
+ tree.originTree = this.lowercaseTree(tree.originTree);
374
+ }
375
+ return tree;
376
+ };
377
+
378
+ TreeCompare.reduceTreeFields = function(trees) {
379
+ var commitSaveFields = [
380
+ 'parents',
381
+ 'id',
382
+ 'rootCommit'
383
+ ];
384
+ var branchSaveFields = [
385
+ 'target',
386
+ 'id',
387
+ 'remoteTrackingBranchID'
388
+ ];
389
+ var tagSaveFields = [
390
+ 'target',
391
+ 'id'
392
+ ];
393
+
394
+ var commitSortFields = ['children', 'parents'];
395
+ // for backwards compatibility, fill in some fields if missing
396
+ var defaults = {
397
+ remoteTrackingBranchID: null
398
+ };
399
+ // also fill tree-level defaults
400
+ var treeDefaults = {
401
+ tags: {}
402
+ };
403
+
404
+ trees.forEach(function(tree) {
405
+ Object.keys(treeDefaults).forEach(function(key) {
406
+ var val = treeDefaults[key];
407
+ if (tree[key] === undefined) {
408
+ tree[key] = val;
409
+ }
410
+ });
411
+ });
412
+
413
+ // this function saves only the specified fields of a tree
414
+ var saveOnly = function(tree, treeKey, saveFields, sortFields) {
415
+ var objects = tree[treeKey];
416
+ Object.keys(objects).forEach(function(objKey) {
417
+ var obj = objects[objKey];
418
+ // our blank slate to copy over
419
+ var blank = {};
420
+ saveFields.forEach(function(field) {
421
+ if (obj[field] !== undefined) {
422
+ blank[field] = obj[field];
423
+ } else if (defaults[field] !== undefined) {
424
+ blank[field] = defaults[field];
425
+ }
426
+ });
427
+
428
+ Object.values(sortFields || {}).forEach(function(field) {
429
+ // also sort some fields
430
+ if (obj[field]) {
431
+ obj[field].sort();
432
+ blank[field] = obj[field];
433
+ }
434
+ });
435
+ tree[treeKey][objKey] = blank;
436
+ });
437
+ };
438
+
439
+ trees.forEach(function(tree) {
440
+ saveOnly(tree, 'commits', commitSaveFields, commitSortFields);
441
+ saveOnly(tree, 'branches', branchSaveFields);
442
+ saveOnly(tree, 'tags', tagSaveFields);
443
+
444
+ tree.HEAD = {
445
+ target: tree.HEAD.target,
446
+ id: tree.HEAD.id
447
+ };
448
+ if (tree.originTree) {
449
+ this.reduceTreeFields([tree.originTree]);
450
+ }
451
+ }, this);
452
+ };
453
+
454
+ TreeCompare.compareTrees = function(treeToCompare, goalTree) {
455
+ treeToCompare = this.convertTreeSafe(treeToCompare);
456
+ goalTree = this.convertTreeSafe(goalTree);
457
+
458
+ // now we need to strip out the fields we don't care about, aka things
459
+ // like createTime, message, author
460
+ this.reduceTreeFields([treeToCompare, goalTree]);
461
+
462
+ return _.isEqual(treeToCompare, goalTree);
463
+ };
464
+
465
+ module.exports = TreeCompare;
src/js/intl/checkStrings.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var { join } = require('path');
2
+ var { readFileSync } = require('fs');
3
+
4
+ var util = require('../util');
5
+ var { strings } = require('../intl/strings');
6
+
7
+ var easyRegex = /intl\.str\(\s*'([a-zA-Z\-]+)'/g;
8
+
9
+ var allKetSet = new Set(Object.keys(strings));
10
+ allKetSet.delete('error-untranslated'); // used in ./index.js
11
+
12
+ var goodKeySet = new Set();
13
+ var validateKey = function(key) {
14
+ if (!strings[key]) {
15
+ console.log('NO KEY for: "', key, '"');
16
+ } else {
17
+ goodKeySet.add(key);
18
+ allKetSet.delete(key);
19
+ }
20
+ };
21
+
22
+ if (!util.isBrowser()) {
23
+ util.readDirDeep(join(__dirname, '../../')).forEach(function(path) {
24
+ var content = readFileSync(path);
25
+ var match;
26
+ while (match = easyRegex.exec(content)) {
27
+ validateKey(match[1]);
28
+ }
29
+ });
30
+ console.log(goodKeySet.size, ' good keys found!');
31
+ console.log(allKetSet.size, ' keys did not use!');
32
+ console.log(allKetSet);
33
+ }
src/js/intl/index.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var LocaleStore = require('../stores/LocaleStore');
2
+
3
+ var _ = require('underscore');
4
+ var strings = require('../intl/strings').strings;
5
+
6
+ var getDefaultLocale = LocaleStore.getDefaultLocale;
7
+
8
+ var fallbackMap = {
9
+ 'zh_TW': 'zh_CN',
10
+ 'es_AR': 'es_ES',
11
+ 'es_MX': 'es_ES'
12
+ };
13
+
14
+ // lets change underscores template settings so it interpolates
15
+ // things like "{branchName} does not exist".
16
+ var templateSettings = Object.assign({}, _.templateSettings);
17
+ templateSettings.interpolate = /\{(.+?)\}/g;
18
+ var template = exports.template = function(str, params) {
19
+ return _.template(str, templateSettings)(params);
20
+ };
21
+
22
+ var str = exports.str = function(key, params) {
23
+ params = params || {};
24
+ // this function takes a key like "error-branch-delete"
25
+ // and parameters like {branchName: 'bugFix', num: 3}.
26
+ //
27
+ // it sticks those into a translation string like:
28
+ // 'en': 'You can not delete the branch {branchName} because' +
29
+ // 'you are currently on that branch! This is error number + {num}'
30
+ //
31
+ // to produce:
32
+ //
33
+ // 'You can not delete the branch bugFix because you are currently on that branch!
34
+ // This is error number 3'
35
+
36
+ var locale = LocaleStore.getLocale();
37
+ if (!strings[key]) {
38
+ console.warn('NO INTL support for key ' + key);
39
+ return 'NO INTL support for key ' + key + '. this is probably a dev error';
40
+ }
41
+
42
+ if (!strings[key][locale]) {
43
+ // try falling back to another locale if in the map
44
+ locale = fallbackMap[locale] || getDefaultLocale();
45
+ }
46
+
47
+ if (!strings[key][locale]) {
48
+ if (key !== 'error-untranslated') {
49
+ return str('error-untranslated');
50
+ }
51
+ return 'No translation for the key "' + key + '"';
52
+ }
53
+
54
+ return template(
55
+ strings[key][locale],
56
+ params
57
+ );
58
+ };
59
+
60
+ var getIntlKey = exports.getIntlKey = function(obj, key, overrideLocale) {
61
+ if (!obj || !obj[key]) {
62
+ throw new Error('that key ' + key + ' doesn\'t exist in this blob ' + obj);
63
+ }
64
+ if (!obj[key][getDefaultLocale()]) {
65
+ console.warn(
66
+ 'WARNING!! This blob does not have intl support:',
67
+ obj,
68
+ 'for this key',
69
+ key
70
+ );
71
+ }
72
+
73
+ var locale = overrideLocale || LocaleStore.getLocale();
74
+ return obj[key][locale];
75
+ };
76
+
77
+ exports.todo = function(str) {
78
+ return str;
79
+ };
80
+
81
+ exports.getDialog = function(obj) {
82
+ return getIntlKey(obj, 'dialog') || obj.dialog[getDefaultLocale()];
83
+ };
84
+
85
+ exports.getHint = function(level) {
86
+ if (!getIntlKey(level, 'hint')) {
87
+ return getIntlKey(level, 'hint', getDefaultLocale()) + ' -- ' + str('error-untranslated');
88
+ }
89
+ return getIntlKey(level, 'hint');
90
+ };
91
+
92
+ exports.getName = function(level) {
93
+ if (!getIntlKey(level, 'name')) {
94
+ return getIntlKey(level, 'name', getDefaultLocale()) + ' -- ' + str('error-untranslated');
95
+ }
96
+ return getIntlKey(level, 'name');
97
+ };
98
+
99
+ exports.getStartDialog = function(level) {
100
+ var startDialog = getIntlKey(level, 'startDialog');
101
+ if (startDialog) { return startDialog; }
102
+
103
+ // this level translation isn't supported yet, so lets add
104
+ // an alert to the front and give the english version.
105
+ var errorAlert = {
106
+ type: 'ModalAlert',
107
+ options: {
108
+ markdown: str('error-untranslated')
109
+ }
110
+ };
111
+ var startCopy = Object.assign(
112
+ {},
113
+ level.startDialog[getDefaultLocale()] || level.startDialog
114
+ );
115
+ startCopy.childViews.unshift(errorAlert);
116
+
117
+ return startCopy;
118
+ };
src/js/intl/strings.js ADDED
The diff for this file is too large to render. See raw diff
 
src/js/level/builder.js ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Backbone = require('backbone');
2
+ var Q = require('q');
3
+
4
+ var util = require('../util');
5
+ var Main = require('../app');
6
+ var intl = require('../intl');
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
+ var LocaleStore = require('../stores/LocaleStore');
13
+ var LevelStore = require('../stores/LevelStore');
14
+
15
+ var Command = require('../models/commandModel').Command;
16
+ var GitShim = require('../git/gitShim').GitShim;
17
+
18
+ var MultiView = require('../views/multiView').MultiView;
19
+
20
+ var CanvasTerminalHolder = require('../views').CanvasTerminalHolder;
21
+ var ConfirmCancelTerminal = require('../views').ConfirmCancelTerminal;
22
+ var NextLevelConfirm = require('../views').NextLevelConfirm;
23
+
24
+ var MarkdownPresenter = require('../views/builderViews').MarkdownPresenter;
25
+ var MultiViewBuilder = require('../views/builderViews').MultiViewBuilder;
26
+ var MarkdownGrabber = require('../views/builderViews').MarkdownGrabber;
27
+
28
+ var regexMap = {
29
+ 'define goal': /^define goal$/,
30
+ 'define name': /^define name$/,
31
+ 'help builder': /^help builder$/,
32
+ 'define start': /^define start$/,
33
+ 'edit dialog': /^edit dialog$/,
34
+ 'show start': /^show start$/,
35
+ 'hide start': /^hide start$/,
36
+ 'define hint': /^define hint$/,
37
+ 'finish': /^finish$/
38
+ };
39
+
40
+ var parse = util.genParseCommand(regexMap, 'processLevelBuilderCommand');
41
+
42
+ var LevelBuilder = Level.extend({
43
+ initialize: function(options) {
44
+ options = options || {};
45
+ options.level = {};
46
+ this.options = options;
47
+
48
+ var locale = LocaleStore.getLocale();
49
+ options.level.startDialog = {};
50
+ options.level.startDialog[locale] = {
51
+ childViews: intl.getDialog(require('../dialogs/levelBuilder'))
52
+ };
53
+
54
+ // if we are editing a level our behavior is a bit different
55
+ var editLevelJSON;
56
+ if (options.editLevel) {
57
+ editLevelJSON = LevelStore.getLevel(options.editLevel);
58
+ options.level = editLevelJSON;
59
+ }
60
+
61
+ LevelBuilder.__super__.initialize.apply(this, [options]);
62
+ if (!options.editLevel) {
63
+ this.startDialogObj = undefined;
64
+ this.definedGoal = false;
65
+ } else {
66
+ this.startDialogObj = editLevelJSON.startDialog[locale];
67
+ this.definedGoal = true;
68
+ }
69
+
70
+ // we won't be using this stuff, and it is deleted to ensure we overwrite all functions that
71
+ // include that functionality
72
+ delete this.treeCompare;
73
+ delete this.solved;
74
+ },
75
+
76
+ initName: function() {
77
+ },
78
+
79
+ initGoalData: function() {
80
+ // add some default behavior in the beginning if we are not editing
81
+ if (!this.options.editLevel) {
82
+ this.level.goalTreeString = '{"branches":{"main":{"target":"C1","id":"main"},"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"}}';
83
+ this.level.solutionCommand = 'git checkout -b makeLevel; git commit';
84
+ }
85
+ LevelBuilder.__super__.initGoalData.apply(this, arguments);
86
+ },
87
+
88
+ /**
89
+ * need custom handlers since we have two visualizations >___<
90
+ */
91
+ minimizeGoal: function (position, size) {
92
+ this.doBothVis('hide');
93
+ this.goalWindowPos = position;
94
+ this.goalWindowSize = size;
95
+ if ($('#goalPlaceholder').is(':visible')) {
96
+ $('#goalPlaceholder').hide();
97
+ this.mainVis.myResize();
98
+ }
99
+ },
100
+
101
+ doBothVis: function(method) {
102
+ if (this.startVis) {
103
+ this.startVis[method].call(this.startVis);
104
+ }
105
+ if (this.goalVis) {
106
+ this.goalVis[method].call(this.goalVis);
107
+ }
108
+ },
109
+
110
+ resizeGoal: function () {
111
+ this.doBothVis('myResize');
112
+ },
113
+
114
+ initStartVisualization: function() {
115
+ this.startCanvasHolder = new CanvasTerminalHolder({
116
+ parent: this,
117
+ additionalClass: 'startTree',
118
+ text: intl.str('hide-start')
119
+ });
120
+
121
+ this.startVis = new Visualization({
122
+ el: this.startCanvasHolder.getCanvasLocation(),
123
+ containerElement: this.startCanvasHolder.getCanvasLocation(),
124
+ treeString: this.level.startTree,
125
+ noKeyboardInput: true,
126
+ smallCanvas: true,
127
+ noClick: true
128
+ });
129
+ return this.startCanvasHolder;
130
+ },
131
+
132
+ startOffCommand: function() {
133
+ Main.getEventBaton().trigger(
134
+ 'commandSubmitted',
135
+ 'echo :D'
136
+ );
137
+ },
138
+
139
+ objectiveDialog: function(command, deferred) {
140
+ var args = [
141
+ command,
142
+ deferred,
143
+ (this.startDialogObj === undefined) ?
144
+ null :
145
+ {
146
+ startDialog: {
147
+ 'en_US': this.startDialogObj
148
+ }
149
+ }
150
+ ];
151
+ LevelBuilder.__super__.objectiveDialog.apply(this, args);
152
+ },
153
+
154
+ initParseWaterfall: function(options) {
155
+ LevelBuilder.__super__.initParseWaterfall.apply(this, [options]);
156
+
157
+ this.parseWaterfall.addFirst(
158
+ 'parseWaterfall',
159
+ parse
160
+ );
161
+ this.parseWaterfall.addFirst(
162
+ 'instantWaterfall',
163
+ this.getInstantCommands()
164
+ );
165
+ },
166
+
167
+ buildLevel: function(command, deferred) {
168
+ this.exitLevel();
169
+
170
+ setTimeout(function() {
171
+ Main.getSandbox().buildLevel(command, deferred);
172
+ }, this.getAnimationTime() * 1.5);
173
+ },
174
+
175
+ getInstantCommands: function() {
176
+ return [
177
+ [/^help$|^\?$/, function() {
178
+ throw new Errors.CommandResult({
179
+ msg: intl.str('help-vague-builder')
180
+ });
181
+ }]
182
+ ];
183
+ },
184
+
185
+ takeControl: function() {
186
+ Main.getEventBaton().stealBaton('processLevelBuilderCommand', this.processLevelBuilderCommand, this);
187
+
188
+ LevelBuilder.__super__.takeControl.apply(this);
189
+ },
190
+
191
+ releaseControl: function() {
192
+ Main.getEventBaton().releaseBaton('processLevelBuilderCommand', this.processLevelBuilderCommand, this);
193
+
194
+ LevelBuilder.__super__.releaseControl.apply(this);
195
+ },
196
+
197
+ showGoal: function() {
198
+ this.hideStart();
199
+ LevelBuilder.__super__.showGoal.apply(this, arguments);
200
+ },
201
+
202
+ showStart: function(command, deferred) {
203
+ this.hideGoal();
204
+ this.showSideVis(command, deferred, this.startCanvasHolder, this.initStartVisualization);
205
+ },
206
+
207
+ resetSolution: function() {
208
+ this.gitCommandsIssued = [];
209
+ this.level.solutionCommand = undefined;
210
+ },
211
+
212
+ hideStart: function(command, deferred) {
213
+ this.hideSideVis(command, deferred, this.startCanvasHolder);
214
+ },
215
+
216
+ defineStart: function(command, deferred) {
217
+ this.hideStart();
218
+
219
+ command.addWarning(intl.str('define-start-warning'));
220
+ this.resetSolution();
221
+
222
+ this.level.startTree = this.mainVis.gitEngine.printTree();
223
+ this.mainVis.resetFromThisTreeNow(this.level.startTree);
224
+
225
+ this.showStart(command, deferred);
226
+ },
227
+
228
+ defineGoal: function(command, deferred) {
229
+ this.hideGoal();
230
+
231
+ if (!this.gitCommandsIssued.length) {
232
+ command.set('error', new Errors.GitError({
233
+ msg: intl.str('solution-empty')
234
+ }));
235
+ deferred.resolve();
236
+ return;
237
+ }
238
+
239
+ this.definedGoal = true;
240
+ this.level.solutionCommand = this.gitCommandsIssued.join(';');
241
+ this.level.goalTreeString = this.mainVis.gitEngine.printTree();
242
+ this.initGoalVisualization();
243
+
244
+ this.showGoal(command, deferred);
245
+ },
246
+
247
+ defineName: function(command, deferred) {
248
+ this.level.name = {
249
+ 'en_US': prompt(intl.str('prompt-name'))
250
+ };
251
+
252
+ if (command) { command.finishWith(deferred); }
253
+ },
254
+
255
+ defineHint: function(command, deferred) {
256
+ this.level.hint = {
257
+ 'en_US': prompt(intl.str('prompt-hint'))
258
+ };
259
+ if (command) { command.finishWith(deferred); }
260
+ },
261
+
262
+ editDialog: function(command, deferred) {
263
+ var whenDoneEditing = Q.defer();
264
+ this.currentBuilder = new MultiViewBuilder({
265
+ multiViewJSON: this.startDialogObj,
266
+ deferred: whenDoneEditing
267
+ });
268
+ whenDoneEditing.promise
269
+ .then(function(levelObj) {
270
+ this.startDialogObj = levelObj;
271
+ }.bind(this))
272
+ .fail(function() {
273
+ // nothing to do, they don't want to edit it apparently
274
+ })
275
+ .done(function() {
276
+ if (command) {
277
+ command.finishWith(deferred);
278
+ } else {
279
+ deferred.resolve();
280
+ }
281
+ });
282
+ },
283
+
284
+ finish: function(command, deferred) {
285
+ if (!this.options.editLevel && (!this.gitCommandsIssued.length || !this.definedGoal)) {
286
+ command.set('error', new Errors.GitError({
287
+ msg: intl.str('solution-empty')
288
+ }));
289
+ deferred.resolve();
290
+ return;
291
+ }
292
+
293
+ while (!this.level.name) {
294
+ this.defineName();
295
+ }
296
+
297
+ var mainDeferred = Q.defer();
298
+ var chain = mainDeferred.promise;
299
+
300
+ if (this.level.hint === undefined) {
301
+ var askForHintDeferred = Q.defer();
302
+ chain = chain.then(function() {
303
+ return askForHintDeferred.promise;
304
+ });
305
+
306
+ // ask for a hint if there is none
307
+ var askForHintView = new ConfirmCancelTerminal({
308
+ markdowns: [
309
+ intl.str('want-hint')
310
+ ]
311
+ });
312
+ askForHintView.getPromise()
313
+ .then(this.defineHint.bind(this))
314
+ .fail(function() {
315
+ this.level.hint = {'en_US': ''};
316
+ }.bind(this))
317
+ .done(function() {
318
+ askForHintDeferred.resolve();
319
+ });
320
+ }
321
+
322
+ if (this.startDialogObj === undefined) {
323
+ var askForStartDeferred = Q.defer();
324
+ chain = chain.then(function() {
325
+ return askForStartDeferred.promise;
326
+ });
327
+
328
+ var askForStartView = new ConfirmCancelTerminal({
329
+ markdowns: [
330
+ intl.str('want-start-dialog')
331
+ ]
332
+ });
333
+ askForStartView.getPromise()
334
+ .then(function() {
335
+ // oh boy this is complex
336
+ var whenEditedDialog = Q.defer();
337
+ // the undefined here is the command that doesn't need resolving just yet...
338
+ this.editDialog(undefined, whenEditedDialog);
339
+ return whenEditedDialog.promise;
340
+ }.bind(this))
341
+ .fail(function() {
342
+ // if they don't want to edit the start dialog, do nothing
343
+ })
344
+ .done(function() {
345
+ askForStartDeferred.resolve();
346
+ });
347
+ }
348
+
349
+ chain = chain.done(function() {
350
+ // ok great! lets just give them the goods
351
+ new MarkdownPresenter({
352
+ fillerText: JSON.stringify(this.getExportObj(), null, 2),
353
+ previewText: intl.str('share-json')
354
+ });
355
+ command.finishWith(deferred);
356
+ }.bind(this));
357
+
358
+ mainDeferred.resolve();
359
+ },
360
+
361
+ getExportObj: function() {
362
+ var compiledLevel = Object.assign(
363
+ {},
364
+ this.level
365
+ );
366
+ // the start dialog now is just our help intro thing
367
+ delete compiledLevel.startDialog;
368
+ if (this.startDialogObj) {
369
+ compiledLevel.startDialog = {'en_US': this.startDialogObj};
370
+ }
371
+ return compiledLevel;
372
+ },
373
+
374
+ processLevelBuilderCommand: function(command, deferred) {
375
+ var methodMap = {
376
+ 'define goal': this.defineGoal,
377
+ 'define start': this.defineStart,
378
+ 'show start': this.showStart,
379
+ 'hide start': this.hideStart,
380
+ 'finish': this.finish,
381
+ 'define hint': this.defineHint,
382
+ 'define name': this.defineName,
383
+ 'edit dialog': this.editDialog,
384
+ 'help builder': LevelBuilder.__super__.startDialog
385
+ };
386
+ if (!methodMap[command.get('method')]) {
387
+ throw new Error('woah we don\'t support that method yet');
388
+ }
389
+
390
+ methodMap[command.get('method')].apply(this, arguments);
391
+ },
392
+
393
+ afterCommandDefer: function(defer, command) {
394
+ // we don't need to compare against the goal anymore
395
+ defer.resolve();
396
+ },
397
+
398
+ die: function() {
399
+ this.hideStart();
400
+ LevelBuilder.__super__.die.apply(this, arguments);
401
+
402
+ delete this.startVis;
403
+ delete this.startCanvasHolder;
404
+ }
405
+ });
406
+
407
+ exports.LevelBuilder = LevelBuilder;
408
+ exports.regexMap = regexMap;
src/js/level/disabledMap.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var intl = require('../intl');
2
+
3
+ var Commands = require('../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 commands
19
+ // :D
20
+ var instants = [];
21
+ var onMatch = function() {
22
+ throw new GitError({
23
+ msg: intl.str('command-disabled')
24
+ });
25
+ };
26
+
27
+ Object.keys(this.disabledMap).forEach(function(disabledCommand) {
28
+ // XXX get hold of vcs from disabledMap
29
+ var vcs = 'git';
30
+ disabledCommand = disabledCommand.slice(vcs.length + 1);
31
+ var gitRegex = Commands.commands.getRegexMap()[vcs][disabledCommand];
32
+ if (!gitRegex) {
33
+ throw new Error('wuttttt this disbaled command' + disabledCommand +
34
+ ' has no regex matching');
35
+ }
36
+ instants.push([gitRegex, onMatch]);
37
+ }.bind(this));
38
+ return instants;
39
+ };
40
+
41
+ exports.DisabledMap = DisabledMap;
42
+
src/js/level/index.js ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Q = require('q');
2
+ var React = require('react');
3
+ var ReactDOM = require('react-dom');
4
+
5
+ var util = require('../util');
6
+ var Main = require('../app');
7
+ var intl = require('../intl');
8
+ var log = require('../log');
9
+
10
+ var Errors = require('../util/errors');
11
+ var Sandbox = require('../sandbox/').Sandbox;
12
+ var GlobalStateActions = require('../actions/GlobalStateActions');
13
+ var GlobalStateStore = require('../stores/GlobalStateStore');
14
+ var LevelActions = require('../actions/LevelActions');
15
+ var LevelStore = require('../stores/LevelStore');
16
+ var Visualization = require('../visuals/visualization').Visualization;
17
+ var DisabledMap = require('../level/disabledMap').DisabledMap;
18
+ var GitShim = require('../git/gitShim').GitShim;
19
+ var Commands = require('../commands');
20
+
21
+ var MultiView = require('../views/multiView').MultiView;
22
+ var CanvasTerminalHolder = require('../views').CanvasTerminalHolder;
23
+ var ConfirmCancelTerminal = require('../views').ConfirmCancelTerminal;
24
+ var NextLevelConfirm = require('../views').NextLevelConfirm;
25
+ var LevelToolbarView = require('../react_views/LevelToolbarView.jsx');
26
+
27
+ var TreeCompare = require('../graph/treeCompare');
28
+
29
+ var regexMap = {
30
+ 'help level': /^help level$/,
31
+ 'start dialog': /^start dialog$/,
32
+ 'show goal': /^(show goal|goal|help goal)$/,
33
+ 'hide goal': /^hide goal$/,
34
+ 'show solution': /^show solution($|\s)/,
35
+ 'objective': /^(objective|assignment)$/
36
+ };
37
+
38
+ var parse = util.genParseCommand(regexMap, 'processLevelCommand');
39
+
40
+ var Level = Sandbox.extend({
41
+ initialize: function(options) {
42
+ options = options || {};
43
+ options.level = options.level || {};
44
+
45
+ this.level = options.level;
46
+
47
+ this.gitCommandsIssued = [];
48
+ this.solved = false;
49
+ this.wasResetAfterSolved = false;
50
+
51
+ this.initGoalData(options);
52
+ this.initName(options);
53
+ this.on('minimizeCanvas', this.minimizeGoal);
54
+ this.on('resizeCanvas', this.resizeGoal);
55
+ this.isGoalExpanded = false;
56
+
57
+ Level.__super__.initialize.apply(this, [options]);
58
+ this.startOffCommand();
59
+
60
+ this.handleOpen(options.deferred);
61
+ },
62
+
63
+ getIsGoalExpanded: function() {
64
+ return this.isGoalExpanded;
65
+ },
66
+
67
+ handleOpen: function(deferred) {
68
+ deferred = deferred || Q.defer();
69
+
70
+ // if there is a multiview in the beginning, open that
71
+ // and let it resolve our deferred
72
+ if (GlobalStateStore.getShouldDisableLevelInstructions()) {
73
+ setTimeout(function() {
74
+ deferred.resolve();
75
+ }, 100);
76
+ return;
77
+ }
78
+
79
+ if (this.level.startDialog && !this.testOption('noIntroDialog')) {
80
+ new MultiView(Object.assign(
81
+ {},
82
+ intl.getStartDialog(this.level),
83
+ { deferred: deferred }
84
+ ));
85
+ return;
86
+ }
87
+
88
+ // otherwise, resolve after a 700 second delay to allow
89
+ // for us to animate easily
90
+ setTimeout(function() {
91
+ deferred.resolve();
92
+ }, this.getAnimationTime() * 1.2);
93
+ },
94
+
95
+ objectiveDialog: function(command, deferred, levelObj) {
96
+ levelObj = (levelObj === undefined) ? this.level : levelObj;
97
+
98
+ if (!levelObj || !levelObj.startDialog) {
99
+ command.set('error', new Errors.GitError({
100
+ msg: intl.str('no-start-dialog')
101
+ }));
102
+ deferred.resolve();
103
+ return;
104
+ }
105
+
106
+ var dialog = $.extend({}, intl.getStartDialog(levelObj));
107
+ // grab the last slide only
108
+ dialog.childViews = dialog.childViews.slice(-1);
109
+ new MultiView(Object.assign(
110
+ dialog,
111
+ { deferred: deferred }
112
+ ));
113
+
114
+ // when its closed we are done
115
+ deferred.promise.then(function() {
116
+ command.set('status', 'finished');
117
+ });
118
+ },
119
+
120
+ startDialog: function(command, deferred) {
121
+ if (!this.level.startDialog) {
122
+ command.set('error', new Errors.GitError({
123
+ msg: intl.str('no-start-dialog')
124
+ }));
125
+ deferred.resolve();
126
+ return;
127
+ }
128
+
129
+ this.handleOpen(deferred);
130
+ deferred.promise.then(function() {
131
+ command.set('status', 'finished');
132
+ });
133
+ },
134
+
135
+ getEnglishName: function() {
136
+ return this.level.name.en_US;
137
+ },
138
+
139
+ initName: function() {
140
+ var name = intl.getName(this.level);
141
+ this.levelToolbar = React.createElement(
142
+ LevelToolbarView,
143
+ {
144
+ name: name,
145
+ onGoalClick: this.toggleGoal.bind(this),
146
+ onObjectiveClick: this.toggleObjective.bind(this),
147
+ parent: this
148
+ }
149
+ );
150
+ ReactDOM.render(
151
+ this.levelToolbar,
152
+ document.getElementById('levelToolbarMount')
153
+ );
154
+ },
155
+
156
+ initGoalData: function(options) {
157
+ if (!this.level.goalTreeString || !this.level.solutionCommand) {
158
+ throw new Error('need goal tree and solution');
159
+ }
160
+ },
161
+
162
+ takeControl: function() {
163
+ Main.getEventBaton().stealBaton('processLevelCommand', this.processLevelCommand, this);
164
+
165
+ Level.__super__.takeControl.apply(this);
166
+ },
167
+
168
+ releaseControl: function() {
169
+ Main.getEventBaton().releaseBaton('processLevelCommand', this.processLevelCommand, this);
170
+
171
+ Level.__super__.releaseControl.apply(this);
172
+ },
173
+
174
+ startOffCommand: function() {
175
+ var method = this.options.command.get('method');
176
+ if (GlobalStateStore.getShouldDisableLevelInstructions()) {
177
+ Main.getEventBaton().trigger(
178
+ 'commandSubmitted',
179
+ 'hint; show goal'
180
+ );
181
+ return;
182
+ }
183
+
184
+ if (!this.testOption('noStartCommand') && method !== 'importLevelNow') {
185
+ Main.getEventBaton().trigger(
186
+ 'commandSubmitted',
187
+ 'hint; delay 2000; show goal'
188
+ );
189
+ }
190
+ },
191
+
192
+ initVisualization: function(options) {
193
+ this.mainVis = new Visualization({
194
+ el: options.el || this.getDefaultVisEl(),
195
+ treeString: options.level.startTree
196
+ });
197
+ },
198
+
199
+ initGoalVisualization: function() {
200
+ var onlyMain = TreeCompare.onlyMainCompared(this.level);
201
+ // first we make the goal visualization holder
202
+ this.goalCanvasHolder = new CanvasTerminalHolder({
203
+ text: (onlyMain) ? intl.str('goal-only-main') : undefined,
204
+ parent: this
205
+ });
206
+
207
+ // then we make a visualization. the "el" here is the element to
208
+ // track for size information. the container is where the canvas will be placed
209
+ this.goalVis = new Visualization({
210
+ el: this.goalCanvasHolder.getCanvasLocation(),
211
+ containerElement: this.goalCanvasHolder.getCanvasLocation(),
212
+ treeString: this.level.goalTreeString,
213
+ noKeyboardInput: true,
214
+ smallCanvas: true,
215
+ isGoalVis: true,
216
+ levelBlob: this.level,
217
+ noClick: true
218
+ });
219
+
220
+ // If the goal visualization gets dragged to the right side of the screen, then squeeze the main
221
+ // repo visualization a bit to make room. This way, you could have the goal window hang out on
222
+ // the right side of the screen and still see the repo visualization.
223
+ this.goalVis.customEvents.on('drag', function(event, ui) {
224
+ if (ui.position.left > 0.5 * $(window).width()) {
225
+ if (!$('#goalPlaceholder').is(':visible')) {
226
+ $('#goalPlaceholder').show();
227
+ this.mainVis.myResize();
228
+ }
229
+ } else {
230
+ if ($('#goalPlaceholder').is(':visible')) {
231
+ $('#goalPlaceholder').hide();
232
+ this.mainVis.myResize();
233
+ }
234
+ }
235
+ }.bind(this));
236
+
237
+ return this.goalCanvasHolder;
238
+ },
239
+
240
+ minimizeGoal: function (position, size) {
241
+ this.isGoalExpanded = false;
242
+ this.trigger('goalToggled');
243
+ this.goalVis.hide();
244
+ this.goalWindowPos = position;
245
+ this.goalWindowSize = size;
246
+ if ($('#goalPlaceholder').is(':visible')) {
247
+ $('#goalPlaceholder').hide();
248
+ this.mainVis.myResize();
249
+ }
250
+ },
251
+
252
+ resizeGoal: function () {
253
+ if (!this.goalVis) {
254
+ return;
255
+ }
256
+ this.goalVis.myResize();
257
+ },
258
+
259
+ showSolution: function(command, deferred) {
260
+ var toIssue = this.level.solutionCommand;
261
+ var issueFunc = function() {
262
+ this.isShowingSolution = true;
263
+ Main.getEventBaton().trigger(
264
+ 'commandSubmitted',
265
+ toIssue
266
+ );
267
+ log.showLevelSolution(this.getEnglishName());
268
+ }.bind(this);
269
+
270
+ var commandStr = command.get('rawStr');
271
+ if (!this.testOptionOnString(commandStr, 'noReset')) {
272
+ toIssue = 'reset --forSolution; ' + toIssue;
273
+ }
274
+ if (this.testOptionOnString(commandStr, 'force')) {
275
+ issueFunc();
276
+ command.finishWith(deferred);
277
+ return;
278
+ }
279
+
280
+ // allow them for force the solution
281
+ var confirmDefer = Q.defer();
282
+ var dialog = intl.getDialog(require('../dialogs/confirmShowSolution'))[0];
283
+ var confirmView = new ConfirmCancelTerminal({
284
+ markdowns: dialog.options.markdowns,
285
+ deferred: confirmDefer
286
+ });
287
+
288
+ confirmDefer.promise
289
+ .then(issueFunc)
290
+ .fail(function() {
291
+ command.setResult("");
292
+ })
293
+ .done(function() {
294
+ // either way we animate, so both options can share this logic
295
+ setTimeout(function() {
296
+ command.finishWith(deferred);
297
+ }, confirmView.getAnimationTime());
298
+ });
299
+ },
300
+
301
+ toggleObjective: function() {
302
+ Main.getEventBaton().trigger(
303
+ 'commandSubmitted',
304
+ 'objective'
305
+ );
306
+ },
307
+
308
+ toggleGoal: function () {
309
+ if (this.goalCanvasHolder && this.goalCanvasHolder.inDom) {
310
+ this.hideGoal();
311
+ } else {
312
+ this.showGoal();
313
+ }
314
+ },
315
+
316
+ showGoal: function(command, defer) {
317
+ this.isGoalExpanded = true;
318
+ this.trigger('goalToggled');
319
+ this.showSideVis(command, defer, this.goalCanvasHolder, this.initGoalVisualization);
320
+ // show the squeezer again we are to the side
321
+ if ($(this.goalVis.el).offset().left > 0.5 * $(window).width()) {
322
+ $('#goalPlaceholder').show();
323
+ this.mainVis.myResize();
324
+ }
325
+ },
326
+
327
+ showSideVis: function(command, defer, canvasHolder, initMethod) {
328
+ var safeFinish = function() {
329
+ if (command) { command.finishWith(defer); }
330
+ };
331
+ if (!canvasHolder || !canvasHolder.inDom) {
332
+ canvasHolder = initMethod.apply(this);
333
+ }
334
+
335
+ canvasHolder.restore(this.goalWindowPos, this.goalWindowSize);
336
+ setTimeout(safeFinish, canvasHolder.getAnimationTime());
337
+ },
338
+
339
+ hideGoal: function(command, defer) {
340
+ this.isGoalExpanded = false;
341
+ this.trigger('goalToggled');
342
+ this.hideSideVis(command, defer, this.goalCanvasHolder);
343
+ },
344
+
345
+ hideSideVis: function(command, defer, canvasHolder, vis) {
346
+ var safeFinish = function() {
347
+ if (command) { command.finishWith(defer); }
348
+ };
349
+
350
+ if (canvasHolder && canvasHolder.inDom) {
351
+ canvasHolder.die();
352
+ setTimeout(safeFinish, canvasHolder.getAnimationTime());
353
+ } else {
354
+ safeFinish();
355
+ }
356
+ },
357
+
358
+ initParseWaterfall: function(options) {
359
+ Level.__super__.initParseWaterfall.apply(this, [options]);
360
+
361
+ // add our specific functionality
362
+ this.parseWaterfall.addFirst(
363
+ 'parseWaterfall',
364
+ parse
365
+ );
366
+
367
+ this.parseWaterfall.addFirst(
368
+ 'instantWaterfall',
369
+ this.getInstantCommands()
370
+ );
371
+
372
+ // if we want to disable certain commands...
373
+ if (options.level.disabledMap) {
374
+ // disable these other commands
375
+ this.parseWaterfall.addFirst(
376
+ 'instantWaterfall',
377
+ new DisabledMap({
378
+ disabledMap: options.level.disabledMap
379
+ }).getInstantCommands()
380
+ );
381
+ }
382
+ },
383
+
384
+ initGitShim: function(options) {
385
+ // ok we definitely want a shim here
386
+ this.gitShim = new GitShim({
387
+ beforeCB: this.beforeCommandCB.bind(this),
388
+ afterCB: this.afterCommandCB.bind(this),
389
+ afterDeferHandler: this.afterCommandDefer.bind(this)
390
+ });
391
+ },
392
+
393
+ undo: function() {
394
+ this.gitCommandsIssued.pop();
395
+ Level.__super__.undo.apply(this, arguments);
396
+ },
397
+
398
+ beforeCommandCB: function(command) {
399
+ // Alright we actually no-op this in the level subclass
400
+ // so we can tell if the command counted or not... kinda :P
401
+ // We have to save the state in this method since the git
402
+ // engine will change by the time afterCommandCB runs
403
+ this._treeBeforeCommand = this.mainVis.gitEngine.printTree();
404
+ },
405
+
406
+ afterCommandCB: function(command) {
407
+ if (this.doesCommandCountTowardsTotal(command)) {
408
+ // Count it as a command AND...
409
+ this.gitCommandsIssued.push(command.get('rawStr'));
410
+ // add our state for undo since our undo pops a command.
411
+ //
412
+ // Ugly inheritance overriding on private implementations ahead!
413
+ this.undoStack.push(this._treeBeforeCommand);
414
+ }
415
+ },
416
+
417
+ doesCommandCountTowardsTotal: function(command) {
418
+ if (command.get('error')) {
419
+ // don't count errors towards our count
420
+ return false;
421
+ }
422
+
423
+ var matched = false;
424
+ var commandsThatCount = Commands.commands.getCommandsThatCount();
425
+ Object.values(commandsThatCount).forEach(function(map) {
426
+ Object.values(map).forEach(function(regex) {
427
+ matched = matched || regex.test(command.get('rawStr'));
428
+ });
429
+ });
430
+ return matched;
431
+ },
432
+
433
+ afterCommandDefer: function(defer, command) {
434
+ if (this.solved) {
435
+ command.addWarning(intl.str('already-solved'));
436
+ defer.resolve();
437
+ return;
438
+ }
439
+
440
+ var current = this.mainVis.gitEngine.printTree();
441
+ var solved = TreeCompare.dispatchFromLevel(this.level, current);
442
+
443
+ if (!solved) {
444
+ defer.resolve();
445
+ return;
446
+ }
447
+
448
+ // woohoo!!! they solved the level, lets animate and such
449
+ this.levelSolved(defer);
450
+ },
451
+
452
+ getNumSolutionCommands: function() {
453
+ // strip semicolons in bad places
454
+ var toAnalyze = this.level.solutionCommand.replace(/^;|;$/g, '');
455
+ return toAnalyze.split(';').length;
456
+ },
457
+
458
+ testOption: function(option) {
459
+ return this.options.command && new RegExp('--' + option).test(this.options.command.get('rawStr'));
460
+ },
461
+
462
+ testOptionOnString: function(str, option) {
463
+ return str && new RegExp('--' + option).test(str);
464
+ },
465
+
466
+ levelSolved: function(defer) {
467
+ this.solved = true;
468
+ if (!this.isShowingSolution) {
469
+ LevelActions.setLevelSolved(this.level.id);
470
+ log.levelSolved(this.getEnglishName());
471
+ }
472
+
473
+ this.hideGoal();
474
+
475
+ var nextLevel = LevelStore.getNextLevel(this.level.id);
476
+ var numCommands = this.gitCommandsIssued.length;
477
+ var best = this.getNumSolutionCommands();
478
+
479
+ var skipFinishDialog = this.testOption('noFinishDialog') ||
480
+ this.wasResetAfterSolved;
481
+ var skipFinishAnimation = this.wasResetAfterSolved;
482
+
483
+ if (!skipFinishAnimation) {
484
+ GlobalStateActions.levelSolved();
485
+ }
486
+
487
+ /**
488
+ * Speed up the animation each time we see it.
489
+ */
490
+ var speed = 1.0;
491
+ switch (GlobalStateStore.getNumLevelsSolved()) {
492
+ case 2:
493
+ speed = 1.5;
494
+ break;
495
+ case 3:
496
+ speed = 1.8;
497
+ break;
498
+ case 4:
499
+ speed = 2.1;
500
+ break;
501
+ case 5:
502
+ speed = 2.4;
503
+ break;
504
+ }
505
+ if (GlobalStateStore.getNumLevelsSolved() > 5) {
506
+ speed = 2.5;
507
+ }
508
+
509
+ var finishAnimationChain = null;
510
+ if (skipFinishAnimation) {
511
+ var deferred = Q.defer();
512
+ deferred.resolve();
513
+ finishAnimationChain = deferred.promise;
514
+ Main.getEventBaton().trigger(
515
+ 'commandSubmitted',
516
+ 'echo "level solved! type in \'levels\' to access the next level"'
517
+ );
518
+ } else {
519
+ GlobalStateActions.changeIsAnimating(true);
520
+ finishAnimationChain = this.mainVis.gitVisuals.finishAnimation(speed);
521
+ if (this.mainVis.originVis) {
522
+ finishAnimationChain = finishAnimationChain.then(
523
+ this.mainVis.originVis.gitVisuals.finishAnimation(speed)
524
+ );
525
+ }
526
+ }
527
+
528
+ if (!skipFinishDialog) {
529
+ finishAnimationChain = finishAnimationChain.then(function() {
530
+ // we want to ask if they will move onto the next level
531
+ // while giving them their results...
532
+ var nextDialog = new NextLevelConfirm({
533
+ nextLevel: nextLevel,
534
+ numCommands: numCommands,
535
+ best: best
536
+ });
537
+
538
+ return nextDialog.getPromise();
539
+ });
540
+ }
541
+
542
+ finishAnimationChain
543
+ .then(function() {
544
+ if (!skipFinishDialog && nextLevel) {
545
+ log.choseNextLevel(nextLevel.id);
546
+ Main.getEventBaton().trigger(
547
+ 'commandSubmitted',
548
+ 'level ' + nextLevel.id
549
+ );
550
+ }
551
+ })
552
+ .fail(function() {
553
+ // nothing to do, we will just close
554
+ })
555
+ .done(function() {
556
+ GlobalStateActions.changeIsAnimating(false);
557
+ defer.resolve();
558
+ });
559
+ },
560
+
561
+ die: function() {
562
+ ReactDOM.unmountComponentAtNode(
563
+ document.getElementById('levelToolbarMount')
564
+ );
565
+
566
+ this.hideGoal();
567
+ this.mainVis.die();
568
+ this.releaseControl();
569
+
570
+ this.clear();
571
+
572
+ delete this.commandCollection;
573
+ delete this.mainVis;
574
+ delete this.goalVis;
575
+ delete this.goalCanvasHolder;
576
+ },
577
+
578
+ getInstantCommands: function() {
579
+ var getHint = function() {
580
+ var hint = intl.getHint(this.level);
581
+ if (!hint || !hint.length) {
582
+ return intl.str('no-hint');
583
+ }
584
+ return hint;
585
+ }.bind(this);
586
+
587
+ return [
588
+ [/^help$|^\?$/, function() {
589
+ throw new Errors.CommandResult({
590
+ msg: intl.str('help-vague-level')
591
+ });
592
+ }],
593
+ [/^hint$/, function() {
594
+ throw new Errors.CommandResult({
595
+ msg: getHint()
596
+ });
597
+ }]
598
+ ];
599
+ },
600
+
601
+ reset: function(command, deferred) {
602
+ this.gitCommandsIssued = [];
603
+
604
+ var commandStr = (command) ? command.get('rawStr') : '';
605
+ if (!this.testOptionOnString(commandStr, 'forSolution')) {
606
+ this.isShowingSolution = false;
607
+ }
608
+ if (this.solved) {
609
+ this.wasResetAfterSolved = true;
610
+ }
611
+ this.solved = false;
612
+ Level.__super__.reset.apply(this, arguments);
613
+ },
614
+
615
+ buildLevel: function(command, deferred) {
616
+ this.exitLevel();
617
+ setTimeout(function() {
618
+ Main.getSandbox().buildLevel(command, deferred);
619
+ }, this.getAnimationTime() * 1.5);
620
+ },
621
+
622
+ importLevel: function(command, deferred) {
623
+ this.exitLevel();
624
+ setTimeout(function() {
625
+ Main.getSandbox().importLevel(command, deferred);
626
+ }, this.getAnimationTime() * 1.5);
627
+ },
628
+
629
+ startLevel: function(command, deferred) {
630
+ this.exitLevel();
631
+
632
+ setTimeout(function() {
633
+ Main.getSandbox().startLevel(command, deferred);
634
+ }, this.getAnimationTime() * 1.5);
635
+ // wow! that was simple :D
636
+ },
637
+
638
+ exitLevel: function(command, deferred) {
639
+ this.die();
640
+
641
+ if (!command || !deferred) {
642
+ return;
643
+ }
644
+
645
+ setTimeout(function() {
646
+ command.finishWith(deferred);
647
+ }, this.getAnimationTime());
648
+
649
+ // we need to fade in the sandbox
650
+ Main.getEventBaton().trigger('levelExited');
651
+ },
652
+
653
+ processLevelCommand: function(command, defer) {
654
+ var methodMap = {
655
+ 'show goal': this.showGoal,
656
+ 'hide goal': this.hideGoal,
657
+ 'show solution': this.showSolution,
658
+ 'start dialog': this.startDialog,
659
+ 'help level': this.startDialog,
660
+ 'objective': this.objectiveDialog
661
+ };
662
+ var method = methodMap[command.get('method')];
663
+ if (!method) {
664
+ throw new Error('woah we don\'t support that method yet', method);
665
+ }
666
+
667
+ method.apply(this, [command, defer]);
668
+ }
669
+ });
670
+
671
+ exports.Level = Level;
672
+ exports.regexMap = regexMap;
src/js/level/parseWaterfall.js ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var GitCommands = require('../git/commands');
2
+ var Commands = require('../commands');
3
+ var SandboxCommands = require('../sandbox/commands');
4
+
5
+ // more or less a static class
6
+ var ParseWaterfall = function(options) {
7
+ options = options || {};
8
+ this.options = options;
9
+ this.shortcutWaterfall = options.shortcutWaterfall || [
10
+ Commands.commands.getShortcutMap()
11
+ ];
12
+
13
+ this.instantWaterfall = options.instantWaterfall || [
14
+ GitCommands.instantCommands,
15
+ SandboxCommands.instantCommands
16
+ ];
17
+
18
+ // defer the parse waterfall until later...
19
+ };
20
+
21
+ ParseWaterfall.prototype.initParseWaterfall = function() {
22
+ // check for node when testing
23
+ if (!require('../util').isBrowser()) {
24
+ this.parseWaterfall = [Commands.parse];
25
+ return;
26
+ }
27
+
28
+ // by deferring the initialization here, we don't require()
29
+ // level too early (which barfs our init)
30
+ this.parseWaterfall = this.options.parseWaterfall || [
31
+ Commands.parse,
32
+ SandboxCommands.parse,
33
+ SandboxCommands.getOptimisticLevelParse(),
34
+ SandboxCommands.getOptimisticLevelBuilderParse()
35
+ ];
36
+ };
37
+
38
+ ParseWaterfall.prototype.clone = function() {
39
+ return new ParseWaterfall({
40
+ shortcutWaterfall: this.shortcutWaterfall.slice(),
41
+ instantWaterfall: this.instantWaterfall.slice(),
42
+ parseWaterfall: this.parseWaterfall.slice()
43
+ });
44
+ };
45
+
46
+ ParseWaterfall.prototype.getWaterfallMap = function() {
47
+ if (!this.parseWaterfall) {
48
+ this.initParseWaterfall();
49
+ }
50
+ return {
51
+ shortcutWaterfall: this.shortcutWaterfall,
52
+ instantWaterfall: this.instantWaterfall,
53
+ parseWaterfall: this.parseWaterfall
54
+ };
55
+ };
56
+
57
+ ParseWaterfall.prototype.addFirst = function(which, value) {
58
+ if (!which || !value) {
59
+ throw new Error('need to know which!!!');
60
+ }
61
+ this.getWaterfallMap()[which].unshift(value);
62
+ };
63
+
64
+ ParseWaterfall.prototype.addLast = function(which, value) {
65
+ this.getWaterfallMap()[which].push(value);
66
+ };
67
+
68
+ ParseWaterfall.prototype.expandAllShortcuts = function(commandStr) {
69
+ this.shortcutWaterfall.forEach(function(shortcutMap) {
70
+ commandStr = this.expandShortcut(commandStr, shortcutMap);
71
+ }, this);
72
+ return commandStr;
73
+ };
74
+
75
+ ParseWaterfall.prototype.expandShortcut = function(commandStr, shortcutMap) {
76
+ Object.keys(shortcutMap).forEach(function(vcs) {
77
+ var map = shortcutMap[vcs];
78
+ Object.keys(map).forEach(function(method) {
79
+ var regex = map[method];
80
+ var results = regex.exec(commandStr);
81
+ if (results) {
82
+ commandStr = vcs + ' ' + method + ' ' + commandStr.slice(results[0].length);
83
+ }
84
+ });
85
+ });
86
+ return commandStr;
87
+ };
88
+
89
+ ParseWaterfall.prototype.processAllInstants = function(commandStr) {
90
+ this.instantWaterfall.forEach(function(instantCommands) {
91
+ this.processInstant(commandStr, instantCommands);
92
+ }, this);
93
+ };
94
+
95
+ ParseWaterfall.prototype.processInstant = function(commandStr, instantCommands) {
96
+ instantCommands.forEach(function(tuple) {
97
+ var regex = tuple[0];
98
+ var results = regex.exec(commandStr);
99
+ if (results) {
100
+ // this will throw a result because it's an instant
101
+ tuple[1](results);
102
+ }
103
+ });
104
+ };
105
+
106
+ ParseWaterfall.prototype.parseAll = function(commandStr) {
107
+ if (!this.parseWaterfall) {
108
+ this.initParseWaterfall();
109
+ }
110
+
111
+ var toReturn = false;
112
+ this.parseWaterfall.forEach(function(parseFunc) {
113
+ var results = parseFunc(commandStr);
114
+ if (results) {
115
+ toReturn = results;
116
+ }
117
+ }, this);
118
+
119
+ return toReturn;
120
+ };
121
+
122
+ exports.ParseWaterfall = ParseWaterfall;
src/js/log/index.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ var log = function(category, action, label) {
3
+ window._gaq = window._gaq || [];
4
+ window._gaq.push(['_trackEvent', category, action, label]);
5
+ //console.log('just logged ', [category, action, label].join('|'));
6
+ };
7
+
8
+ exports.viewInteracted = function(viewName) {
9
+ log('views', 'interacted', viewName);
10
+ };
11
+
12
+ exports.showLevelSolution = function(levelName) {
13
+ log('levels', 'showedLevelSolution', levelName);
14
+ };
15
+
16
+ exports.choseNextLevel = function(levelID) {
17
+ log('levels', 'nextLevelChosen', levelID);
18
+ };
19
+
20
+ exports.levelSelected = function(levelName) {
21
+ log('levels', 'levelSelected', levelName);
22
+ };
23
+
24
+ exports.levelSolved = function(levelName) {
25
+ log('levels', 'levelSolved', levelName);
26
+ };
27
+
28
+ exports.commandEntered = function(value) {
29
+ log('commands', 'commandEntered', value);
30
+ };
31
+
src/js/mercurial/commands.js ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var intl = require('../intl');
2
+
3
+ var GitCommands = require('../git/commands');
4
+ var Errors = require('../util/errors');
5
+
6
+ var CommandProcessError = Errors.CommandProcessError;
7
+ var GitError = Errors.GitError;
8
+ var Warning = Errors.Warning;
9
+ var CommandResult = Errors.CommandResult;
10
+
11
+ var commandConfig = {
12
+ commit: {
13
+ regex: /^hg +(commit|ci)($|\s)/,
14
+ options: [
15
+ '--amend',
16
+ '-A',
17
+ '-m'
18
+ ],
19
+ delegate: function(engine, command) {
20
+ var options = command.getOptionsMap();
21
+ if (options['-A']) {
22
+ command.addWarning(intl.str('hg-a-option'));
23
+ }
24
+
25
+ return {
26
+ vcs: 'git',
27
+ name: 'commit'
28
+ };
29
+ }
30
+ },
31
+
32
+ status: {
33
+ regex: /^hg +(status|st) *$/,
34
+ dontCountForGolf: true,
35
+ execute: function(engine, command) {
36
+ throw new GitError({
37
+ msg: intl.str('hg-error-no-status')
38
+ });
39
+ }
40
+ },
41
+
42
+ 'export': {
43
+ regex: /^hg +export($|\s)/,
44
+ dontCountForGolf: true,
45
+ delegate: function(engine, command) {
46
+ command.mapDotToHead();
47
+ return {
48
+ vcs: 'git',
49
+ name: 'show'
50
+ };
51
+ }
52
+ },
53
+
54
+ graft: {
55
+ regex: /^hg +graft($|\s)/,
56
+ options: [
57
+ '-r'
58
+ ],
59
+ delegate: function(engine, command) {
60
+ command.acceptNoGeneralArgs();
61
+ command.prependOptionR();
62
+ return {
63
+ vcs: 'git',
64
+ name: 'cherrypick'
65
+ };
66
+ }
67
+ },
68
+
69
+ log: {
70
+ regex: /^hg +log($|\s)/,
71
+ options: [
72
+ '-f'
73
+ ],
74
+ dontCountForGolf: true,
75
+ delegate: function(engine, command) {
76
+ var options = command.getOptionsMap();
77
+ command.acceptNoGeneralArgs();
78
+
79
+ if (!options['-f']) {
80
+ throw new GitError({
81
+ msg: intl.str('hg-error-log-no-follow')
82
+ });
83
+ }
84
+ command.mapDotToHead();
85
+ return {
86
+ vcs: 'git',
87
+ name: 'log'
88
+ };
89
+ }
90
+ },
91
+
92
+ bookmark: {
93
+ regex: /^hg (bookmarks|bookmark|book)($|\s)/,
94
+ options: [
95
+ '-r',
96
+ '-f',
97
+ '-d'
98
+ ],
99
+ delegate: function(engine, command) {
100
+ var options = command.getOptionsMap();
101
+ var generalArgs = command.getGeneralArgs();
102
+ var branchName;
103
+ var rev;
104
+
105
+ var delegate = {vcs: 'git'};
106
+
107
+ if (options['-m'] && options['-d']) {
108
+ throw new GitError({
109
+ msg: intl.todo('-m and -d are incompatible')
110
+ });
111
+ }
112
+ if (options['-d'] && options['-r']) {
113
+ throw new GitError({
114
+ msg: intl.todo('-r is incompatible with -d')
115
+ });
116
+ }
117
+ if (options['-m'] && options['-r']) {
118
+ throw new GitError({
119
+ msg: intl.todo('-r is incompatible with -m')
120
+ });
121
+ }
122
+ if (generalArgs.length + (options['-r'] ? options['-r'].length : 0) +
123
+ (options['-d'] ? options['-d'].length : 0) === 0) {
124
+ delegate.name = 'branch';
125
+ return delegate;
126
+ }
127
+
128
+ if (options['-d']) {
129
+ options['-D'] = options['-d'];
130
+ delete options['-d'];
131
+ delegate.name = 'branch';
132
+ } else {
133
+ if (options['-r']) {
134
+ // we specified a revision with -r but
135
+ // need to flip the order
136
+ generalArgs = command.getGeneralArgs();
137
+ branchName = generalArgs[0];
138
+ rev = options['-r'][0];
139
+ delegate.name = 'branch';
140
+
141
+ // transform to what git wants
142
+ command.setGeneralArgs([branchName, rev]);
143
+ } else if (generalArgs.length > 0) {
144
+ command.setOptionsMap({'-b': [generalArgs[0]]});
145
+ delegate.name = 'checkout';
146
+ command.setGeneralArgs([]);
147
+ } else {
148
+ delegate.name = 'branch';
149
+ }
150
+ }
151
+
152
+ return delegate;
153
+ }
154
+ },
155
+
156
+ rebase: {
157
+ regex: /^hg +rebase($|\s+)/,
158
+ options: [
159
+ '-d',
160
+ '-s',
161
+ '-b'
162
+ ],
163
+ execute: function(engine, command) {
164
+ var throwE = function() {
165
+ throw new GitError({
166
+ msg: intl.str('git-error-options')
167
+ });
168
+ };
169
+
170
+ var options = command.getOptionsMap();
171
+ // if we have both OR if we have neither
172
+ if ((options['-d'] && options['-s']) ||
173
+ (!options['-d'] && !options['-s'])) {
174
+ }
175
+
176
+ if (!options['-b']) {
177
+ options['-b'] = ['.'];
178
+ }
179
+
180
+ command.setOptionsMap(options);
181
+ command.mapDotToHead();
182
+ options = command.getOptionsMap();
183
+
184
+ if (options['-d']) {
185
+ var dest = options['-d'][0] || throwE();
186
+ var base = options['-b'][0];
187
+
188
+ engine.hgRebase(dest, base);
189
+ } else {
190
+ // TODO!!!
191
+ throwE();
192
+ }
193
+ }
194
+ },
195
+
196
+ update: {
197
+ regex: /^hg +(update|up)($|\s+)/,
198
+ options: [
199
+ '-r'
200
+ ],
201
+ delegate: function(engine, command) {
202
+ command.appendOptionR();
203
+ return {
204
+ vcs: 'git',
205
+ name: 'checkout'
206
+ };
207
+ }
208
+ },
209
+
210
+ backout: {
211
+ regex: /^hg +backout($|\s+)/,
212
+ options: [
213
+ '-r'
214
+ ],
215
+ delegate: function(engine, command) {
216
+ command.prependOptionR();
217
+ return {
218
+ vcs: 'git',
219
+ name: 'revert'
220
+ };
221
+ }
222
+ },
223
+
224
+ histedit: {
225
+ regex: /^hg +histedit($|\s+)/,
226
+ delegate: function(engine, command) {
227
+ var args = command.getGeneralArgs();
228
+ command.validateArgBounds(args, 1, 1);
229
+ command.setOptionsMap({
230
+ '-i': args
231
+ });
232
+ command.setGeneralArgs([]);
233
+ return {
234
+ vcs: 'git',
235
+ name: 'rebase'
236
+ };
237
+ }
238
+ },
239
+
240
+ pull: {
241
+ regex: /^hg +pull($|\s+)/,
242
+ delegate: function(engine, command) {
243
+ return {
244
+ vcs: 'git',
245
+ name: 'pull'
246
+ };
247
+ }
248
+ },
249
+
250
+ summary: {
251
+ regex: /^hg +(summary|sum) *$/,
252
+ delegate: function(engine, command) {
253
+ return {
254
+ vcs: 'git',
255
+ name: 'branch'
256
+ };
257
+ }
258
+ }
259
+ };
260
+
261
+ exports.commandConfig = commandConfig;
src/js/models/collections.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Q = require('q');
2
+ var Backbone = require('backbone');
3
+
4
+ var Commit = require('../git').Commit;
5
+ var Branch = require('../git').Branch;
6
+ var Tag = require('../git').Tag;
7
+
8
+ var Command = require('../models/commandModel').Command;
9
+ var TIME = require('../util/constants').TIME;
10
+
11
+ var intl = require('../intl');
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 TagCollection = Backbone.Collection.extend({
26
+ model: Tag
27
+ });
28
+
29
+ var CommandBuffer = Backbone.Model.extend({
30
+ defaults: {
31
+ collection: null
32
+ },
33
+
34
+ initialize: function(options) {
35
+ options.collection.bind('add', this.addCommand, this);
36
+
37
+ this.buffer = [];
38
+ this.timeout = null;
39
+ },
40
+
41
+ addCommand: function(command) {
42
+ this.buffer.push(command);
43
+ this.touchBuffer();
44
+ },
45
+
46
+ touchBuffer: function() {
47
+ // touch buffer just essentially means we just check if our buffer is being
48
+ // processed. if it's not, we immediately process the first item
49
+ // and then set the timeout.
50
+ if (this.timeout) {
51
+ // timeout existence implies its being processed
52
+ return;
53
+ }
54
+ this.setTimeout();
55
+ },
56
+
57
+
58
+ setTimeout: function() {
59
+ this.timeout = setTimeout(function() {
60
+ this.sipFromBuffer();
61
+ }.bind(this), TIME.betweenCommandsDelay);
62
+ },
63
+
64
+ popAndProcess: function() {
65
+ var popped = this.buffer.shift(0);
66
+
67
+ // find a command with no error (aka unprocessed)
68
+ while (popped.get('error') && this.buffer.length) {
69
+ popped = this.buffer.shift(0);
70
+ }
71
+ if (!popped.get('error')) {
72
+ this.processCommand(popped);
73
+ } else {
74
+ // no more commands to process
75
+ this.clear();
76
+ }
77
+ },
78
+
79
+ processCommand: function(command) {
80
+ command.set('status', 'processing');
81
+
82
+ var deferred = Q.defer();
83
+ deferred.promise.then(function() {
84
+ this.setTimeout();
85
+ }.bind(this));
86
+
87
+ var eventName = command.get('eventName');
88
+ if (!eventName) {
89
+ throw new Error('I need an event to trigger when this guy is parsed and ready');
90
+ }
91
+
92
+ var Main = require('../app');
93
+ var eventBaton = Main.getEventBaton();
94
+
95
+ var numListeners = eventBaton.getNumListeners(eventName);
96
+ if (!numListeners) {
97
+ var Errors = require('../util/errors');
98
+ command.set('error', new Errors.GitError({
99
+ msg: intl.str('error-command-currently-not-supported')
100
+ }));
101
+ deferred.resolve();
102
+ return;
103
+ }
104
+
105
+ Main.getEventBaton().trigger(eventName, command, deferred);
106
+ },
107
+
108
+ clear: function() {
109
+ clearTimeout(this.timeout);
110
+ this.timeout = null;
111
+ },
112
+
113
+ sipFromBuffer: function() {
114
+ if (!this.buffer.length) {
115
+ this.clear();
116
+ return;
117
+ }
118
+
119
+ this.popAndProcess();
120
+ }
121
+ });
122
+
123
+ exports.CommitCollection = CommitCollection;
124
+ exports.CommandCollection = CommandCollection;
125
+ exports.BranchCollection = BranchCollection;
126
+ exports.TagCollection = TagCollection;
127
+ exports.CommandBuffer = CommandBuffer;
128
+
src/js/models/commandModel.js ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Backbone = require('backbone');
2
+
3
+ var Errors = require('../util/errors');
4
+
5
+ var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
6
+ var LevelStore = require('../stores/LevelStore');
7
+ var intl = require('../intl');
8
+
9
+ var CommandProcessError = Errors.CommandProcessError;
10
+ var GitError = Errors.GitError;
11
+ var Warning = Errors.Warning;
12
+ var CommandResult = Errors.CommandResult;
13
+
14
+ var Command = Backbone.Model.extend({
15
+ defaults: {
16
+ status: 'inqueue',
17
+ rawStr: null,
18
+ result: '',
19
+ createTime: null,
20
+
21
+ error: null,
22
+ warnings: null,
23
+ parseWaterfall: new ParseWaterfall(),
24
+
25
+ generalArgs: null,
26
+ supportedMap: null,
27
+ options: null,
28
+ method: null
29
+
30
+ },
31
+
32
+ initialize: function() {
33
+ this.initDefaults();
34
+ this.validateAtInit();
35
+
36
+ this.on('change:error', this.errorChanged, this);
37
+ // catch errors on init
38
+ if (this.get('error')) {
39
+ this.errorChanged();
40
+ }
41
+
42
+ this.parseOrCatch();
43
+ },
44
+
45
+ initDefaults: function() {
46
+ // weird things happen with defaults if you don't
47
+ // make new objects
48
+ this.set('generalArgs', []);
49
+ this.set('supportedMap', {});
50
+ this.set('warnings', []);
51
+ },
52
+
53
+ replaceDotWithHead: function(string) {
54
+ return string.replace(/\./g, 'HEAD');
55
+ },
56
+
57
+ /**
58
+ * Since mercurial always wants revisions with
59
+ * -r, we want to just make these general
60
+ * args for git
61
+ */
62
+ appendOptionR: function() {
63
+ var rOptions = this.getOptionsMap()['-r'] || [];
64
+ this.setGeneralArgs(
65
+ this.getGeneralArgs().concat(rOptions)
66
+ );
67
+ },
68
+
69
+ // if order is important
70
+ prependOptionR: function() {
71
+ var rOptions = this.getOptionsMap()['-r'] || [];
72
+ this.setGeneralArgs(
73
+ rOptions.concat(this.getGeneralArgs())
74
+ );
75
+ },
76
+
77
+ mapDotToHead: function() {
78
+ var generalArgs = this.getGeneralArgs();
79
+ var options = this.getOptionsMap();
80
+
81
+ generalArgs = generalArgs.map(function(arg) {
82
+ return this.replaceDotWithHead(arg);
83
+ }, this);
84
+ var newMap = {};
85
+ Object.keys(options).forEach(function(key) {
86
+ var args = options[key];
87
+ newMap[key] = Object.values(args).map(function (arg) {
88
+ return this.replaceDotWithHead(arg);
89
+ }, this);
90
+ }, this);
91
+ this.setGeneralArgs(generalArgs);
92
+ this.setOptionsMap(newMap);
93
+ },
94
+
95
+ deleteOptions: function(options) {
96
+ var map = this.getOptionsMap();
97
+ options.forEach(function(option) {
98
+ delete map[option];
99
+ }, this);
100
+ this.setOptionsMap(map);
101
+ },
102
+
103
+ getGeneralArgs: function() {
104
+ return this.get('generalArgs');
105
+ },
106
+
107
+ setGeneralArgs: function(args) {
108
+ this.set('generalArgs', args);
109
+ },
110
+
111
+ setOptionsMap: function(map) {
112
+ this.set('supportedMap', map);
113
+ },
114
+
115
+ getOptionsMap: function() {
116
+ return this.get('supportedMap');
117
+ },
118
+
119
+ acceptNoGeneralArgs: function() {
120
+ if (this.getGeneralArgs().length) {
121
+ throw new GitError({
122
+ msg: intl.str('git-error-no-general-args')
123
+ });
124
+ }
125
+ },
126
+
127
+ argImpliedHead: function (args, lower, upper, option) {
128
+ // our args we expect to be between {lower} and {upper}
129
+ this.validateArgBounds(args, lower, upper, option);
130
+ // and if it's one, add a HEAD to the back
131
+ this.impliedHead(args, lower);
132
+ },
133
+
134
+ oneArgImpliedHead: function(args, option) {
135
+ this.argImpliedHead(args, 0, 1, option);
136
+ },
137
+
138
+ twoArgsImpliedHead: function(args, option) {
139
+ this.argImpliedHead(args, 1, 2, option);
140
+ },
141
+
142
+ threeArgsImpliedHead: function(args, option) {
143
+ this.argImpliedHead(args, 2, 3, option);
144
+ },
145
+
146
+ oneArgImpliedOrigin: function(args) {
147
+ this.validateArgBounds(args, 0, 1);
148
+ if (!args.length) {
149
+ args.unshift('origin');
150
+ }
151
+ },
152
+
153
+ twoArgsForOrigin: function(args) {
154
+ this.validateArgBounds(args, 0, 2);
155
+ },
156
+
157
+ impliedHead: function(args, min) {
158
+ if(args.length == min) {
159
+ args.push('HEAD');
160
+ }
161
+ },
162
+
163
+ // this is a little utility class to help arg validation that happens over and over again
164
+ validateArgBounds: function(args, lower, upper, option) {
165
+ var what = (option === undefined) ?
166
+ 'git ' + this.get('method') :
167
+ this.get('method') + ' ' + option + ' ';
168
+ what = 'with ' + what;
169
+
170
+ if (args.length < lower) {
171
+ throw new GitError({
172
+ msg: intl.str(
173
+ 'git-error-args-few',
174
+ {
175
+ lower: String(lower),
176
+ what: what
177
+ }
178
+ )
179
+ });
180
+ }
181
+ if (args.length > upper) {
182
+ throw new GitError({
183
+ msg: intl.str(
184
+ 'git-error-args-many',
185
+ {
186
+ upper: String(upper),
187
+ what: what
188
+ }
189
+ )
190
+ });
191
+ }
192
+ },
193
+
194
+ validateAtInit: function() {
195
+ if (this.get('rawStr') === null) {
196
+ throw new Error('Give me a string!');
197
+ }
198
+ if (!this.get('createTime')) {
199
+ this.set('createTime', new Date().toString());
200
+ }
201
+ },
202
+
203
+ setResult: function(msg) {
204
+ this.set('result', msg);
205
+ },
206
+
207
+ finishWith: function(deferred) {
208
+ this.set('status', 'finished');
209
+ deferred.resolve();
210
+ },
211
+
212
+ addWarning: function(msg) {
213
+ this.get('warnings').push(msg);
214
+ // change numWarnings so the change event fires. This is bizarre -- Backbone can't
215
+ // detect if an array changes, so adding an element does nothing
216
+ this.set('numWarnings', this.get('numWarnings') ? this.get('numWarnings') + 1 : 1);
217
+ },
218
+
219
+ parseOrCatch: function() {
220
+ this.expandShortcuts(this.get('rawStr'));
221
+ try {
222
+ this.processInstants();
223
+ } catch (err) {
224
+ Errors.filterError(err);
225
+ // errorChanged() will handle status and all of that
226
+ this.set('error', err);
227
+ return;
228
+ }
229
+
230
+ if (this.parseAll()) {
231
+ // something in our parse waterfall succeeded
232
+ return;
233
+ }
234
+
235
+ // if we reach here, this command is not supported :-/
236
+ this.set('error', new CommandProcessError({
237
+ msg: intl.str(
238
+ 'git-error-command-not-supported',
239
+ {
240
+ command: this.get('rawStr')
241
+ })
242
+ })
243
+ );
244
+ },
245
+
246
+ errorChanged: function() {
247
+ var err = this.get('error');
248
+ if (!err) { return; }
249
+ if (err instanceof CommandProcessError ||
250
+ err instanceof GitError) {
251
+ this.set('status', 'error');
252
+ } else if (err instanceof CommandResult) {
253
+ this.set('status', 'finished');
254
+ } else if (err instanceof Warning) {
255
+ this.set('status', 'warning');
256
+ }
257
+ this.formatError();
258
+ },
259
+
260
+ formatError: function() {
261
+ this.set('result', this.get('error').getMsg());
262
+ },
263
+
264
+ expandShortcuts: function(str) {
265
+ str = this.get('parseWaterfall').expandAllShortcuts(str);
266
+ this.set('rawStr', str);
267
+ },
268
+
269
+ processInstants: function() {
270
+ var str = this.get('rawStr');
271
+ // first if the string is empty, they just want a blank line
272
+ if (!str.length) {
273
+ throw new CommandResult({msg: ""});
274
+ }
275
+
276
+ // then instant commands that will throw
277
+ this.get('parseWaterfall').processAllInstants(str);
278
+ },
279
+
280
+ parseAll: function() {
281
+ var rawInput = this.get('rawStr');
282
+ const aliasMap = LevelStore.getAliasMap();
283
+ for (var i = 0; i<Object.keys(aliasMap).length; i++) {
284
+ var alias = Object.keys(aliasMap)[i];
285
+ var searcher = new RegExp(alias + "(\\s|$)", "g");
286
+ if (searcher.test(rawInput)) {
287
+ rawInput = rawInput.replace(searcher, aliasMap[alias] + ' ');
288
+ break;
289
+ }
290
+ }
291
+
292
+ var results = this.get('parseWaterfall').parseAll(rawInput);
293
+
294
+ if (!results) {
295
+ // nothing parsed successfully
296
+ return false;
297
+ }
298
+
299
+ Object.keys(results.toSet).forEach(function(key) {
300
+ var obj = results.toSet[key];
301
+ // data comes back from the parsing functions like
302
+ // options (etc) that need to be set
303
+ this.set(key, obj);
304
+ }, this);
305
+ return true;
306
+ }
307
+ });
308
+
309
+ exports.Command = Command;
src/js/react_views/CommandHistoryView.jsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var React = require('react');
2
+ var PropTypes = require('prop-types');
3
+
4
+ var CommandView = require('../react_views/CommandView.jsx');
5
+ var Main = require('../app');
6
+
7
+ var _subscribeEvents = [
8
+ 'add',
9
+ 'reset',
10
+ 'change',
11
+ 'all'
12
+ ];
13
+
14
+ class CommandHistoryView extends React.Component {
15
+
16
+ componentDidMount() {
17
+ for (var i = 0; i < _subscribeEvents.length; i++) {
18
+ this.props.commandCollection.on(
19
+ _subscribeEvents[i],
20
+ this.updateFromCollection,
21
+ this
22
+ );
23
+ }
24
+
25
+ this.props.commandCollection.on('change', this.scrollDown, this);
26
+ Main.getEvents().on('commandScrollDown', this.scrollDown, this);
27
+ Main.getEvents().on('clearOldCommands', () => this.clearOldCommands(), this);
28
+ }
29
+
30
+ componentWillUnmount() {
31
+ for (var i = 0; i < _subscribeEvents.length; i++) {
32
+ this.props.commandCollection.off(
33
+ _subscribeEvents[i],
34
+ this.updateFromCollection,
35
+ this
36
+ );
37
+ }
38
+ }
39
+
40
+ updateFromCollection() {
41
+ this.forceUpdate();
42
+ }
43
+
44
+ render() {
45
+ var allCommands = [];
46
+ this.props.commandCollection.each(function(command, index) {
47
+ allCommands.push(
48
+ <CommandView
49
+ id={'command_' + index}
50
+ command={command}
51
+ key={command.cid}
52
+ />
53
+ );
54
+ }, this);
55
+ return (
56
+ <div>
57
+ {allCommands}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ scrollDown() {
63
+ var cD = document.getElementById('commandDisplay');
64
+ var t = document.getElementById('terminal');
65
+
66
+ // firefox hack
67
+ var shouldScroll = (cD.clientHeight > t.clientHeight) ||
68
+ (window.innerHeight < cD.clientHeight);
69
+
70
+ // ugh sometimes i wish i had toggle class
71
+ var hasScroll = t.className.match(/scrolling/g);
72
+ if (shouldScroll && !hasScroll) {
73
+ t.className += ' scrolling';
74
+ } else if (!shouldScroll && hasScroll) {
75
+ t.className = t.className.replace(/shouldScroll/g, '');
76
+ }
77
+
78
+ if (shouldScroll) {
79
+ t.scrollTop = t.scrollHeight;
80
+ }
81
+ }
82
+
83
+ clearOldCommands() {
84
+ // go through and get rid of every command that is "processed" or done
85
+ var toDestroy = [];
86
+
87
+ this.props.commandCollection.each(function(command) {
88
+ if (command.get('status') !== 'inqueue' &&
89
+ command.get('status') !== 'processing') {
90
+ toDestroy.push(command);
91
+ }
92
+ }, this);
93
+ for (var i = 0; i < toDestroy.length; i++) {
94
+ toDestroy[i].destroy();
95
+ }
96
+ this.updateFromCollection();
97
+ this.scrollDown();
98
+ }
99
+
100
+ }
101
+
102
+ CommandHistoryView.propTypes = {
103
+ // the backbone command model collection
104
+ commandCollection: PropTypes.object.isRequired
105
+ };
106
+
107
+ module.exports = CommandHistoryView;
src/js/react_views/CommandView.jsx ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var React = require('react');
2
+ var ReactDOM = require('react-dom');
3
+ var PropTypes = require('prop-types');
4
+
5
+ var reactUtil = require('../util/reactUtil');
6
+ var keyMirror = require('../util/keyMirror');
7
+
8
+ var STATUSES = keyMirror({
9
+ inqueue: null,
10
+ processing: null,
11
+ finished: null
12
+ });
13
+
14
+ class CommandView extends React.Component{
15
+
16
+ componentDidMount() {
17
+ this.props.command.on('change', this.updateStateFromModel, this);
18
+ this.updateStateFromModel();
19
+ }
20
+
21
+ componentWillUnmount() {
22
+ this.props.command.off('change', this.updateStateFromModel, this);
23
+ }
24
+
25
+ updateStateFromModel() {
26
+ var commandJSON = this.props.command.toJSON();
27
+ this.setState({
28
+ status: commandJSON.status,
29
+ rawStr: commandJSON.rawStr,
30
+ warnings: commandJSON.warnings,
31
+ result: commandJSON.result
32
+ });
33
+ }
34
+
35
+ constructor(props, context) {
36
+ super(props, context);
37
+ this.state = {
38
+ status: STATUSES.inqueue,
39
+ rawStr: 'git commit',
40
+ warnings: [],
41
+ result: ''
42
+ };
43
+ }
44
+
45
+ render() {
46
+ var commandClass = reactUtil.joinClasses([
47
+ this.state.status,
48
+ 'commandLine',
49
+ 'transitionBackground'
50
+ ]);
51
+
52
+ return (
53
+ <div id={this.props.id} className="reactCommandView">
54
+ <p className={commandClass}>
55
+ <span className="prompt">{'$'}</span>
56
+ {' '}
57
+ <span dangerouslySetInnerHTML={{
58
+ __html: this.state.rawStr
59
+ }}
60
+ />
61
+ <span className="icons transitionAllSlow">
62
+ <i className="icon-exclamation-sign"></i>
63
+ <i className="icon-check-empty"></i>
64
+ <i className="icon-retweet"></i>
65
+ <i className="icon-check"></i>
66
+ </span>
67
+ </p>
68
+ {this.renderResult()}
69
+ <div className="commandLineWarnings">
70
+ {this.renderFormattedWarnings()}
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ renderResult() {
77
+ if (!this.state.result) {
78
+ return null;
79
+ }
80
+ // We are going to get a ton of raw markup here
81
+ // so lets split into paragraphs ourselves
82
+ var paragraphs = this.state.result.split("\n");
83
+ var result = [];
84
+ for (var i = 0; i < paragraphs.length; i++) {
85
+ if (paragraphs[i].startsWith('https://')) {
86
+ result.push(
87
+ <a
88
+ href={paragraphs[i]}
89
+ key={'paragraph_' + i}
90
+ dangerouslySetInnerHTML={{
91
+ __html: paragraphs[i]
92
+ }}
93
+ />
94
+ );
95
+ } else {
96
+ result.push(
97
+ <p
98
+ key={'paragraph_' + i}
99
+ dangerouslySetInnerHTML={{
100
+ __html: paragraphs[i]
101
+ }}
102
+ />
103
+ );
104
+ }
105
+ }
106
+ return (
107
+ <div className={'commandLineResult'}>
108
+ {result}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ renderFormattedWarnings() {
114
+ var warnings = this.state.warnings;
115
+ var result = [];
116
+ for (var i = 0; i < warnings.length; i++) {
117
+ result.push(
118
+ <p key={'warning_' + i}>
119
+ <i className="icon-exclamation-sign"></i>
120
+ {warnings[i]}
121
+ </p>
122
+ );
123
+ }
124
+ return result;
125
+ }
126
+ };
127
+
128
+ CommandView.propTypes = {
129
+ // the backbone command model
130
+ command: PropTypes.object.isRequired,
131
+ id: PropTypes.string,
132
+ };
133
+
134
+ module.exports = CommandView;
src/js/react_views/CommandsHelperBarView.jsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var React = require('react');
2
+ var PropTypes = require('prop-types');
3
+ var HelperBarView = require('../react_views/HelperBarView.jsx');
4
+ var Main = require('../app');
5
+
6
+ var log = require('../log');
7
+ var intl = require('../intl');
8
+
9
+ class CommandsHelperBarView extends React.Component {
10
+
11
+ render() {
12
+ return (
13
+ <HelperBarView
14
+ items={this.getItems()}
15
+ shown={this.props.shown}
16
+ />
17
+ );
18
+ }
19
+
20
+ fireCommand(command) {
21
+ log.viewInteracted('commandHelperBar');
22
+ Main.getEventBaton().trigger('commandSubmitted', command);
23
+ }
24
+
25
+ getItems() {
26
+ return [{
27
+ text: intl.str('command-helper-bar-levels'),
28
+ onClick: function() {
29
+ this.fireCommand('levels');
30
+ }.bind(this),
31
+ }, {
32
+ text: intl.str('command-helper-bar-solution'),
33
+ onClick: function() {
34
+ this.fireCommand('show solution');
35
+ }.bind(this),
36
+ }, {
37
+ text: intl.str('command-helper-bar-reset'),
38
+ onClick: function() {
39
+ this.fireCommand('reset');
40
+ }.bind(this),
41
+ }, {
42
+ text: intl.str('command-helper-bar-undo'),
43
+ onClick: function() {
44
+ this.fireCommand('undo');
45
+ }.bind(this),
46
+ }, {
47
+ text: intl.str('command-helper-bar-objective'),
48
+ onClick: function() {
49
+ this.fireCommand('objective');
50
+ }.bind(this),
51
+ }, {
52
+ text: intl.str('command-helper-bar-help'),
53
+ onClick: function() {
54
+ this.fireCommand('help general; git help');
55
+ }.bind(this)
56
+ }, {
57
+ icon: 'signout',
58
+ onClick: function() {
59
+ this.props.onExit();
60
+ }.bind(this)
61
+ }];
62
+ }
63
+
64
+ };
65
+
66
+ CommandsHelperBarView.propTypes = {
67
+ shown: PropTypes.bool.isRequired,
68
+ onExit: PropTypes.func.isRequired
69
+ };
70
+
71
+ module.exports = CommandsHelperBarView;
src/js/react_views/HelperBarView.jsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var React = require('react');
2
+ var PropTypes = require('prop-types');
3
+
4
+ var reactUtil = require('../util/reactUtil');
5
+
6
+ class HelperBarView extends React.Component {
7
+
8
+ render() {
9
+ var topClassName = reactUtil.joinClasses([
10
+ 'helperBar',
11
+ 'transitionAll',
12
+ this.props.shown ? 'show' : '',
13
+ this.props.className ? this.props.className : ''
14
+ ]);
15
+
16
+ return (
17
+ <div className={topClassName}>
18
+ {this.props.items.map(function(item, index) {
19
+ return [
20
+ this.renderItem(item, index),
21
+ // ugh -- we need this spacer at the end only
22
+ // if we are not the last element
23
+ index === this.props.items.length - 1 ?
24
+ null :
25
+ <span key={'helper_bar_span_' + index}>{' '}</span>
26
+ ];
27
+ }.bind(this))}
28
+ </div>
29
+ );
30
+ }
31
+
32
+ renderItem(item, index) {
33
+ var testID = item.icon || item.testID ||
34
+ item.text.toLowerCase();
35
+ if (item.newPageLink) {
36
+ return (
37
+ <a
38
+ data-testid={testID}
39
+ key={'helper_bar_' + index}
40
+ onClick={item.onClick}
41
+ target="_blank"
42
+ href={item.href}>
43
+ <i className={'icon-' + item.icon} />
44
+ {' '}
45
+ </a>
46
+ );
47
+ }
48
+ return (
49
+ <a
50
+ data-testid={testID}
51
+ key={'helper_bar_' + index}
52
+ onClick={item.onClick}>
53
+ {item.text ? item.text :
54
+ <i className={'icon-' + item.icon} />
55
+ }
56
+ {' '}
57
+ </a>
58
+ );
59
+ }
60
+
61
+ };
62
+
63
+ HelperBarView.propTypes = {
64
+ className: PropTypes.string,
65
+ shown: PropTypes.bool.isRequired,
66
+ items: PropTypes.array.isRequired
67
+ };
68
+
69
+
70
+ module.exports = HelperBarView;
src/js/react_views/IntlHelperBarView.jsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var PropTypes = require('prop-types');
2
+
3
+ var HelperBarView = require('../react_views/HelperBarView.jsx');
4
+ var Main = require('../app');
5
+ var React = require('react');
6
+
7
+ var log = require('../log');
8
+
9
+ class IntlHelperBarView extends React.Component{
10
+
11
+ render() {
12
+ return (
13
+ <HelperBarView
14
+ items={this.getItems()}
15
+ shown={this.props.shown}
16
+ />
17
+ );
18
+ }
19
+
20
+ fireCommand(command) {
21
+ log.viewInteracted('intlSelect');
22
+ Main.getEventBaton().trigger('commandSubmitted', command);
23
+ this.props.onExit();
24
+ }
25
+
26
+ getItems() {
27
+ return [{
28
+ text: 'Git Branching',
29
+ testID: 'english',
30
+ onClick: function() {
31
+ this.fireCommand('locale en_US; levels');
32
+ }.bind(this)
33
+ }, {
34
+ text: '日本語版リポジトリ',
35
+ testID: 'japanese',
36
+ onClick: function() {
37
+ this.fireCommand('locale ja; levels');
38
+ }.bind(this)
39
+ }, {
40
+ text: 'Git 브랜치 배우기',
41
+ testID: 'korean',
42
+ onClick: function() {
43
+ this.fireCommand('locale ko; levels');
44
+ }.bind(this)
45
+ }, {
46
+ text: '学习 Git 分支',
47
+ testID: 'simplifiedChinese',
48
+ onClick: function() {
49
+ this.fireCommand('locale zh_CN; levels');
50
+ }.bind(this)
51
+ }, {
52
+ text: '學習 Git 分支',
53
+ testID: 'traditionalChinese',
54
+ onClick: function() {
55
+ this.fireCommand('locale zh_TW; levels');
56
+ }.bind(this)
57
+ }, {
58
+ text: 'español',
59
+ testID: 'spanish',
60
+ onClick: function() {
61
+ this.fireCommand('locale es_ES; levels');
62
+ }.bind(this)
63
+ }, {
64
+ text: 'argentino',
65
+ testID: 'argentinian',
66
+ onClick: function() {
67
+ this.fireCommand('locale es_AR; levels');
68
+ }.bind(this)
69
+ }, {
70
+ text: 'mexicano',
71
+ testID: 'mexican',
72
+ onClick: function() {
73
+ this.fireCommand('locale es_MX; levels');
74
+ }.bind(this)
75
+ }, {
76
+ text: 'português',
77
+ testID: 'portuguese',
78
+ onClick: function() {
79
+ this.fireCommand('locale pt_BR; levels');
80
+ }.bind(this)
81
+ }, {
82
+ text: 'français',
83
+ testID: 'french',
84
+ onClick: function() {
85
+ this.fireCommand('locale fr_FR; levels');
86
+ }.bind(this)
87
+ }, {
88
+ text: 'Deutsch',
89
+ testID: 'german',
90
+ onClick: function() {
91
+ this.fireCommand('locale de_DE; levels');
92
+ }.bind(this)
93
+ }, {
94
+ text: 'Русский',
95
+ testID: 'russian',
96
+ onClick: function() {
97
+ this.fireCommand('locale ru_RU; levels');
98
+ }.bind(this)
99
+ }, {
100
+ text: 'Українська',
101
+ testID: 'ukrainian',
102
+ onClick: function() {
103
+ this.fireCommand('locale uk; levels');
104
+ }.bind(this)
105
+ }, {
106
+ text: 'Tiếng Việt',
107
+ testID: 'vietnamese',
108
+ onClick: function() {
109
+ this.fireCommand('locale vi; levels');
110
+ }.bind(this)
111
+ }, {
112
+ text: 'Galego',
113
+ testID: 'galician',
114
+ onClick: function() {
115
+ this.fireCommand('locale gl; levels');
116
+ }.bind(this)
117
+ }, {
118
+ text: 'Slovensko',
119
+ testID: 'slovenian',
120
+ onClick: function() {
121
+ this.fireCommand('locale sl_SI; levels');
122
+ }.bind(this)
123
+ }, {
124
+ text: 'Polski',
125
+ testID: 'polish',
126
+ onClick: function() {
127
+ this.fireCommand('locale pl; levels');
128
+ }.bind(this)
129
+ }, {
130
+ text: 'தமிழ்',
131
+ testID: 'tamil',
132
+ onClick: function() {
133
+ this.fireCommand('locale ta_IN; levels');
134
+ }.bind(this)
135
+ }, {
136
+ text: "italiano",
137
+ testID: "italian",
138
+ onClick: function () {
139
+ this.fireCommand("locale it_IT; levels");
140
+ }.bind(this),
141
+ },{
142
+ icon: 'signout',
143
+ onClick: function() {
144
+ this.props.onExit();
145
+ }.bind(this)
146
+ }
147
+ ];
148
+ }
149
+
150
+ };
151
+
152
+ IntlHelperBarView.propTypes = {
153
+ shown: PropTypes.bool.isRequired,
154
+ onExit: PropTypes.func.isRequired
155
+ }
156
+
157
+ module.exports = IntlHelperBarView;
src/js/react_views/LevelToolbarView.jsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var React = require('react');
2
+ var PropTypes = require('prop-types');
3
+
4
+ var intl = require('../intl');
5
+ var reactUtil = require('../util/reactUtil');
6
+
7
+ class LevelToolbarView extends React.Component {
8
+
9
+ constructor(props, context) {
10
+ super(props, context);
11
+ this.state = {
12
+ isHidden: true,
13
+ isGoalExpanded: this.props.parent.getIsGoalExpanded()
14
+ };
15
+ }
16
+
17
+ componentWillUnmount() {
18
+ this._isMounted = false;
19
+ }
20
+ componentDidMount() {
21
+ this._isMounted = true;
22
+ this.setState({
23
+ isHidden: this.props.parent.getIsGoalExpanded(),
24
+ isGoalExpanded: this.props.parent.getIsGoalExpanded()
25
+ });
26
+ this.props.parent.on('goalToggled', function() {
27
+ if (!this._isMounted) {
28
+ return;
29
+ }
30
+
31
+ this.setState({
32
+ isGoalExpanded: this.props.parent.getIsGoalExpanded()
33
+ });
34
+ }.bind(this));
35
+ }
36
+
37
+ render() {
38
+ return (
39
+ <div className={reactUtil.joinClasses([
40
+ 'toolbar',
41
+ 'level-toolbar',
42
+ 'box',
43
+ 'vertical',
44
+ 'center',
45
+ 'transitionAll',
46
+ this.state.isHidden ? 'hidden' : ''
47
+ ])}>
48
+ <div className="clearfix">
49
+ <div className="levelNameWrapper">
50
+ <i className="icon-bolt"></i>
51
+ { intl.str('level-label') }
52
+ <span className="levelToolbarSpan">
53
+ {this.props.name}
54
+ </span>
55
+ </div>
56
+ </div>
57
+ <div className="buttonsWrapper">
58
+ <div className="showGoalWrapper">
59
+ <button
60
+ onClick={this.props.onGoalClick}
61
+ type="button">
62
+ {this.state.isGoalExpanded ?
63
+ intl.str('hide-goal-button') :
64
+ intl.str('show-goal-button')
65
+ }
66
+ </button>
67
+ </div>
68
+ <div className="showObjectiveWrapper">
69
+ <button
70
+ onClick={this.props.onObjectiveClick}
71
+ type="button">
72
+ {intl.str('objective-button')}
73
+ </button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ };
81
+
82
+ LevelToolbarView.propTypes = {
83
+ name: PropTypes.string.isRequired,
84
+ onGoalClick: PropTypes.func.isRequired,
85
+ onObjectiveClick: PropTypes.func.isRequired,
86
+ parent: PropTypes.object.isRequired
87
+ }
88
+
89
+ module.exports = LevelToolbarView;
src/js/react_views/MainHelperBarView.jsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var HelperBarView = require('../react_views/HelperBarView.jsx');
2
+ var IntlHelperBarView =
3
+ require('../react_views/IntlHelperBarView.jsx');
4
+ var CommandsHelperBarView =
5
+ require('../react_views/CommandsHelperBarView.jsx');
6
+ var React = require('react');
7
+
8
+ var keyMirror = require('../util/keyMirror');
9
+ var log = require('../log');
10
+
11
+ var BARS = keyMirror({
12
+ SELF: null,
13
+ INTL: null,
14
+ COMMANDS: null
15
+ });
16
+
17
+ class MainHelperBarView extends React.Component {
18
+
19
+ constructor(props, context) {
20
+ super(props, context);
21
+ this.state = {
22
+ shownBar: BARS.SELF
23
+ };
24
+ }
25
+
26
+ render() {
27
+ return (
28
+ <div>
29
+ <HelperBarView
30
+ className="BaseHelperBar"
31
+ items={this.getItems()}
32
+ shown={this.state.shownBar === BARS.SELF}
33
+ />
34
+ <CommandsHelperBarView
35
+ shown={this.state.shownBar === BARS.COMMANDS}
36
+ onExit={this.showSelf.bind(this)}
37
+ />
38
+ <IntlHelperBarView
39
+ shown={this.state.shownBar === BARS.INTL}
40
+ onExit={this.showSelf.bind(this)}
41
+ />
42
+ </div>
43
+ );
44
+ }
45
+
46
+ showSelf() {
47
+ this.setState({
48
+ shownBar: BARS.SELF
49
+ });
50
+ }
51
+
52
+ getItems() {
53
+ return [{
54
+ icon: 'question-sign',
55
+ onClick: function() {
56
+ this.setState({
57
+ shownBar: BARS.COMMANDS
58
+ });
59
+ }.bind(this)
60
+ }, {
61
+ icon: 'globe',
62
+ onClick: function() {
63
+ this.setState({
64
+ shownBar: BARS.INTL
65
+ });
66
+ }.bind(this)
67
+ }, {
68
+ newPageLink: true,
69
+ icon: 'twitter',
70
+ href: 'https://twitter.com/petermcottle'
71
+ }];
72
+ }
73
+
74
+ };
75
+
76
+ module.exports = MainHelperBarView;
src/js/sandbox/commands.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var util = require('../util');
2
+
3
+ var constants = require('../util/constants');
4
+ var intl = require('../intl');
5
+
6
+ var Commands = require('../commands');
7
+ var Errors = require('../util/errors');
8
+ var CommandProcessError = Errors.CommandProcessError;
9
+ var LocaleStore = require('../stores/LocaleStore');
10
+ var LocaleActions = require('../actions/LocaleActions');
11
+ var LevelStore = require('../stores/LevelStore');
12
+ var GlobalStateStore = require('../stores/GlobalStateStore');
13
+ var GlobalStateActions = require('../actions/GlobalStateActions');
14
+ var GitError = Errors.GitError;
15
+ var Warning = Errors.Warning;
16
+ var CommandResult = Errors.CommandResult;
17
+
18
+ var instantCommands = [
19
+ [/^ls( |$)/, function() {
20
+ throw new CommandResult({
21
+ msg: intl.str('ls-command')
22
+ });
23
+ }],
24
+ [/^cd( |$)/, function() {
25
+ throw new CommandResult({
26
+ msg: intl.str('cd-command')
27
+ });
28
+ }],
29
+ [/^(locale|locale reset)$/, function(bits) {
30
+ LocaleActions.changeLocale(
31
+ LocaleStore.getDefaultLocale()
32
+ );
33
+
34
+ throw new CommandResult({
35
+ msg: intl.str(
36
+ 'locale-reset-command',
37
+ { locale: LocaleStore.getDefaultLocale() }
38
+ )
39
+ });
40
+ }],
41
+ [/^show$/, function(bits) {
42
+ var lines = [
43
+ intl.str('show-command'),
44
+ '<br/>',
45
+ 'show commands',
46
+ 'show solution',
47
+ 'show goal'
48
+ ];
49
+
50
+ throw new CommandResult({
51
+ msg: lines.join('\n')
52
+ });
53
+ }],
54
+ [/^alias (\w+)="(.+)"$/, function(bits) {
55
+ const alias = bits[1];
56
+ const expansion = bits[2];
57
+ LevelStore.addToAliasMap(alias, expansion);
58
+ throw new CommandResult({
59
+ msg: 'Set alias "'+alias+'" to "'+expansion+'"',
60
+ });
61
+ }],
62
+ [/^unalias (\w+)$/, function(bits) {
63
+ const alias = bits[1];
64
+ LevelStore.removeFromAliasMap(alias);
65
+ throw new CommandResult({
66
+ msg: 'Removed alias "'+alias+'"',
67
+ });
68
+ }],
69
+ [/^locale (\w+)$/, function(bits) {
70
+ LocaleActions.changeLocale(bits[1]);
71
+ throw new CommandResult({
72
+ msg: intl.str(
73
+ 'locale-command',
74
+ { locale: bits[1] }
75
+ )
76
+ });
77
+ }],
78
+ [/^flip$/, function() {
79
+ GlobalStateActions.changeFlipTreeY(
80
+ !GlobalStateStore.getFlipTreeY()
81
+ );
82
+ require('../app').getEvents().trigger('refreshTree');
83
+ throw new CommandResult({
84
+ msg: intl.str('flip-tree-command')
85
+ });
86
+ }],
87
+ [/^disableLevelInstructions$/, function() {
88
+ GlobalStateActions.disableLevelInstructions();
89
+ throw new CommandResult({
90
+ msg: intl.todo('Level instructions disabled'),
91
+ });
92
+ }],
93
+ [/^refresh$/, function() {
94
+ var events = require('../app').getEvents();
95
+
96
+ events.trigger('refreshTree');
97
+ throw new CommandResult({
98
+ msg: intl.str('refresh-tree-command')
99
+ });
100
+ }],
101
+ [/^rollup (\d+)$/, function(bits) {
102
+ var events = require('../app').getEvents();
103
+
104
+ // go roll up these commands by joining them with semicolons
105
+ events.trigger('rollupCommands', bits[1]);
106
+ throw new CommandResult({
107
+ msg: 'Commands combined!'
108
+ });
109
+ }],
110
+ [/^echo "(.*?)"$|^echo (.*?)$/, function(bits) {
111
+ var msg = bits[1] || bits[2];
112
+ throw new CommandResult({
113
+ msg: msg
114
+ });
115
+ }],
116
+ [/^show +commands$/, function(bits) {
117
+ var allCommands = getAllCommands();
118
+ var allOptions = Commands.commands.getOptionMap();
119
+ var commandToOptions = {};
120
+
121
+ Object.keys(allOptions).forEach(function(vcs) {
122
+ var vcsMap = allOptions[vcs];
123
+ Object.keys(vcsMap).forEach(function(method) {
124
+ var options = vcsMap[method];
125
+ if (options) {
126
+ commandToOptions[vcs + ' ' + method] = Object.keys(options).filter(option => option.length > 1);
127
+ }
128
+ });
129
+ });
130
+
131
+
132
+ var lines = [
133
+ intl.str('show-all-commands'),
134
+ '<br/>'
135
+ ];
136
+ Object.keys(allCommands)
137
+ .forEach(function(command) {
138
+ lines.push(command);
139
+ if (commandToOptions[command]) {
140
+ commandToOptions[command].forEach(option => lines.push('&nbsp;&nbsp;&nbsp;&nbsp;' + option));
141
+ }
142
+ });
143
+
144
+ throw new CommandResult({
145
+ msg: lines.join('\n')
146
+ });
147
+ }]
148
+ ];
149
+
150
+ var regexMap = {
151
+ 'reset solved': /^reset solved($|\s)/,
152
+ 'help': /^help( +general)?$|^\?$/,
153
+ 'reset': /^reset( +--forSolution)?$/,
154
+ 'delay': /^delay (\d+)$/,
155
+ 'clear': /^clear($|\s)/,
156
+ 'exit level': /^exit level($|\s)/,
157
+ 'sandbox': /^sandbox($|\s)/,
158
+ 'level': /^level\s?([a-zA-Z0-9]*)/,
159
+ 'levels': /^levels($|\s)/,
160
+ 'mobileAlert': /^mobile alert($|\s)/,
161
+ 'build level': /^build +level\s?([a-zA-Z0-9]*)$/,
162
+ 'export tree': /^export +tree$/,
163
+ 'importTreeNow': /^importTreeNow($|\s)/,
164
+ 'importLevelNow': /^importLevelNow($|\s)/,
165
+ 'import tree': /^import +tree$/,
166
+ 'import level': /^import +level$/,
167
+ 'undo': /^undo($|\s)/,
168
+ 'share permalink': /^share( +permalink)?$/
169
+ };
170
+
171
+ var getAllCommands = function() {
172
+ var toDelete = [
173
+ 'mobileAlert'
174
+ ];
175
+
176
+ var allCommands = Object.assign(
177
+ {},
178
+ require('../level').regexMap,
179
+ regexMap
180
+ );
181
+ var mRegexMap = Commands.commands.getRegexMap();
182
+ Object.keys(mRegexMap).forEach(function(vcs) {
183
+ var map = mRegexMap[vcs];
184
+ Object.keys(map).forEach(function(method) {
185
+ var regex = map[method];
186
+ allCommands[vcs + ' ' + method] = regex;
187
+ });
188
+ });
189
+ toDelete.forEach(function(key) {
190
+ delete allCommands[key];
191
+ });
192
+
193
+ return allCommands;
194
+ };
195
+
196
+ exports.getAllCommands = getAllCommands;
197
+ exports.instantCommands = instantCommands;
198
+ exports.parse = util.genParseCommand(regexMap, 'processSandboxCommand');
199
+
200
+ // optimistically parse some level and level builder commands; we do this
201
+ // so you can enter things like "level intro1; show goal" and not
202
+ // have it barf. when the
203
+ // command fires the event, it will check if there is a listener and if not throw
204
+ // an error
205
+
206
+ // note: these are getters / setters because the require kills us
207
+ exports.getOptimisticLevelParse = function() {
208
+ return util.genParseCommand(
209
+ require('../level').regexMap,
210
+ 'processLevelCommand'
211
+ );
212
+ };
213
+
214
+ exports.getOptimisticLevelBuilderParse = function() {
215
+ return util.genParseCommand(
216
+ require('../level/builder').regexMap,
217
+ 'processLevelBuilderCommand'
218
+ );
219
+ };
src/js/sandbox/index.js ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Q = require('q');
2
+ var Backbone = require('backbone');
3
+
4
+ var util = require('../util');
5
+ var intl = require('../intl');
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 DisabledMap = require('../level/disabledMap').DisabledMap;
12
+ var Command = require('../models/commandModel').Command;
13
+ var GitShim = require('../git/gitShim').GitShim;
14
+ var LevelActions = require('../actions/LevelActions');
15
+ var LevelStore = require('../stores/LevelStore');
16
+
17
+ var Views = require('../views');
18
+ var ModalTerminal = Views.ModalTerminal;
19
+ var ModalAlert = Views.ModalAlert;
20
+ var BuilderViews = require('../views/builderViews');
21
+ var MultiView = require('../views/multiView').MultiView;
22
+
23
+ var Sandbox = Backbone.View.extend({
24
+ // tag name here is purely vestigial. I made this a view
25
+ // simply to use inheritance and have a nice event system in place
26
+ tagName: 'div',
27
+ initialize: function(options) {
28
+ options = options || {};
29
+ this.options = options;
30
+
31
+ this.initVisualization(options);
32
+ this.initCommandCollection(options);
33
+ this.initParseWaterfall(options);
34
+ this.initGitShim(options);
35
+ this.initUndoStack(options);
36
+
37
+ if (!options.wait) {
38
+ this.takeControl();
39
+ }
40
+ },
41
+
42
+ getDefaultVisEl: function() {
43
+ return $('#mainVisSpace')[0];
44
+ },
45
+
46
+ getAnimationTime: function() { return 700 * 1.5; },
47
+
48
+ initVisualization: function(options) {
49
+ this.mainVis = new Visualization({
50
+ el: options.el || this.getDefaultVisEl()
51
+ });
52
+ },
53
+
54
+ initUndoStack: function(options) {
55
+ this.undoStack = [];
56
+ },
57
+
58
+ initCommandCollection: function(options) {
59
+ // don't add it to just any collection -- adding to the
60
+ // CommandUI collection will put in history
61
+ this.commandCollection = Main.getCommandUI().commandCollection;
62
+ },
63
+
64
+ initParseWaterfall: function(options) {
65
+ this.parseWaterfall = new ParseWaterfall();
66
+ },
67
+
68
+ initGitShim: function(options) {
69
+ this.gitShim = new GitShim({
70
+ beforeCB: this.beforeCommandCB.bind(this)
71
+ });
72
+ },
73
+
74
+ takeControl: function() {
75
+ // we will be handling commands that are submitted, mainly to add the sandbox
76
+ // functionality (which is included by default in ParseWaterfall())
77
+ Main.getEventBaton().stealBaton('commandSubmitted', this.commandSubmitted, this);
78
+ // we obviously take care of sandbox commands
79
+ Main.getEventBaton().stealBaton('processSandboxCommand', this.processSandboxCommand, this);
80
+
81
+ // a few things to help transition between levels and sandbox
82
+ Main.getEventBaton().stealBaton('levelExited', this.levelExited, this);
83
+
84
+ this.insertGitShim();
85
+ },
86
+
87
+ releaseControl: function() {
88
+ // we will be handling commands that are submitted, mainly to add the sandbox
89
+ // functionality (which is included by default in ParseWaterfall())
90
+ Main.getEventBaton().releaseBaton('commandSubmitted', this.commandSubmitted, this);
91
+ // we obviously take care of sandbox commands
92
+ Main.getEventBaton().releaseBaton('processSandboxCommand', this.processSandboxCommand, this);
93
+ // a few things to help transition between levels and sandbox
94
+ Main.getEventBaton().releaseBaton('levelExited', this.levelExited, this);
95
+
96
+ this.releaseGitShim();
97
+ },
98
+
99
+ releaseGitShim: function() {
100
+ if (this.gitShim) {
101
+ this.gitShim.removeShim();
102
+ }
103
+ },
104
+
105
+ insertGitShim: function() {
106
+ // and our git shim goes in after the git engine is ready so it doesn't steal the baton
107
+ // too early
108
+ if (this.gitShim) {
109
+ this.mainVis.customEvents.on('gitEngineReady', function() {
110
+ this.gitShim.insertShim();
111
+ },this);
112
+ }
113
+ },
114
+
115
+ beforeCommandCB: function(command) {
116
+ this.pushUndo();
117
+ },
118
+
119
+ pushUndo: function() {
120
+ // go ahead and push the three onto the stack
121
+ this.undoStack.push(this.mainVis.gitEngine.printTree());
122
+ },
123
+
124
+ undo: function(command, deferred) {
125
+ var toRestore = this.undoStack.pop();
126
+ if (!toRestore) {
127
+ command.set('error', new Errors.GitError({
128
+ msg: intl.str('undo-stack-empty')
129
+ }));
130
+ deferred.resolve();
131
+ return;
132
+ }
133
+
134
+ this.mainVis.reset(toRestore);
135
+ setTimeout(function() {
136
+ command.finishWith(deferred);
137
+ }, this.mainVis.getAnimationTime());
138
+ },
139
+
140
+ commandSubmitted: function(value) {
141
+ // allow other things to see this command (aka command history on terminal)
142
+ Main.getEvents().trigger('commandSubmittedPassive', value);
143
+
144
+ util.splitTextCommand(value, function(command) {
145
+ this.commandCollection.add(new Command({
146
+ rawStr: command,
147
+ parseWaterfall: this.parseWaterfall
148
+ }));
149
+ }, this);
150
+ },
151
+
152
+ startLevel: function(command, deferred) {
153
+ var regexResults = command.get('regexResults') || [];
154
+ var desiredID = regexResults[1] || '';
155
+ var levelJSON = LevelStore.getLevel(desiredID);
156
+
157
+ // handle the case where that level is not found...
158
+ if (!levelJSON) {
159
+ command.addWarning(
160
+ intl.str(
161
+ 'level-no-id',
162
+ { id: desiredID }
163
+ )
164
+ );
165
+ Main.getEventBaton().trigger('commandSubmitted', 'levels');
166
+
167
+ command.set('status', 'error');
168
+ deferred.resolve();
169
+ return;
170
+ }
171
+
172
+ // we are good to go!! lets prep a bit visually
173
+ this.hide();
174
+ this.clear();
175
+
176
+ // we don't even need a reference to this,
177
+ // everything will be handled via event baton :DDDDDDDDD
178
+ var whenLevelOpen = Q.defer();
179
+ var Level = require('../level').Level;
180
+
181
+ this.currentLevel = new Level({
182
+ level: levelJSON,
183
+ deferred: whenLevelOpen,
184
+ command: command
185
+ });
186
+
187
+ whenLevelOpen.promise.then(function() {
188
+ command.finishWith(deferred);
189
+ });
190
+ },
191
+
192
+ buildLevel: function(command, deferred) {
193
+ this.hide();
194
+ this.clear();
195
+
196
+ var whenBuilderOpen = Q.defer();
197
+ var LevelBuilder = require('../level/builder').LevelBuilder;
198
+
199
+ var regexResults = command.get('regexResults') || [];
200
+ var toEdit = regexResults[1] || false;
201
+ this.levelBuilder = new LevelBuilder({
202
+ deferred: whenBuilderOpen,
203
+ editLevel: toEdit
204
+ });
205
+ whenBuilderOpen.promise.then(function() {
206
+ command.finishWith(deferred);
207
+ });
208
+ },
209
+
210
+ exitLevel: function(command, deferred) {
211
+ command.addWarning(
212
+ intl.str('level-cant-exit')
213
+ );
214
+ command.set('status', 'error');
215
+ deferred.resolve();
216
+ },
217
+
218
+ showLevels: function(command, deferred) {
219
+ var whenClosed = Q.defer();
220
+ Main.getLevelDropdown().show(whenClosed, command);
221
+ whenClosed.promise.done(function() {
222
+ command.finishWith(deferred);
223
+ });
224
+ },
225
+
226
+ sharePermalink: function(command, deferred) {
227
+ var treeJSON = JSON.stringify(this.mainVis.gitEngine.exportTree());
228
+ var url =
229
+ 'https://learngitbranching.js.org/?NODEMO&command=importTreeNow%20' + escape(treeJSON);
230
+ command.setResult(
231
+ intl.todo('Here is a link to the current state of the tree: ') + '\n' + url
232
+ );
233
+ command.finishWith(deferred);
234
+ },
235
+
236
+ resetSolved: function(command, deferred) {
237
+ if (command.get('regexResults').input !== 'reset solved --confirm') {
238
+ command.set('error', new Errors.GitError({
239
+ msg: 'Reset solved will mark each level as not yet solved; because ' +
240
+ 'this is a destructive command, please pass in --confirm to execute',
241
+ }));
242
+ command.finishWith(deferred);
243
+ return;
244
+ }
245
+
246
+ LevelActions.resetLevelsSolved();
247
+ command.addWarning(
248
+ intl.str('solved-map-reset')
249
+ );
250
+ command.finishWith(deferred);
251
+ },
252
+
253
+ processSandboxCommand: function(command, deferred) {
254
+ // I'm tempted to do cancel case conversion, but there are
255
+ // some exceptions to the rule
256
+ var commandMap = {
257
+ 'reset solved': this.resetSolved,
258
+ 'undo': this.undo,
259
+ 'help general': this.helpDialog,
260
+ 'help': this.helpDialog,
261
+ 'reset': this.reset,
262
+ 'delay': this.delay,
263
+ 'clear': this.clear,
264
+ 'exit level': this.exitLevel,
265
+ 'level': this.startLevel,
266
+ 'sandbox': this.exitLevel,
267
+ 'levels': this.showLevels,
268
+ 'mobileAlert': this.mobileAlert,
269
+ 'build level': this.buildLevel,
270
+ 'export tree': this.exportTree,
271
+ 'import tree': this.importTree,
272
+ 'importTreeNow': this.importTreeNow,
273
+ 'import level': this.importLevel,
274
+ 'importLevelNow': this.importLevelNow,
275
+ 'share permalink': this.sharePermalink,
276
+ };
277
+
278
+ var method = commandMap[command.get('method')];
279
+ if (!method) { throw new Error('no method for that wut'); }
280
+
281
+ method.apply(this, [command, deferred]);
282
+ },
283
+
284
+ hide: function() {
285
+ this.mainVis.hide();
286
+ },
287
+
288
+ levelExited: function() {
289
+ this.show();
290
+ },
291
+
292
+ show: function() {
293
+ this.mainVis.show();
294
+ },
295
+
296
+ importLevelNow: function(command, deferred) {
297
+ var options = command.get('regexResults') || [];
298
+ if (options.length < 2) {
299
+ command.set('error', new Errors.GitError({
300
+ msg: intl.str('git-error-options')
301
+ }));
302
+ command.finishWith(deferred);
303
+ return;
304
+ }
305
+ var string = options.input.replace(/importLevelNow\s+/g, '');
306
+ var Level = require('../level').Level;
307
+ try {
308
+ var levelJSON = JSON.parse(unescape(string));
309
+ var whenLevelOpen = Q.defer();
310
+ this.currentLevel = new Level({
311
+ level: levelJSON,
312
+ deferred: whenLevelOpen,
313
+ command: command
314
+ });
315
+ this.hide();
316
+
317
+ whenLevelOpen.promise.then(function() {
318
+ command.finishWith(deferred);
319
+ });
320
+ } catch(e) {
321
+ command.set('error', new Errors.GitError({
322
+ msg: 'Something went wrong ' + String(e)
323
+ }));
324
+ throw e;
325
+ }
326
+ command.finishWith(deferred);
327
+ },
328
+
329
+ importTreeNow: function(command, deferred) {
330
+ var options = command.get('regexResults') || [];
331
+ if (options.length < 2) {
332
+ command.set('error', new Errors.GitError({
333
+ msg: intl.str('git-error-options')
334
+ }));
335
+ command.finishWith(deferred);
336
+ }
337
+ var string = options.input.replace(/importTreeNow\s+/g, '');
338
+ try {
339
+ this.mainVis.gitEngine.loadTreeFromString(string);
340
+ } catch (e) {
341
+ command.set('error', new Errors.GitError({
342
+ msg: String(e)
343
+ }));
344
+ }
345
+ command.finishWith(deferred);
346
+ },
347
+
348
+ importTree: function(command, deferred) {
349
+ var jsonGrabber = new BuilderViews.MarkdownPresenter({
350
+ previewText: intl.str('paste-json'),
351
+ fillerText: ' '
352
+ });
353
+ jsonGrabber.deferred.promise
354
+ .then(function(treeJSON) {
355
+ try {
356
+ this.mainVis.gitEngine.loadTree(JSON.parse(treeJSON));
357
+ } catch(e) {
358
+ this.mainVis.reset();
359
+ new MultiView({
360
+ childViews: [{
361
+ type: 'ModalAlert',
362
+ options: {
363
+ markdowns: [
364
+ '## Error!',
365
+ '',
366
+ 'Something is wrong with that JSON! Here is the error:',
367
+ '',
368
+ String(e)
369
+ ]
370
+ }
371
+ }]
372
+ });
373
+ }
374
+ }.bind(this))
375
+ .fail(function() { })
376
+ .done(function() {
377
+ command.finishWith(deferred);
378
+ });
379
+ },
380
+
381
+ importLevel: function(command, deferred) {
382
+ var jsonGrabber = new BuilderViews.MarkdownPresenter({
383
+ previewText: intl.str('paste-json'),
384
+ fillerText: ' '
385
+ });
386
+
387
+ jsonGrabber.deferred.promise
388
+ .then(function(inputText) {
389
+ var Level = require('../level').Level;
390
+ try {
391
+ var levelJSON = JSON.parse(inputText);
392
+ var whenLevelOpen = Q.defer();
393
+ this.currentLevel = new Level({
394
+ level: levelJSON,
395
+ deferred: whenLevelOpen,
396
+ command: command
397
+ });
398
+ this.hide();
399
+
400
+ whenLevelOpen.promise.then(function() {
401
+ command.finishWith(deferred);
402
+ });
403
+ } catch(e) {
404
+ new MultiView({
405
+ childViews: [{
406
+ type: 'ModalAlert',
407
+ options: {
408
+ markdowns: [
409
+ '## Error!',
410
+ '',
411
+ 'Something is wrong with that level JSON, this happened:',
412
+ '',
413
+ String(e)
414
+ ]
415
+ }
416
+ }]
417
+ });
418
+ command.finishWith(deferred);
419
+ }
420
+ }.bind(this))
421
+ .fail(function() {
422
+ command.finishWith(deferred);
423
+ })
424
+ .done();
425
+ },
426
+
427
+ exportTree: function(command, deferred) {
428
+ var treeJSON = JSON.stringify(this.mainVis.gitEngine.exportTree(), null, 2);
429
+
430
+ var showJSON = new MultiView({
431
+ childViews: [{
432
+ type: 'MarkdownPresenter',
433
+ options: {
434
+ previewText: intl.str('share-tree'),
435
+ fillerText: treeJSON,
436
+ noConfirmCancel: true
437
+ }
438
+ }]
439
+ });
440
+ showJSON.getPromise()
441
+ .then(function() {
442
+ command.finishWith(deferred);
443
+ })
444
+ .done();
445
+ },
446
+
447
+ clear: function(command, deferred) {
448
+ Main.getEvents().trigger('clearOldCommands');
449
+ if (command && deferred) {
450
+ command.finishWith(deferred);
451
+ }
452
+ },
453
+
454
+ mobileAlert: function(command, deferred) {
455
+ alert(intl.str('mobile-alert'));
456
+ command.finishWith(deferred);
457
+ },
458
+
459
+ delay: function(command, deferred) {
460
+ var amount = parseInt(command.get('regexResults')[1], 10);
461
+ setTimeout(function() {
462
+ command.finishWith(deferred);
463
+ }, amount);
464
+ },
465
+
466
+ reset: function(command, deferred) {
467
+ this.mainVis.reset();
468
+ this.initUndoStack();
469
+
470
+ setTimeout(function() {
471
+ command.finishWith(deferred);
472
+ }, this.mainVis.getAnimationTime());
473
+ },
474
+
475
+ helpDialog: function(command, deferred) {
476
+ var helpDialog = new MultiView({
477
+ childViews: intl.getDialog(require('../dialogs/sandbox'))
478
+ });
479
+ helpDialog.getPromise().then(function() {
480
+ // the view has been closed, lets go ahead and resolve our command
481
+ command.finishWith(deferred);
482
+ }.bind(this))
483
+ .done();
484
+ }
485
+ });
486
+
487
+ exports.Sandbox = Sandbox;