├── .gitignore ├── .travis.yml ├── package.json ├── README.md ├── index.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitjk", 3 | "version": "0.1.14", 4 | "description": "undo or explain how to undo last git command", 5 | "keywords": [ 6 | "git", 7 | "undo", 8 | "oops" 9 | ], 10 | "author": "mapmeld", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mapmeld/gitjk.git" 14 | }, 15 | "dependencies": { 16 | "commander": "~5.1.0", 17 | "git-config": "0.0.7" 18 | }, 19 | "devDependencies": { 20 | "should": ">= 0.0.1", 21 | "mocha": "~7.2.0", 22 | "chai": "~4.1.2" 23 | }, 24 | "preferGlobal": "true", 25 | "bin": { 26 | "gitjk_cmd": "index.js", 27 | "git-jk": "index.js" 28 | }, 29 | "scripts": { 30 | "test": "mocha" 31 | }, 32 | "license": "GPL-3.0+", 33 | "engines": { 34 | "node": ">= 0.6.x" 35 | }, 36 | "files": [ 37 | "index.js" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitjk 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/mapmeld/gitjk.svg)](https://greenkeeper.io/) 4 | 5 | If you just ran a git command that you didn't mean to, this program will either undo it, 6 | tell you how to undo it, or tell you it's impossible to undo. Based on a joke I posted a while ago. 7 | 8 | ## Examples 9 | 10 | Asking for undo-ing advice. 11 | 12 | git init 13 | gitjk 14 | 15 | This created a .git folder in the current directory. You can remove it. 16 | rm -rf .git 17 | 18 | Asking to fix it automatically 19 | 20 | git add file.js 21 | gitjk -f 22 | 23 | This added file.js to the changes staged for commit. All changes to file.js will be removed from 24 | staging for this commit, but remain saved in your file. 25 | Running... git reset file.js 26 | Completed 27 | 28 | ## Coverage 29 | 30 | ### Included: 31 | 32 | add, 33 | archive, 34 | branch, 35 | cat-file, 36 | checkout, 37 | clone, 38 | commit, 39 | diff, 40 | fetch, 41 | grep, 42 | init, 43 | log, 44 | ls-tree, 45 | merge, 46 | mv, 47 | pull, 48 | push, 49 | remote, 50 | revert, 51 | rm, 52 | show, 53 | stash, 54 | status 55 | 56 | Basic [aliases](http://git-scm.com/book/en/Git-Basics-Tips-and-Tricks#Git-Aliases) are also supported (e.g. `git cm` for `git commit`). 57 | 58 | ### Not included: 59 | 60 | bisect, 61 | fsck, 62 | gc, 63 | prune, 64 | rebase, 65 | reset, 66 | tag 67 | 68 | Compound aliases are not supported (e.g. `git ac` for `git add -A && git commit`). 69 | 70 | ## Install 71 | 72 | You need to run npm install and alias to fully install. 73 | The module is named gitjk but you need to set up an alias to pipe the most recent commands into the program. 74 | 75 | ### OSX or BSD 76 | 77 | npm install -g gitjk 78 | alias gitjk="history 10 | tail -r | gitjk_cmd" 79 | 80 | ### Ubuntu / other Linux 81 | 82 | npm install -g gitjk 83 | alias gitjk="history 10 | tac | gitjk_cmd" 84 | 85 | ### Different Terminals 86 | 87 | If you are using `fish`, place this is in `~/.config/fish.config` (from [lunixbochs](https://news.ycombinator.com/user?id=lunixbochs) on Hacker News): 88 | 89 | alias jk="history | head -n+10 | tail -r | gitjk_cmd" 90 | 91 | If you are using `iTerm` 92 | 93 | alias gitjk="history | tail -r -n 10 | gitjk_cmd" 94 | 95 | ## License 96 | 97 | Available under GPLv3 license 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | // requirements 4 | const fs = require('fs'); 5 | const program = require('commander'); 6 | const exec = require('child_process').exec; 7 | const gitConfig = require('git-config'); 8 | 9 | // command line options 10 | program 11 | .version('0.0.12') 12 | .option('-f', '--fix', 'attempt to fix') 13 | .parse(process.argv); 14 | 15 | // get last command 16 | var foundGit = false; 17 | process.stdin.resume(); 18 | process.stdin.setEncoding('utf8'); 19 | process.stdin.on('data', function(last_lines) { 20 | if(last_lines.indexOf('git ') == -1){ 21 | return; 22 | } 23 | last_lines = last_lines.split('\n'); 24 | 25 | for(var a=0; a < last_lines.length; a++){ 26 | var initial_command = last_lines[a]; 27 | if(initial_command.indexOf('git ') == -1){ 28 | continue; 29 | } 30 | undoCommand(initial_command, function(err, info, command, autorun){ 31 | if(info){ 32 | console.log(info); 33 | } 34 | else{ 35 | console.log("I didn't recognize that command"); 36 | return; 37 | } 38 | if(command){ 39 | if((program.rawArgs.indexOf('-f') > -1 || program.rawArgs.indexOf('--fix') > -1) && autorun){ 40 | console.log('Running ' + command); 41 | exec(command, function(){ 42 | console.log('Completed'); 43 | }); 44 | } 45 | else{ 46 | console.log(command); 47 | } 48 | } 49 | else if(autorun){ 50 | console.log("No undo command necessary"); 51 | } 52 | else{ 53 | console.log("No undo command known"); 54 | } 55 | }); 56 | foundGit = true; 57 | break; 58 | } 59 | }); 60 | process.stdin.on('end', function() { 61 | if(!foundGit){ 62 | console.log("I didn't find a git command"); 63 | } 64 | }); 65 | 66 | var getFileNames = function(cmd){ 67 | var parts = cmd.split(" "); 68 | for(var p = 0; p < parts.length; p++){ 69 | if(parts[p].indexOf("-") === 0){ 70 | parts[p] = ""; 71 | } 72 | } 73 | return parts; 74 | }; 75 | 76 | var undoCommand = function(cmd, callback){ 77 | try{ 78 | var info = null, 79 | undo = null, 80 | autorun = false, 81 | filenames, 82 | repo_name, 83 | repo_url, 84 | old_name, 85 | new_name, 86 | aliases; 87 | 88 | cmd = cmd.replace(/\r?\n|\r/g, ' ').replace(/\s\s+/g, ' ').replace(/\s$/, ''); 89 | 90 | // Using sync here; we won't be running this thousands of times 91 | // per second, so it's probably fine. Sync with no arguments 92 | // looks for a .gitconfig in the $HOME directory. 93 | aliases = gitConfig.sync().alias || {}; 94 | 95 | // Try replacing aliases with the expanded command. 96 | Object.keys(aliases).forEach(function (alias) { 97 | var command, 98 | replacement; 99 | 100 | command = 'git ' + alias; 101 | command = new RegExp(command + '\\s|' + command + '$'); 102 | replacement = 'git ' + aliases[alias]; 103 | 104 | // If the alias is present in the command and the alias maps 105 | // to multiple commands (e.g. git ca -> git add -A && git commit), 106 | // we don't do anything. 107 | if (command.test(cmd) && replacement.indexOf('&&') > -1) { 108 | info = 'Sorry, I don\'t know how to undo compound aliases.'; 109 | } else { 110 | cmd = cmd.replace(command, replacement); 111 | } 112 | }); 113 | 114 | if(cmd.indexOf('git init') > -1){ 115 | info = 'This created a .git folder in the current directory. You can remove it.'; 116 | undo = 'rm -rf .git'; 117 | autorun = true; 118 | } 119 | 120 | else if(cmd.indexOf('git clone') > -1){ 121 | var outputfolder = null; 122 | if (cmd.indexOf('git clone ') > -1) { 123 | var cloned_into = (cmd.split('git clone ')[1] || '').split(' '); 124 | if(cloned_into.length > 1){ 125 | // specified output folder 126 | outputfolder = cloned_into[1]; 127 | } 128 | else{ 129 | // default output folder 130 | // extract from remote - for example https://github.com/mapmeld/gitjk.git 131 | outputfolder = cloned_into[0].split("/"); 132 | outputfolder = outputfolder[outputfolder.length-1]; 133 | outputfolder = outputfolder.split('.git')[0]; 134 | } 135 | } 136 | else{ 137 | // default output folder 138 | // extract from remote - for example https://github.com/mapmeld/gitjk.git 139 | outputfolder = cloned_into[0].split("/"); 140 | outputfolder = outputfolder[outputfolder.length-1]; 141 | outputfolder = outputfolder.split('.git')[0]; 142 | } 143 | 144 | info = 'This downloaded a repo and all of its git history to a folder. You can remove it.'; 145 | if(outputfolder && outputfolder.length && outputfolder.indexOf("..") == -1){ 146 | undo = 'rm -rf ./' + outputfolder.replace(' ', '\\ '); 147 | autorun = true; 148 | } 149 | else{ 150 | info += "\nCouldn't figure out what folder this was downloaded to."; 151 | autorun = false; 152 | } 153 | } 154 | 155 | else if(cmd.indexOf('git add') > -1){ 156 | filenames = getFileNames(cmd.split('git add ')[1] || ''); 157 | info = 'This added files to the changes staged for commit. All changes to files will be removed from staging for this commit, but remain saved in the local file system.'; 158 | if(filenames.indexOf('.') > -1 || filenames.indexOf('*') > -1){ 159 | info += "\nUsing . or * affects all files, so you will need to run 'git reset ' on each file you didn't want to add."; 160 | autorun = false; 161 | } 162 | else{ 163 | undo = 'git reset ' + filenames.join(' '); 164 | autorun = true; 165 | } 166 | } 167 | 168 | else if(cmd.indexOf('git rm') > -1){ 169 | filenames = cmd.split('git rm ')[1] || ''; 170 | if(cmd.indexOf("--cached") > -1){ 171 | info = 'This took files out of the changes staged for commit. All changes will be re-added to staging for this commit.'; 172 | undo = 'git add ' + filenames.replace('--cached', ''); 173 | } 174 | else{ 175 | info = "Don't panic, but this deleted files from the file system. They're not in the recycle bin; they're gone. These files can be restored from your last commit, but uncommited changes were lost."; 176 | undo = 'git checkout HEAD ' + filenames; 177 | } 178 | autorun = true; 179 | } 180 | 181 | else if(cmd.indexOf('git mv') > -1){ 182 | old_name = (cmd.split('git mv ')[1] || '').split(' ')[0]; 183 | new_name = (cmd.split('git mv ')[1] || '').split(' ')[1]; 184 | info = 'This moved the file (named ' + old_name + ') to ' + new_name + '. It can be moved back.'; 185 | undo = 'git mv ' + new_name + ' ' + old_name; 186 | autorun = true; 187 | } 188 | 189 | else if(cmd.indexOf('git checkout') > -1){ 190 | if (cmd.split(/\s+/).length < 3) { 191 | info = 'git checkout command was invalid'; 192 | } else if ((cmd.split(/\s+/)[2] !== '-b') && (cmd.split(/\s+/).length !== 3 || fs.existsSync(cmd.split(/\s+/)[2]))) { 193 | info = 'Don\'t panic, but this overwrote any changes that you made to these files since your last commit. It isn\'t reversible using git.'; 194 | } else { 195 | // handling branch change 196 | info = 'git checkout moved you into a different branch of the repo. You can checkout any branch by name, or checkout the last one using -'; 197 | undo = 'git checkout -'; 198 | autorun = true; 199 | } 200 | } 201 | 202 | else if(cmd.indexOf('git remote add') > -1){ 203 | repo_name = (cmd.split('git remote add ')[1] || '').split(' ')[0]; 204 | repo_url = (cmd.split('git remote add ')[1] || '').split(' ')[1]; 205 | 206 | info = 'This added a remote repo (named ' + repo_name + ') pointing to ' + repo_url; 207 | info += "\nIt can be removed."; 208 | undo = 'git remote rm ' + repo_name; 209 | autorun = true; 210 | } 211 | 212 | else if(cmd.indexOf('git remote remove') > -1 || cmd.indexOf('git remote rm') > -1){ 213 | repo_name = (cmd.split('git remote ')[1] || '').split(' ')[1]; 214 | 215 | info = 'This removed a remote repo (named ' + repo_name + ')'; 216 | info += "\nIt needs to be added back using git remote add " + repo_name + " "; 217 | autorun = false; 218 | } 219 | 220 | else if(cmd.indexOf('git remote set-url') > -1){ 221 | repo_name = (cmd.split('git remote set-url ')[1] || '').split(' ')[0]; 222 | repo_url = (cmd.split('git remote set-url ')[1] || '').split(' ')[1] || ''; 223 | 224 | info = 'This changed the remote repo (named ' + repo_name + ') to point to ' + repo_url; 225 | info += "\nIt can be removed (using git remote rm) or set again (using git remote set-url)."; 226 | autorun = false; 227 | } 228 | 229 | else if(cmd.indexOf('git remote rename') > -1){ 230 | old_name = (cmd.split('git remote rename ')[1] || '').split(' ')[0]; 231 | new_name = (cmd.split('git remote rename ')[1] || '').split(' ')[1] || ''; 232 | info = 'This changed the remote repo (named ' + old_name + ') to have the name ' + new_name + '. It can be reset.'; 233 | undo = 'git remote rename ' + new_name + ' ' + old_name; 234 | autorun = true; 235 | } 236 | 237 | else if(cmd.indexOf('git commit') > -1){ 238 | info = 'This saved your staged changes as a commit, which can be updated with git commit --amend or completely uncommited:'; 239 | undo = "git reset --soft 'HEAD^'"; 240 | } 241 | 242 | else if(cmd.indexOf('git revert') > -1){ 243 | info = 'This made a new commit to retract a commit. You can undo *the revert commit* using a more extreme approach:'; 244 | undo = "git reset --soft 'HEAD^'"; 245 | } 246 | 247 | else if(cmd.indexOf('git fetch') > -1){ 248 | info = 'This updated the local copy of all branches in this repo. Un-updating master (and you can do other branches, too).'; 249 | undo = 'git update-ref refs/remotes/origin/master refs/remotes/origin/master@{1}'; 250 | autorun = true; 251 | } 252 | 253 | else if(cmd.indexOf('git pull') > -1 || cmd.indexOf('git merge') > -1){ 254 | info = 'This merged another branch (local or remote) into your current branch. You want to reset to a specific previous state.'; 255 | undo = '"git reset --hard [commit-hash]" will set you to previous commit, "git reset --hard origin/" will return you to the state of the remote named "origin."'; 256 | autorun = false; 257 | } 258 | 259 | else if(cmd.indexOf('git push') > -1){ 260 | autorun = false; 261 | info = 'This uploaded all of your committed changes to a remote repo. It may be difficult to reverse it.'; 262 | info += '\nYou can use git revert to tell repos to turn back these commits.'; 263 | info += '\nThere is git checkout and git push --force, but this will mess up others\' git history!'; 264 | if(cmd.indexOf('git push heroku') > -1){ 265 | info += '\nIf you are hosting this app on Heroku, run "heroku rollback" to reset your app now.'; 266 | } 267 | } 268 | 269 | else if(cmd.indexOf('git branch') > -1){ 270 | autorun = true; 271 | if(cmd.indexOf(' -D') > -1){ 272 | // delete branch 273 | info = 'You deleted a branch. You can use "git branch" to create it again, or "git pull" to restore it from a remote repo.'; 274 | autorun = false; 275 | } 276 | else if(cmd.indexOf('git branch ') > -1){ 277 | // create branch 278 | var branch_name = (cmd.split('git branch ')[1] || '').split(' ')[0]; 279 | if(branch_name.length && branch_name[0] != '-'){ 280 | info = 'You created a new branch named ' + branch_name + '. You can delete it:'; 281 | undo = 'git branch -D ' + branch_name; 282 | } 283 | } 284 | if(!info){ 285 | // must have listed branches 286 | info = "git branch on its own doesn't change the repo; it just lists all branches. Use it often!"; 287 | } 288 | } 289 | 290 | else if(cmd.indexOf('git stash') > -1){ 291 | if(cmd.indexOf('stash list') > -1){ 292 | info = "git stash list doesn't change the repo; it just tells you the stashed changes which you can restore using git stash apply."; 293 | autorun = true; 294 | } 295 | else if(cmd.indexOf('stash pop') > -1 || cmd.indexOf('stash apply') > -1){ 296 | info = 'You restored changes from the stash. You can stash specific changes again using git stash.'; 297 | autorun = false; 298 | } 299 | else{ 300 | info = 'You stashed any changes which were not yet commited. Restore the latest stash using:'; 301 | undo = 'git stash apply'; 302 | autorun = true; 303 | } 304 | } 305 | 306 | else if(cmd.indexOf('git archive') > -1){ 307 | info = 'This created an archive of part of the repo - you can delete it using "rm -rf ".'; 308 | autorun = false; 309 | } 310 | 311 | // harmless 312 | 313 | else if(cmd.indexOf('git cat-file') > -1){ 314 | info = "git cat-file doesn't change the repo; it just tells you the type of an object in the repo."; 315 | autorun = true; 316 | } 317 | else if(cmd.indexOf('git diff') > -1){ 318 | info = "git diff doesn't change the repo; it just tells you the changes waiting for commit OR the changes between branches. Use it often!"; 319 | autorun = true; 320 | } 321 | else if(cmd.indexOf('git grep') > -1){ 322 | info = "git grep doesn't change the repo; it's a search tool. Use grep and git grep often!"; 323 | autorun = true; 324 | } 325 | else if(cmd.indexOf('git ls-tree') > -1){ 326 | info = "git ls-tree doesn't change the repo; it just tells you about an object in the git repo."; 327 | autorun = true; 328 | } 329 | else if(cmd.indexOf('git show') > -1){ 330 | info = "git show doesn't change the repo; it just tells you the changes waiting for commit OR the changes between branches."; 331 | autorun = true; 332 | } 333 | else if(cmd.indexOf('git log') > -1){ 334 | info = "git log doesn't change the repo; it just lists the last several commits in this branch. Use it often!"; 335 | autorun = true; 336 | } 337 | else if(cmd.indexOf('git status') > -1){ 338 | info = "git status doesn't change the repo; it just tells you what changes there are. Use it often!"; 339 | autorun = true; 340 | } 341 | else if(cmd.indexOf('git remote') > -1){ 342 | info = "git remote (without additional arguments) doesn't change the repo; it just tells you what remotes there are. Use it often!"; 343 | autorun = true; 344 | } 345 | 346 | callback(null, info, undo, autorun); 347 | } 348 | catch(e){ 349 | callback(e, null, null, false); 350 | } 351 | }; 352 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var assert = require('chai').assert; 3 | 4 | describe('git init', function(){ 5 | it('should remove the .git directory', function(done){ 6 | exec('echo "git init\n" | ./index.js', function(err, response){ 7 | if(err){ 8 | throw err; 9 | } 10 | assert.include(response, 'rm -rf .git'); 11 | done(); 12 | }); 13 | }); 14 | }); 15 | 16 | describe('git clone', function(){ 17 | it('should remove the default download directory', function(done){ 18 | exec('echo "git clone git@github.com:mapmeld/gitjk.git\n" | ./index.js', function(err, response){ 19 | if(err){ 20 | throw err; 21 | } 22 | assert.include(response, 'rm -rf ./gitjk'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should remove a custom download directory', function(done){ 28 | exec('echo "git clone git@github.com:mapmeld/gitjk.git test_gitjk\n" | ./index.js', function(err, response){ 29 | if(err){ 30 | throw err; 31 | } 32 | assert.include(response, 'rm -rf ./test_gitjk'); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | 38 | describe('git add', function(){ 39 | it('should reset a previously-indexed file', function(done){ 40 | exec('echo "git add package.json\n" | ./index.js', function(err, response){ 41 | if(err){ 42 | throw err; 43 | } 44 | assert.include(response, 'git reset package.json'); 45 | done(); 46 | }); 47 | }); 48 | 49 | it('should warn instead of doing git reset . or git reset *', function(done){ 50 | exec('echo "git add .\n" | ./index.js', function(err, response){ 51 | if(err){ 52 | throw err; 53 | } 54 | assert.include(response, 'Using . or * affects all files'); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('git rm', function(){ 61 | it('should re-index a cached/removed file', function(done){ 62 | exec('echo "git rm package.json --cached\n" | ./index.js', function(err, response){ 63 | if(err){ 64 | throw err; 65 | } 66 | assert.include(response, 'git add package.json'); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should un-delete a deleted file', function(done){ 72 | exec('echo "git rm package.json\n" | ./index.js', function(err, response){ 73 | if(err){ 74 | throw err; 75 | } 76 | assert.include(response, 'git checkout HEAD package.json'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('should avoid saying -undefined-', function(done){ 82 | exec('echo "git rm" | ./index.js', function(err, response){ 83 | if(err){ 84 | throw err; 85 | } 86 | assert.notInclude(response, 'undefined'); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('git mv', function(){ 93 | it('should move the file back', function(done){ 94 | exec('echo "git mv package.json p.json\n" | ./index.js', function(err, response){ 95 | if(err){ 96 | throw err; 97 | } 98 | assert.include(response, 'git mv p.json package.json'); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('git checkout', function(){ 105 | it('should checkout back to the previous directory', function(done){ 106 | exec('echo "git checkout bogus\n" | ./index.js', function(err, response){ 107 | if(err){ 108 | throw err; 109 | } 110 | assert.include(response, 'git checkout -'); 111 | done(); 112 | }); 113 | }); 114 | 115 | it('should checkout back to the previous directory', function(done){ 116 | exec('echo "git checkout -b created\n" | ./index.js', function(err, response){ 117 | if(err){ 118 | throw err; 119 | } 120 | assert.include(response, 'git checkout -'); 121 | done(); 122 | }); 123 | }); 124 | 125 | it('should warn you can\t undo', function(done){ 126 | exec('echo "git checkout package.json\n" | ./index.js', function(err, response){ 127 | if(err){ 128 | throw err; 129 | } 130 | assert.include(response, 'panic'); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | 136 | describe('git remote', function(){ 137 | it('should remove a remote add', function(done){ 138 | exec('echo "git remote add github https://github.com\n" | ./index.js', function(err, response){ 139 | if(err){ 140 | throw err; 141 | } 142 | assert.include(response, 'git remote rm github'); 143 | done(); 144 | }); 145 | }); 146 | 147 | it('should warn a remote remove', function(done){ 148 | exec('echo "git remote remove github\n" | ./index.js', function(err, response){ 149 | if(err){ 150 | throw err; 151 | } 152 | assert.include(response, 'git remote add github'); 153 | done(); 154 | }); 155 | }); 156 | 157 | it('should warn a remote rm', function(done){ 158 | exec('echo "git remote rm github\n" | ./index.js', function(err, response){ 159 | if(err){ 160 | throw err; 161 | } 162 | assert.include(response, 'git remote add github'); 163 | done(); 164 | }); 165 | }); 166 | 167 | it('should swap names in a remote rename', function(done){ 168 | exec('echo "git remote rename github banana\n" | ./index.js', function(err, response){ 169 | if(err){ 170 | throw err; 171 | } 172 | assert.include(response, 'git remote rename banana github'); 173 | done(); 174 | }); 175 | }); 176 | 177 | it('does nothing without args', function(done){ 178 | exec('echo "git remote\n" | ./index.js', function(err, response){ 179 | if(err){ 180 | throw err; 181 | } 182 | assert.include(response, "doesn't change the repo"); 183 | done(); 184 | }); 185 | }); 186 | }); 187 | 188 | describe('git commit', function(){ 189 | it('should unseal a commit', function(done){ 190 | exec('echo "git commit\n" | ./index.js', function(err, response){ 191 | if(err){ 192 | throw err; 193 | } 194 | assert.include(response, "git reset --soft 'HEAD^'"); 195 | done(); 196 | }); 197 | }); 198 | }); 199 | 200 | describe('git revert', function(){ 201 | it('should unseal the git revert commit', function(done){ 202 | exec('echo "git revert 0ee030\n" | ./index.js', function(err, response){ 203 | if(err){ 204 | throw err; 205 | } 206 | assert.include(response, "git reset --soft 'HEAD^'"); 207 | done(); 208 | }); 209 | }); 210 | }); 211 | 212 | describe('git fetch', function(){ 213 | it('should un-update master branch', function(done){ 214 | exec('echo "git fetch\n" | ./index.js', function(err, response){ 215 | if(err){ 216 | throw err; 217 | } 218 | assert.include(response, "git update-ref refs/remotes/origin/master refs/remotes/origin/master@{1}"); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('git pull', function(){ 225 | it('should do a reset after git pull', function(done){ 226 | exec('echo "git pull origin master\n" | ./index.js', function(err, response){ 227 | if(err){ 228 | throw err; 229 | } 230 | assert.include(response, "git reset --hard"); 231 | done(); 232 | }); 233 | }); 234 | }); 235 | 236 | describe('git merge', function(){ 237 | it('should do a reset after git merge', function(done){ 238 | exec('echo "git merge merged\n" | ./index.js', function(err, response){ 239 | if(err){ 240 | throw err; 241 | } 242 | assert.include(response, "git reset --hard"); 243 | done(); 244 | }); 245 | }); 246 | }); 247 | 248 | describe('git archive', function(){ 249 | it('should tell user to remove archive', function(done){ 250 | exec('echo "git archive HEAD > sample.zip\n" | ./index.js', function(err, response){ 251 | if(err){ 252 | throw err; 253 | } 254 | assert.include(response, "rm -rf"); 255 | done(); 256 | }); 257 | }); 258 | }); 259 | 260 | describe('git stash', function(){ 261 | it('should tell user when they stashed changes', function(done){ 262 | exec('echo "git stash\n" | ./index.js', function(err, response){ 263 | if(err){ 264 | throw err; 265 | } 266 | assert.include(response, "git stash apply"); 267 | done(); 268 | }); 269 | }); 270 | 271 | it('should tell user when they un-stashed changes', function(done){ 272 | exec('echo "git stash apply\n" | ./index.js', function(err, response){ 273 | if(err){ 274 | throw err; 275 | } 276 | assert.include(response, "git stash"); 277 | done(); 278 | }); 279 | }); 280 | 281 | it('should tell user when they un-stashed changes', function(done){ 282 | exec('echo "git stash pop\n" | ./index.js', function(err, response){ 283 | if(err){ 284 | throw err; 285 | } 286 | assert.include(response, "git stash"); 287 | done(); 288 | }); 289 | }); 290 | 291 | it('should tell user when they listed stashes', function(done){ 292 | exec('echo "git stash list\n" | ./index.js', function(err, response){ 293 | if(err){ 294 | throw err; 295 | } 296 | assert.include(response, "doesn't change the repo"); 297 | done(); 298 | }); 299 | }); 300 | }); 301 | 302 | describe('git branch', function(){ 303 | it('should tell user when they added a branch', function(done){ 304 | exec('echo "git branch banana\n" | ./index.js', function(err, response){ 305 | if(err){ 306 | throw err; 307 | } 308 | assert.include(response, "git branch -D banana"); 309 | done(); 310 | }); 311 | }); 312 | 313 | it('should tell user when they deleted a branch', function(done){ 314 | exec('echo "git branch -D banana\n" | ./index.js', function(err, response){ 315 | if(err){ 316 | throw err; 317 | } 318 | assert.include(response, "git branch"); 319 | assert.include(response, "git pull"); 320 | done(); 321 | }); 322 | }); 323 | 324 | it('should tell user when they listed branches', function(done){ 325 | exec('echo "git branch -a\n" | ./index.js', function(err, response){ 326 | if(err){ 327 | throw err; 328 | } 329 | assert.include(response, "doesn't change the repo"); 330 | done(); 331 | }); 332 | }); 333 | }); 334 | 335 | describe('git push', function(){ 336 | it('should not fix a git push', function(done){ 337 | exec('echo "git push origin master\n" | ./index.js', function(err, response){ 338 | if(err){ 339 | throw err; 340 | } 341 | assert.include(response, "This uploaded all of your committed changes to a remote repo."); 342 | done(); 343 | }); 344 | }); 345 | 346 | it('should help fix a push to heroku', function(done){ 347 | exec('echo "git push heroku master\n" | ./index.js', function(err, response){ 348 | if(err){ 349 | throw err; 350 | } 351 | assert.include(response, "heroku rollback"); 352 | done(); 353 | }); 354 | }); 355 | }); 356 | 357 | describe('reassure user on do-nothing commands', function(){ 358 | it('git status', function(done){ 359 | exec('echo "git status\n" | ./index.js', function(err, response){ 360 | if(err){ 361 | throw err; 362 | } 363 | assert.include(response, "doesn't change the repo"); 364 | done(); 365 | }); 366 | }); 367 | 368 | it('git cat-file', function(done){ 369 | exec('echo "git cat-file 0ee030\n" | ./index.js', function(err, response){ 370 | if(err){ 371 | throw err; 372 | } 373 | assert.include(response, "doesn't change the repo"); 374 | done(); 375 | }); 376 | }); 377 | 378 | it('git diff', function(done){ 379 | exec('echo "git diff\n" | ./index.js', function(err, response){ 380 | if(err){ 381 | throw err; 382 | } 383 | assert.include(response, "doesn't change the repo"); 384 | done(); 385 | }); 386 | }); 387 | 388 | it('git show', function(done){ 389 | exec('echo "git show\n" | ./index.js', function(err, response){ 390 | if(err){ 391 | throw err; 392 | } 393 | assert.include(response, "doesn't change the repo"); 394 | done(); 395 | }); 396 | }); 397 | 398 | it('git log', function(done){ 399 | exec('echo "git log\n" | ./index.js', function(err, response){ 400 | if(err){ 401 | throw err; 402 | } 403 | assert.include(response, "doesn't change the repo"); 404 | done(); 405 | }); 406 | }); 407 | 408 | it('git ls-tree', function(done){ 409 | exec('echo "git ls-tree 0ee030\n" | ./index.js', function(err, response){ 410 | if(err){ 411 | throw err; 412 | } 413 | assert.include(response, "doesn't change the repo"); 414 | done(); 415 | }); 416 | }); 417 | 418 | it('git grep', function(done){ 419 | exec('echo "git grep\n" | ./index.js', function(err, response){ 420 | if(err){ 421 | throw err; 422 | } 423 | assert.include(response, "doesn't change the repo"); 424 | done(); 425 | }); 426 | }); 427 | }); 428 | 429 | describe('unknown command', function(){ 430 | it('should print an error message with unknown command', function(done){ 431 | exec('echo "git blog\n" | ./index.js', function(err, response){ 432 | if(err){ 433 | throw err; 434 | } 435 | assert.include(response, "I didn't recognize that command"); 436 | done(); 437 | }); 438 | }); 439 | 440 | it('should print an error message without git', function(done){ 441 | exec('echo "ls\n" | ./index.js', function(err, response){ 442 | if(err){ 443 | throw err; 444 | } 445 | assert.include(response, "I didn't find a git command"); 446 | done(); 447 | }); 448 | }); 449 | }); 450 | 451 | describe('aliases', function() { 452 | var renamedConfig = false, 453 | fs = require('fs'), 454 | path = require('path'), 455 | oldPath = path.join(process.env.HOME, '.gitconfig'), 456 | tempPath = path.join(process.env.HOME, '.GITJKTEMP'); 457 | 458 | var newConfig = 459 | '[alias]\n' + 460 | ' st = status\n' + 461 | ' cm = commit\n' + 462 | ' ac = !git add -A && git commit\n'; 463 | 464 | // Create a gitconfig with some aliases before tests run. 465 | before(function (done) { 466 | fs.rename(oldPath, tempPath, function (err) { 467 | 468 | // No existing gitconfig. 469 | if (err) renamedConfig = false; 470 | 471 | fs.writeFile(oldPath, newConfig, function (err) { 472 | 473 | // If something went wrong, try restoring the gitconfig. 474 | if (err) afterTests(function () {}); 475 | }); 476 | 477 | done(); 478 | }); 479 | }); 480 | 481 | function afterTests (done) { 482 | fs.unlink(oldPath, function (err) { 483 | 484 | if (!renamedConfig) return done(); 485 | 486 | fs.rename(tempPath, oldPath, function (err) { 487 | if (err) throw err; 488 | 489 | done(); 490 | }); 491 | }); 492 | } 493 | 494 | after(afterTests); 495 | 496 | it('should properly handle simple aliases (1)', function (done) { 497 | exec('echo "git st\n" | ./index.js', function(err, response){ 498 | if(err) throw err; 499 | 500 | assert.include(response, "doesn't change the repo"); 501 | done(); 502 | }); 503 | }); 504 | 505 | it('should properly handle simple aliases (2)', function (done) { 506 | exec('echo "git cm\n" | ./index.js', function(err, response){ 507 | if(err) throw err; 508 | 509 | assert.include(response, "git reset --soft 'HEAD^'"); 510 | done(); 511 | }); 512 | }); 513 | 514 | it('should inform the user it cannot handle compound aliases.', function (done) { 515 | exec('echo "git ac\n" | ./index.js', function(err, response){ 516 | if(err) throw err; 517 | 518 | assert.include(response, 'No undo command known'); 519 | done(); 520 | }); 521 | }); 522 | }); 523 | --------------------------------------------------------------------------------