├── .gitignore ├── .prettierignore ├── git-pretty.gif ├── .prettierrc.js ├── eslint.config.js ├── LICENSE.md ├── package.json ├── README.md └── git-pretty.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /git-pretty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/draperunner/git-pretty/master/git-pretty.gif -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | trailingComma: 'all', 3 | tabWidth: 4, 4 | semi: false, 5 | singleQuote: true, 6 | } 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import { defineConfig } from 'eslint/config' 4 | 5 | export default defineConfig([ 6 | { 7 | files: ['**/*.{js,mjs,cjs}'], 8 | plugins: { js }, 9 | extends: ['js/recommended'], 10 | languageOptions: { globals: globals.node }, 11 | }, 12 | ]) 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mats Byrkjeland 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-pretty", 3 | "version": "4.0.0", 4 | "description": "An implementation of Justin Hileman's chart from \"Changing History, or How to Git Pretty\"", 5 | "main": "git-pretty/git-pretty.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node git-pretty.js", 9 | "test": "prettier . --check", 10 | "lint": "eslint git-pretty.js", 11 | "format": "prettier . --write" 12 | }, 13 | "bin": { 14 | "git-pretty": "git-pretty.js" 15 | }, 16 | "files": [ 17 | "git-pretty.js" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/draperunner/git-pretty.git" 22 | }, 23 | "author": "draperunner", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/draperunner/git-pretty/issues" 27 | }, 28 | "homepage": "https://github.com/draperunner/git-pretty#readme", 29 | "dependencies": { 30 | "@inquirer/prompts": "^7.8.4", 31 | "chalk": "^5.6.0" 32 | }, 33 | "devDependencies": { 34 | "@eslint/js": "^9.35.0", 35 | "eslint": "^9.35.0", 36 | "globals": "^16.3.0", 37 | "prettier": "^3.6.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-pretty 2 | 3 | An implementation of Justin Hileman's handy chart for finding the suitable git operation. 4 | 5 | … with some additions! 6 | 7 | ![git-pretty GIF](git-pretty.gif) 8 | 9 | ## Use 10 | 11 | Just run 12 | 13 | ```bash 14 | npx git-pretty 15 | ``` 16 | 17 | This opens an interactive session like this: 18 | 19 | ```bash 20 | So you have a mess on your hands. What sort of mess? 21 | 22 | 1: An uncommitted mess 23 | 2: I accidentally committed something 24 | 3: My Git history is ugly 25 | 4: I have a bunch of old branches I want gone 26 | 5: I want to sync my fork with the original repo 27 | > 28 | ``` 29 | 30 | ## Install 31 | 32 | If you need this kind of help often, you could install git-pretty globally: 33 | 34 | ``` 35 | npm i -g git-pretty 36 | ``` 37 | 38 | Then you can drop `npx` and run 39 | 40 | ``` 41 | git-pretty 42 | ``` 43 | 44 | Using `npx` is recommended though, because it always uses the latest version. 45 | 46 | ## Node JS? 47 | 48 | > Wasn't this a Python package, installable through `pip`? 49 | 50 | Yes it was! But now it's a Node package, installable through `npm`. Ah, how things change through life. 51 | -------------------------------------------------------------------------------- /git-pretty.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { select } from '@inquirer/prompts' 3 | import chalk from 'chalk' 4 | 5 | async function ask(question, choices) { 6 | return await select({ 7 | message: question, 8 | choices: choices.map((name, index) => ({ 9 | name, 10 | value: `${index + 1}`, 11 | })), 12 | }) 13 | } 14 | 15 | function askYesNo(question) { 16 | return ask(question, ['Yes', 'No']) 17 | } 18 | 19 | function printCode(code) { 20 | console.log(chalk.bgGreenBright(chalk.black(` ${code} `))) 21 | } 22 | 23 | function dangerZone() { 24 | console.log('\n' + chalk.bgRed('Welcome to the DANGER ZONE!') + '\n') 25 | } 26 | 27 | function interactiveRebase() { 28 | dangerZone() 29 | console.log( 30 | `We're going to do an ${chalk.bold('** interactive rebase! **')}`, 31 | ) 32 | printCode('git rebase -i {COMMITISH}') 33 | console.log() 34 | console.log("And when that's done, do this:") 35 | console.log() 36 | printCode('git push --force origin {branch}') 37 | console.log() 38 | } 39 | 40 | async function main() { 41 | let answer = await ask( 42 | 'So you have a mess on your hands. What sort of mess?', 43 | [ 44 | 'An uncommitted mess', 45 | 'I accidentally committed something', 46 | 'My Git history is ugly', 47 | 'I have a bunch of old branches I want gone', 48 | 'I want to sync my fork with the original repo', 49 | 'I want my branch to be exactly like on GitHub!', 50 | ], 51 | ) 52 | 53 | if (answer === '1') { 54 | answer = await askYesNo( 55 | 'Do you care enough about your mess to keep it?', 56 | ) 57 | 58 | if (answer === '1') { 59 | console.log('Looks like we caught this just in time.') 60 | console.log( 61 | 'Split off a logical chunk from your mess, stage it and commit it with a good message.', 62 | ) 63 | console.log('Still have a mess? Do it again.') 64 | return '11' 65 | } 66 | 67 | if (answer === '2') { 68 | console.log('Looks like this is what you are looking for:') 69 | console.log() 70 | printCode('git reset --hard') 71 | console.log() 72 | return '12' 73 | } 74 | } 75 | 76 | if (answer === '2') { 77 | answer = await askYesNo('Has anyone else seen it?') 78 | 79 | if (answer === '1') { 80 | console.log('Looks like this is what you are looking for:') 81 | console.log() 82 | printCode('git revert {COMMITISH}') 83 | console.log() 84 | return '21' 85 | } 86 | 87 | if (answer === '2') { 88 | answer = await ask('How long ago?', [ 89 | 'Last commit', 90 | 'It seems like forever ago', 91 | ]) 92 | 93 | if (answer === '1') { 94 | answer = await ask('What would make this better?', [ 95 | 'I forgot to add a file', 96 | 'A better message', 97 | 'Remove the last commit, but keep the changes', 98 | 'Throw the last commit away, and delete its changes', 99 | ]) 100 | 101 | if (answer === '1') { 102 | console.log('Looks like this is what you are looking for:') 103 | console.log() 104 | printCode('git add {my_awesome_file}') 105 | printCode('git commit --amend') 106 | console.log() 107 | return '2211' 108 | } 109 | 110 | if (answer === '2') { 111 | console.log('Looks like this is what you are looking for:') 112 | console.log() 113 | printCode('git commit --amend') 114 | console.log() 115 | return '2212' 116 | } 117 | 118 | if (answer === '3') { 119 | console.log('Looks like this is what you are looking for:') 120 | console.log() 121 | printCode('git reset HEAD~') 122 | console.log() 123 | return '2213' 124 | } 125 | 126 | if (answer === '4') { 127 | console.log('Looks like this is what you are looking for:') 128 | console.log() 129 | printCode('git reset --hard HEAD^') 130 | console.log() 131 | return '2214' 132 | } 133 | } 134 | 135 | if (answer === '2') { 136 | answer = await askYesNo('Take a mulligan?') 137 | 138 | if (answer === '1') { 139 | console.log("We'll reset and commit from scratch:") 140 | console.log() 141 | printCode('git reset {COMMITISH}') 142 | console.log() 143 | console.log( 144 | 'Then split off a logical chunk from your mess, stage it and commit it with a good message.', 145 | ) 146 | console.log('Still have a mess? Do it again.') 147 | return '2221' 148 | } 149 | 150 | if (answer === '2') { 151 | interactiveRebase() 152 | return '2222' 153 | } 154 | } 155 | } 156 | } 157 | 158 | if (answer === '3') { 159 | answer = await askYesNo('Is it already on GitHub?') 160 | 161 | if (answer === '1') { 162 | answer = await askYesNo('Is anyone down stream?') 163 | 164 | if (answer === '1') { 165 | answer = await askYesNo('Enough to form a lynch mob?') 166 | 167 | if (answer === '1') { 168 | console.log("It's safest to let it stay ugly then") 169 | return '3111' 170 | } 171 | 172 | if (answer === '2') { 173 | answer = await askYesNo('Do you hate them?') 174 | 175 | if (answer === '1') { 176 | interactiveRebase() 177 | return '31121' 178 | } 179 | 180 | if (answer === '2') { 181 | console.log( 182 | "Send them a note, let 'em know you're changing history.", 183 | ) 184 | console.log() 185 | interactiveRebase() 186 | return '31122' 187 | } 188 | } 189 | } 190 | 191 | if (answer === '2') { 192 | interactiveRebase() 193 | return '312' 194 | } 195 | } 196 | 197 | if (answer === '2') { 198 | answer = await ask('Should we remove merge conflicts?', [ 199 | 'That would do the trick', 200 | 'No, I need to change history!', 201 | ]) 202 | 203 | if (answer === '1') { 204 | console.log('Looks like this is what you are looking for:') 205 | console.log() 206 | printCode('git rebase origin/{branch}') 207 | console.log() 208 | return '321' 209 | } 210 | 211 | if (answer === '2') { 212 | interactiveRebase() 213 | return '322' 214 | } 215 | } 216 | } 217 | 218 | if (answer === '4') { 219 | dangerZone() 220 | console.log( 221 | 'To delete all local branches that are already merged into the currently checked out branch:', 222 | ) 223 | printCode( 224 | "git branch --merged | egrep -v '(^\\*|master|main|dev)' | xargs git branch -d", 225 | ) 226 | console.log() 227 | console.log( 228 | 'You can see that master and dev are excluded in case they are an ancestor.', 229 | ) 230 | console.log( 231 | 'Check out https://stackoverflow.com/questions/6127328/how-can-i-delete-all-git-branches-which-have-been-merged', 232 | ) 233 | return '4' 234 | } 235 | 236 | if (answer === '5') { 237 | console.log( 238 | "Alright! You have forked a repo a while ago, and now it's time to update it with the changes in the original repo.", 239 | ) 240 | console.log() 241 | 242 | const answer = await ask( 243 | 'Have you already configured the original repo as a remote?', 244 | ['No', 'Yes', 'Not sure'], 245 | ) 246 | 247 | if (answer === '1') { 248 | console.log("Let's set up a remote.") 249 | printCode( 250 | 'git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git', 251 | ) 252 | console.log() 253 | console.log('Verify that it was added by listing all remotes:') 254 | printCode('git remote -v') 255 | console.log() 256 | console.log('Fetch the changes from the remote:') 257 | printCode('git fetch upstream') 258 | console.log() 259 | console.log('Now checkout your local master branch') 260 | printCode('git checkout master') 261 | console.log() 262 | console.log( 263 | 'Merge the changes from the remote into your local branch', 264 | ) 265 | printCode('git merge upstream/master') 266 | console.log() 267 | console.log( 268 | "Read GitHub's guide for this if you don't trust git-pretty: https://help.github.com/en/articles/syncing-a-fork", 269 | ) 270 | return '52' 271 | } 272 | 273 | if (answer === '2') { 274 | console.log( 275 | 'Ok then. I\'ll assume the name of your remote for the original repo is "upstream"', 276 | ) 277 | console.log() 278 | console.log('Fetch the changes from the remote:') 279 | printCode('git fetch upstream') 280 | console.log() 281 | console.log('Now checkout your local master branch') 282 | printCode('git checkout master') 283 | console.log() 284 | console.log( 285 | 'Merge the changes from the remote into your local branch', 286 | ) 287 | printCode('git merge upstream/master') 288 | console.log() 289 | console.log( 290 | "Read GitHub's guide for this if you don't trust git-pretty: https://help.github.com/en/articles/syncing-a-fork", 291 | ) 292 | return '51' 293 | } 294 | 295 | if (answer === '3') { 296 | console.log('Not sure? Do this:') 297 | printCode('git remote -v') 298 | console.log( 299 | 'If the original repo is in that list, that means the remote is configured.', 300 | ) 301 | return '53' 302 | } 303 | } 304 | 305 | if (answer === '6') { 306 | console.log( 307 | 'Here is what you need. if master is not your desired branch, replace it in the code below with the desired branch name.', 308 | ) 309 | console.log( 310 | "Make sure you don't have any local changes you want to keep!", 311 | ) 312 | dangerZone() 313 | printCode('git fetch') 314 | printCode('git reset --hard origin/master') 315 | return '6' 316 | } 317 | } 318 | 319 | main() 320 | --------------------------------------------------------------------------------