├── .gitignore ├── img ├── after.png └── before.png ├── LICENSE ├── package.json ├── README.md └── bin └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /img/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trambarhq/git-thanos/HEAD/img/after.png -------------------------------------------------------------------------------- /img/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trambarhq/git-thanos/HEAD/img/before.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-thanos", 3 | "version": "0.0.2", 4 | "description": "Rebalance your open-source project with a snap of your finger", 5 | "bin": "bin/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:trambarhq/git-thanos.git" 12 | }, 13 | "author": "Chung Leong", 14 | "homepage": "https://github.com/trambarhq/git-thanos", 15 | "bugs": { 16 | "url": "http://github.com/trambarhq/git-thanos/issues" 17 | }, 18 | "license": "WTFPL", 19 | "keywords": [ 20 | "git", 21 | "balance", 22 | "thanos", 23 | "mercy" 24 | ], 25 | "tags": [ 26 | "git", 27 | "mcu" 28 | ], 29 | "dependencies": { 30 | "command-line-args": "^4.0.7", 31 | "command-line-usage": "^4.0.1", 32 | "tmp": "0.0.33" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | git-thanos 2 | ========== 3 | Like most OSS projects yours was allowed to grow unchecked. Too many contributors, not enough time to review their patches. Now your project is on the brink of collapse. How do you bring balance back to it? 4 | 5 | git-thanos offers a simple solution. With just a single command half your contributors would cease to exist. At random. Dispassionate, fair. Gurus and noobs alike. 6 | 7 | Prerequisites 8 | ------------- 9 | The hardest choices require the strongest wills. 10 | 11 | Installing 12 | ---------- 13 | `npm install -g git-thanos` 14 | 15 | Usage 16 | ----- 17 | To rebalance your project, cd to the git working directory and run `git-thanos`. The process might take a long time. Afterward, you finally rest. And watch git outputs the shortlog of a grateful repository: 18 | 19 | ![Before](img/before.png) ![After](img/after.png) 20 | 21 | Depending on your hardware, it might be necessary to enable certain experimental features. To see a complete list, please run `git-thanos -h`. 22 | 23 | License 24 | ------- 25 | This project is licensed under the WTFPL License - see [LICENSE](LICENSE) for details. 26 | 27 | Acknowledgments 28 | --------------- 29 | Special hat tip to Eitri and the rest of the crew at Nidavellir. 30 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var FS = require('fs'); 4 | var Path = require('path'); 5 | var ChildProcess = require('child_process'); 6 | var CommandLineArgs = require('command-line-args'); 7 | var CommandLineUsage = require('command-line-usage'); 8 | var Tmp = require('tmp'); 9 | 10 | var optionDefinitions = [ 11 | { 12 | name: 'help', 13 | alias: 'h', 14 | type: Boolean, 15 | description: 'Print this usage guide' 16 | }, 17 | { 18 | name: 'version', 19 | alias: 'v', 20 | type: Boolean, 21 | description: 'Show version number' 22 | }, 23 | { 24 | name: 'enable-inf-stone-mind', 25 | type: Boolean, 26 | description: `Enable Mind Stone hardware optimizations`, 27 | experimental: true, 28 | }, 29 | { 30 | name: 'enable-inf-stone-power', 31 | type: Boolean, 32 | description: `Enable Power Stone hardware optimizations`, 33 | experimental: true, 34 | }, 35 | { 36 | name: 'enable-inf-stone-reality', 37 | type: Boolean, 38 | description: `Enable Reality Stone hardware optimizations`, 39 | experimental: true, 40 | }, 41 | { 42 | name: 'enable-inf-stone-soul', 43 | type: Boolean, 44 | description: `Enable Soul Stone hardware optimizations`, 45 | experimental: true, 46 | }, 47 | { 48 | name: 'enable-inf-stone-space', 49 | type: Boolean, 50 | description: `Enable Space Stone hardware optimizations`, 51 | experimental: true, 52 | }, 53 | { 54 | name: 'enable-inf-stone-time', 55 | type: Boolean, 56 | description: `Enable Time Stone hardware optimizations`, 57 | experimental: true, 58 | }, 59 | ]; 60 | var scriptDescription = [ 61 | { 62 | header: 'Git-Thanos', 63 | content: 'Rebalance your open-source project with a snap of your finger' 64 | }, 65 | { 66 | header: 'Options', 67 | optionList: optionDefinitions.filter((def) => { 68 | return !def.defaultOption && !def.experimental; 69 | }) 70 | }, 71 | { 72 | header: 'Options (experimental)', 73 | optionList: optionDefinitions.filter((def) => { 74 | return !def.defaultOption && def.experimental; 75 | }) 76 | }, 77 | ]; 78 | 79 | try { 80 | var options = CommandLineArgs(optionDefinitions); 81 | if (options.help) { 82 | printUsage(); 83 | } else if (options.version) { 84 | printVersion(); 85 | } else { 86 | checkWorkingDirectory(); 87 | checkHardwareSupport(options); 88 | checkHardwareLocations(); 89 | 90 | var authors = getCommitAuthors(); 91 | var authorsChosen = performRandomFairSelection(authors); 92 | applyDispassionateMercy(authorsChosen); 93 | } 94 | } catch (err) { 95 | console.log('Unexpected error: ' + err.message); 96 | process.exit(-1); 97 | } 98 | 99 | function printUsage() { 100 | var usage = CommandLineUsage(scriptDescription); 101 | console.log(usage); 102 | } 103 | 104 | function printVersion() { 105 | var text = FS.readFileSync(`${__dirname}/../package.json`, 'utf-8'); 106 | var json = JSON.parse(text); 107 | var version = json.version; 108 | console.log(`Git-Thanos version ${version}`); 109 | } 110 | 111 | function checkWorkingDirectory() { 112 | try { 113 | var cmd = 'git rev-parse --is-inside-work-tree'; 114 | var options = { 115 | stdio: [ 'pipe', 'pipe', 'pipe' ], 116 | } 117 | ChildProcess.execSync(cmd, options); 118 | } catch (err) { 119 | throw new Error("Not a git repository"); 120 | } 121 | } 122 | 123 | function checkHardwareSupport(options) { 124 | if (!options['enable-inf-stone-power']) { 125 | throw new Error("Unauthorized (401)"); 126 | } 127 | if (!options['enable-inf-stone-space']) { 128 | throw new Error("Requested Range Not Satisfiable (416)"); 129 | } 130 | if (!options['enable-inf-stone-reality']) { 131 | throw new Error("I'm a teapot (418)"); 132 | } 133 | if (!options['enable-inf-stone-soul']) { 134 | throw new Error("Enhance Your Calm (420)"); 135 | } 136 | if (!options['enable-inf-stone-time']) { 137 | throw new Error("Request Timeout (408)"); 138 | } 139 | if (!options['enable-inf-stone-mind']) { 140 | throw new Error("Blocked by Windows Parental Controls (450)"); 141 | } 142 | } 143 | 144 | function checkHardwareLocations() { 145 | if (!process.env.POWER_STONE_HOME) { 146 | throw new Error("POWER_STONE_HOME is not set and the Power Stone is not found in your PATH"); 147 | } else if (!/^xandar$/i.test(process.env.POWER_STONE_HOME)) { 148 | throw new Error("Unable to locate the Power Stone"); 149 | } 150 | if (!process.env.SPACE_STONE_HOME) { 151 | throw new Error("SPACE_STONE_HOME is not set and the Space Stone is not found in your PATH"); 152 | } else if (!/^asgard$/i.test(process.env.SPACE_STONE_HOME)) { 153 | throw new Error("Unable to locate the Space Stone"); 154 | } 155 | if (!process.env.REALITY_STONE_HOME) { 156 | throw new Error("REALITY_STONE_HOME is not set and the Reality Stone is not found in your PATH"); 157 | } else if (!/^knowhere$/i.test(process.env.REALITY_STONE_HOME)) { 158 | throw new Error("Unable to locate the Reality Stone"); 159 | } 160 | if (!process.env.SOUL_STONE_HOME) { 161 | throw new Error("SOUL_STONE_HOME is not set and the Soul Stone is not found in your PATH"); 162 | } else if (!/^vormir$/i.test(process.env.SOUL_STONE_HOME)) { 163 | throw new Error("Unable to locate the Soul Stone"); 164 | } 165 | if (!process.env.TIME_STONE_HOME) { 166 | throw new Error("TIME_STONE_HOME is not set and the Time Stone is not found in your PATH"); 167 | } else if (!/^earth$/i.test(process.env.TIME_STONE_HOME)) { 168 | throw new Error("Unable to locate the Time Stone"); 169 | } 170 | if (!process.env.MIND_STONE_HOME) { 171 | throw new Error("MIND_STONE_HOME is not set and the Mind Stone is not found in your PATH"); 172 | } else if (!/^(earth|vision's forehead)$/i.test(process.env.MIND_STONE_HOME)) { 173 | throw new Error("Unable to locate the Mind Stone"); 174 | } 175 | } 176 | 177 | function getCommitAuthors() { 178 | var options = { 179 | encoding: 'utf8', 180 | stdio: [ 'inherit', 'pipe', 'pipe' ], 181 | }; 182 | var cmd = `git shortlog -sne`; 183 | var output = ChildProcess.execSync(cmd, options); 184 | var lines = output.split(/[\r\n]+/); 185 | var authors = []; 186 | lines.forEach(function(s) { 187 | var m = /(\d+)\s+(.*?)\s+<(.*?)>/.exec(s); 188 | if (m) { 189 | var author = { 190 | commits: parseInt(m[1]), 191 | name: m[2], 192 | email: m[3] 193 | }; 194 | authors.push(author); 195 | } 196 | }); 197 | return authors; 198 | } 199 | 200 | function performRandomFairSelection(authors) { 201 | // shuffle the array 202 | var copy = authors.filter(function(author) { 203 | return !isDustAlready(author.name); 204 | }); 205 | /** 206 | * TODO: Find a better solution! 207 | * 208 | * See issue #1 209 | */ 210 | copy = copy.filter(function(author) { 211 | return !/Thanos/i.test(author.name); 212 | }); 213 | var currentIndex = copy.length, temporaryValue, randomIndex; 214 | while (0 !== currentIndex) { 215 | randomIndex = Math.floor(Math.random() * currentIndex); 216 | currentIndex -= 1; 217 | temporaryValue = copy[currentIndex]; 218 | copy[currentIndex] = copy[randomIndex]; 219 | copy[randomIndex] = temporaryValue; 220 | } 221 | 222 | var half = copy.length / 2; 223 | if (half % 1) { 224 | // if we have an odd number then person at the cusp has a 50/50 chance 225 | if (Math.random() >= 0.5) { 226 | half = Math.floor(half); 227 | } else { 228 | half = Math.ceil(half); 229 | } 230 | } 231 | return copy.slice(0, half); 232 | } 233 | 234 | function applyDispassionateMercy(authors) { 235 | var emailCases = authors.map(function(author) { 236 | return ' "' + author.name + '") echo "' + turnToDust(author.email) + '" ;;'; 237 | }); 238 | var emailScript = [ '#!/bin/sh' ].concat('case "$1" in', emailCases, 'esac'); 239 | var emailTmp = Tmp.fileSync({ mode: 0700, prefix: 'email-', postfix: '.sh' }); 240 | FS.writeSync(emailTmp.fd, emailScript.join('\n')); 241 | FS.closeSync(emailTmp.fd); 242 | 243 | var nameCases = authors.map(function(author) { 244 | return ' "' + author.name + '") echo "' + turnToDust(author.name) + '" ;;'; 245 | }); 246 | var nameScript = [ '#!/bin/sh' ].concat('case "$1" in', nameCases, 'esac'); 247 | var nameTmp = Tmp.fileSync({ mode: 0700, prefix: 'name-', postfix: '.sh' }); 248 | FS.writeSync(nameTmp.fd, nameScript.join('\n')); 249 | FS.closeSync(nameTmp.fd); 250 | 251 | var envf = [ 252 | 'NEW_NAME=$(' + nameTmp.name + ' "$GIT_AUTHOR_NAME")', 253 | 'if [ ! -z "$NEW_NAME" ]', 254 | 'then', 255 | ' NEW_EMAIL=$(' + emailTmp.name + ' "$GIT_AUTHOR_NAME")', 256 | ' GIT_AUTHOR_NAME="$NEW_NAME";', 257 | ' GIT_AUTHOR_EMAIL="$NEW_EMAIL";', 258 | 'fi', 259 | ]; 260 | var cmd = "git filter-branch -f --env-filter '" 261 | + "\n" + envf.join("\n") + "\n" 262 | + "' --tag-name-filter cat -- --branches --tags"; 263 | var options = { 264 | encoding: 'utf8', 265 | stdio: [ 'inherit', 'inherit', 'inherit' ], 266 | }; 267 | ChildProcess.execSync(cmd, options); 268 | 269 | FS.unlinkSync(emailTmp.name); 270 | FS.unlinkSync(nameTmp.name); 271 | } 272 | 273 | function turnToDust(src) { 274 | var dst = ''; 275 | for (let i = 0; i < src.length; i++) { 276 | var r = Math.random(); 277 | if (r < 0.5) { 278 | dst += '\u2591'; 279 | } else if (r < 0.8) { 280 | dst += '\u2592'; 281 | } else { 282 | dst += '\u2593'; 283 | } 284 | } 285 | return dst; 286 | } 287 | 288 | function isDustAlready(s) { 289 | return /^[\u2591\u2592\u2593]+$/.test(s); 290 | } 291 | 292 | Tmp.setGracefulCleanup(); 293 | --------------------------------------------------------------------------------