├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── git ├── mod.rs └── models.rs ├── i18n.rs ├── main.rs └── prompt.rs /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1 2 | 3 | # This Dockerfile adds a non-root user with sudo access. Update the “remoteUser” property in 4 | # devcontainer.json to use it. More info: https://aka.ms/vscode-remote/containers/non-root-user. 5 | ARG USERNAME=vscode 6 | ARG USER_UID=1000 7 | ARG USER_GID=$USER_UID 8 | 9 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 10 | RUN apt-get update \ 11 | && export DEBIAN_FRONTEND=noninteractive \ 12 | && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ 13 | # 14 | # Verify git, needed tools installed 15 | && apt-get -y install git openssh-client cmake less iproute2 procps lsb-release \ 16 | # 17 | # Install lldb, vadimcn.vscode-lldb VSCode extension dependencies 18 | && apt-get install -y lldb python3-minimal libpython3.7 \ 19 | # 20 | # Install Rust components 21 | && rustup update 2>&1 \ 22 | && rustup component add rls rust-analysis rust-src rustfmt clippy 2>&1 \ 23 | # 24 | # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. 25 | && groupadd --gid $USER_GID $USERNAME \ 26 | && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ 27 | # [Optional] Add sudo support for the non-root user 28 | && apt-get install -y sudo \ 29 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ 30 | && chmod 0440 /etc/sudoers.d/$USERNAME \ 31 | # 32 | # Clean up 33 | && apt-get autoremove -y \ 34 | && apt-get clean -y \ 35 | && rm -rf /var/lib/apt/lists/* 36 | 37 | # [Optional] Uncomment this section to install additional OS packages. 38 | # RUN apt-get update \ 39 | # && export DEBIAN_FRONTEND=noninteractive \ 40 | # && apt-get -y install --no-install-recommends 41 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "dockerFile": "Dockerfile", 4 | "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], 5 | 6 | // Set *default* container specific settings.json values on container create. 7 | "settings": { 8 | "terminal.integrated.shell.linux": "/bin/bash", 9 | "lldb.executable": "/usr/bin/lldb", 10 | // VS Code don't watch files under ./target 11 | "files.watcherExclude": { 12 | "**/target/**": true 13 | } 14 | }, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "rust-lang.rust", 19 | "bungcip.better-toml", 20 | "vadimcn.vscode-lldb", 21 | // (Optional) Displays the current CPU stats, memory/disk consumption, clock freq. etc. of the container host in the VS Code status bar. 22 | "mutantdino.resourcemonitor" 23 | ] 24 | 25 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 26 | // "forwardPorts": [], 27 | 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "rustc --version", 30 | 31 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 32 | // "remoteUser": "vscode" 33 | } 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["main"] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | cargo: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | command: [fmt, build] 17 | include: 18 | - command: fmt 19 | args: -- --check 20 | - command: build 21 | args: --verbose 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Setup 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | - name: Run ${{ matrix.command }} 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: ${{ matrix.command }} 32 | args: ${{ matrix.args }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | gitclean 3 | git-tidy 4 | *.snap 5 | coverage.txt 6 | 7 | 8 | #Added by cargo 9 | 10 | /target 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'git-tidy'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=git-tidy", 15 | "--package=git-tidy" 16 | ], 17 | "filter": { 18 | "name": "git-tidy", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'git-tidy'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=git-tidy", 34 | "--package=git-tidy" 35 | ], 36 | "filter": { 37 | "name": "git-tidy", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.13" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "ansi_term" 14 | version = "0.11.0" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 17 | dependencies = [ 18 | "winapi", 19 | ] 20 | 21 | [[package]] 22 | name = "atty" 23 | version = "0.2.14" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 26 | dependencies = [ 27 | "hermit-abi", 28 | "libc", 29 | "winapi", 30 | ] 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "1.2.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 37 | 38 | [[package]] 39 | name = "cfg-if" 40 | version = "1.0.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 43 | 44 | [[package]] 45 | name = "clap" 46 | version = "2.33.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 49 | dependencies = [ 50 | "ansi_term", 51 | "atty", 52 | "bitflags", 53 | "strsim", 54 | "textwrap", 55 | "unicode-width", 56 | "vec_map", 57 | ] 58 | 59 | [[package]] 60 | name = "console" 61 | version = "0.11.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "8c0994e656bba7b922d8dd1245db90672ffb701e684e45be58f20719d69abc5a" 64 | dependencies = [ 65 | "encode_unicode", 66 | "lazy_static", 67 | "libc", 68 | "regex", 69 | "terminal_size", 70 | "termios", 71 | "unicode-width", 72 | "winapi", 73 | "winapi-util", 74 | ] 75 | 76 | [[package]] 77 | name = "console" 78 | version = "0.13.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "a50aab2529019abfabfa93f1e6c41ef392f91fbf179b347a7e96abb524884a08" 81 | dependencies = [ 82 | "encode_unicode", 83 | "lazy_static", 84 | "libc", 85 | "regex", 86 | "terminal_size", 87 | "unicode-width", 88 | "winapi", 89 | "winapi-util", 90 | ] 91 | 92 | [[package]] 93 | name = "dialoguer" 94 | version = "0.7.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "70f807b2943dc90f9747497d9d65d7e92472149be0b88bf4ce1201b4ac979c26" 97 | dependencies = [ 98 | "console 0.13.0", 99 | "lazy_static", 100 | "tempfile", 101 | "zeroize", 102 | ] 103 | 104 | [[package]] 105 | name = "encode_unicode" 106 | version = "0.3.6" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 109 | 110 | [[package]] 111 | name = "getrandom" 112 | version = "0.2.2" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" 115 | dependencies = [ 116 | "cfg-if", 117 | "libc", 118 | "wasi", 119 | ] 120 | 121 | [[package]] 122 | name = "git-tidy" 123 | version = "2.0.1" 124 | dependencies = [ 125 | "dialoguer", 126 | "indicatif", 127 | "regex", 128 | "structopt", 129 | ] 130 | 131 | [[package]] 132 | name = "heck" 133 | version = "0.3.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 136 | dependencies = [ 137 | "unicode-segmentation", 138 | ] 139 | 140 | [[package]] 141 | name = "hermit-abi" 142 | version = "0.1.11" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "8a0d737e0f947a1864e93d33fdef4af8445a00d1ed8dc0c8ddb73139ea6abf15" 145 | dependencies = [ 146 | "libc", 147 | ] 148 | 149 | [[package]] 150 | name = "indicatif" 151 | version = "0.15.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4" 154 | dependencies = [ 155 | "console 0.11.3", 156 | "lazy_static", 157 | "number_prefix", 158 | "regex", 159 | ] 160 | 161 | [[package]] 162 | name = "lazy_static" 163 | version = "1.4.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 166 | 167 | [[package]] 168 | name = "libc" 169 | version = "0.2.69" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 172 | 173 | [[package]] 174 | name = "memchr" 175 | version = "2.3.3" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 178 | 179 | [[package]] 180 | name = "number_prefix" 181 | version = "0.3.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" 184 | 185 | [[package]] 186 | name = "ppv-lite86" 187 | version = "0.2.10" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 190 | 191 | [[package]] 192 | name = "proc-macro-error" 193 | version = "1.0.2" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" 196 | dependencies = [ 197 | "proc-macro-error-attr", 198 | "proc-macro2", 199 | "quote", 200 | "syn", 201 | "version_check", 202 | ] 203 | 204 | [[package]] 205 | name = "proc-macro-error-attr" 206 | version = "1.0.2" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" 209 | dependencies = [ 210 | "proc-macro2", 211 | "quote", 212 | "syn", 213 | "syn-mid", 214 | "version_check", 215 | ] 216 | 217 | [[package]] 218 | name = "proc-macro2" 219 | version = "1.0.10" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" 222 | dependencies = [ 223 | "unicode-xid", 224 | ] 225 | 226 | [[package]] 227 | name = "quote" 228 | version = "1.0.3" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 231 | dependencies = [ 232 | "proc-macro2", 233 | ] 234 | 235 | [[package]] 236 | name = "rand" 237 | version = "0.8.3" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" 240 | dependencies = [ 241 | "libc", 242 | "rand_chacha", 243 | "rand_core", 244 | "rand_hc", 245 | ] 246 | 247 | [[package]] 248 | name = "rand_chacha" 249 | version = "0.3.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" 252 | dependencies = [ 253 | "ppv-lite86", 254 | "rand_core", 255 | ] 256 | 257 | [[package]] 258 | name = "rand_core" 259 | version = "0.6.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" 262 | dependencies = [ 263 | "getrandom", 264 | ] 265 | 266 | [[package]] 267 | name = "rand_hc" 268 | version = "0.3.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" 271 | dependencies = [ 272 | "rand_core", 273 | ] 274 | 275 | [[package]] 276 | name = "redox_syscall" 277 | version = "0.2.4" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" 280 | dependencies = [ 281 | "bitflags", 282 | ] 283 | 284 | [[package]] 285 | name = "regex" 286 | version = "1.3.9" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 289 | dependencies = [ 290 | "aho-corasick", 291 | "memchr", 292 | "regex-syntax", 293 | "thread_local", 294 | ] 295 | 296 | [[package]] 297 | name = "regex-syntax" 298 | version = "0.6.18" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 301 | 302 | [[package]] 303 | name = "remove_dir_all" 304 | version = "0.5.3" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 307 | dependencies = [ 308 | "winapi", 309 | ] 310 | 311 | [[package]] 312 | name = "strsim" 313 | version = "0.8.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 316 | 317 | [[package]] 318 | name = "structopt" 319 | version = "0.3.13" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb" 322 | dependencies = [ 323 | "clap", 324 | "lazy_static", 325 | "structopt-derive", 326 | ] 327 | 328 | [[package]] 329 | name = "structopt-derive" 330 | version = "0.4.6" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac" 333 | dependencies = [ 334 | "heck", 335 | "proc-macro-error", 336 | "proc-macro2", 337 | "quote", 338 | "syn", 339 | ] 340 | 341 | [[package]] 342 | name = "syn" 343 | version = "1.0.17" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" 346 | dependencies = [ 347 | "proc-macro2", 348 | "quote", 349 | "unicode-xid", 350 | ] 351 | 352 | [[package]] 353 | name = "syn-mid" 354 | version = "0.5.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 357 | dependencies = [ 358 | "proc-macro2", 359 | "quote", 360 | "syn", 361 | ] 362 | 363 | [[package]] 364 | name = "tempfile" 365 | version = "3.2.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 368 | dependencies = [ 369 | "cfg-if", 370 | "libc", 371 | "rand", 372 | "redox_syscall", 373 | "remove_dir_all", 374 | "winapi", 375 | ] 376 | 377 | [[package]] 378 | name = "terminal_size" 379 | version = "0.1.13" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "9a14cd9f8c72704232f0bfc8455c0e861f0ad4eb60cc9ec8a170e231414c1e13" 382 | dependencies = [ 383 | "libc", 384 | "winapi", 385 | ] 386 | 387 | [[package]] 388 | name = "termios" 389 | version = "0.3.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" 392 | dependencies = [ 393 | "libc", 394 | ] 395 | 396 | [[package]] 397 | name = "textwrap" 398 | version = "0.11.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 401 | dependencies = [ 402 | "unicode-width", 403 | ] 404 | 405 | [[package]] 406 | name = "thread_local" 407 | version = "1.0.1" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 410 | dependencies = [ 411 | "lazy_static", 412 | ] 413 | 414 | [[package]] 415 | name = "unicode-segmentation" 416 | version = "1.6.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 419 | 420 | [[package]] 421 | name = "unicode-width" 422 | version = "0.1.7" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 425 | 426 | [[package]] 427 | name = "unicode-xid" 428 | version = "0.2.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 431 | 432 | [[package]] 433 | name = "vec_map" 434 | version = "0.8.1" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 437 | 438 | [[package]] 439 | name = "version_check" 440 | version = "0.9.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" 443 | 444 | [[package]] 445 | name = "wasi" 446 | version = "0.10.2+wasi-snapshot-preview1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 449 | 450 | [[package]] 451 | name = "winapi" 452 | version = "0.3.8" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 455 | dependencies = [ 456 | "winapi-i686-pc-windows-gnu", 457 | "winapi-x86_64-pc-windows-gnu", 458 | ] 459 | 460 | [[package]] 461 | name = "winapi-i686-pc-windows-gnu" 462 | version = "0.4.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 465 | 466 | [[package]] 467 | name = "winapi-util" 468 | version = "0.1.5" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 471 | dependencies = [ 472 | "winapi", 473 | ] 474 | 475 | [[package]] 476 | name = "winapi-x86_64-pc-windows-gnu" 477 | version = "0.4.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 480 | 481 | [[package]] 482 | name = "zeroize" 483 | version = "0.9.3" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "45af6a010d13e4cf5b54c94ba5a2b2eba5596b9e46bf5875612d332a1f2b3f86" 486 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "git-tidy" 3 | description = "Tidy up stale git branches." 4 | version = "2.0.1" 5 | authors = ["Drew Wyatt ", 6 | "Dalton Claybrook "] 7 | license = "MIT" 8 | edition = "2018" 9 | repository = "https://github.com/drewwyatt/git-tidy" 10 | keywords = ["git", "cli", "command-line", "command-line-tool"] 11 | categories = ["command-line-utilities"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | dialoguer = "0.7.1" 17 | indicatif = "0.15.0" 18 | regex = "1" 19 | structopt = { version = "0.3" } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🗑 git-tidy 2 | 3 | [![crates.io](https://img.shields.io/crates/v/git-tidy?style=flat-square)](https://crates.io/crates/git-tidy) 4 | 5 | Tidy up stale git branches. 6 | 7 | [![asciicast](https://asciinema.org/a/389715.svg)](https://asciinema.org/a/389715) 8 | 9 | ## Installation 10 | 11 | ### Homebrew 12 | 13 | ```bash 14 | $ brew tap drewwyatt/tap 15 | $ brew install git-tidy 16 | ``` 17 | 18 | ### Cargo 19 | 20 | ```bash 21 | $ cargo install git-tidy 22 | ``` 23 | 24 | #### ⚠️ You may need to update `cargo` for this ⚠️ 25 | 26 | If you are seeing an error like the one in [this issue](https://github.com/drewwyatt/git-tidy/issues/45): 27 | 28 | ``` 29 | ▪ cargo install git-tidy 30 | Updating crates.io index 31 | Installing git-tidy v2.0.1 32 | error: failed to compile `git-tidy v2.0.1`, intermediate artifacts can be found at `/tmp/cargo-installgtcftB` 33 | 34 | Caused by: 35 | failed to select a version for the requirement `zeroize = "^0.9.3"` 36 | candidate versions found which didn't match: 1.3.0, 1.2.0, 1.1.1, ... 37 | location searched: crates.io index 38 | required by package `dialoguer v0.7.1` 39 | ... which is depended on by `git-tidy v2.0.1` 40 | ``` 41 | 42 | You can probably fix this by updating cargo with: 43 | 44 | ```sh 45 | rustup update 46 | ``` 47 | 48 | 49 | ### Previous versions 50 | 51 | Newer versions of `git-tidy` are (for now) only available from Homebrew and [crates.io](https://crates.io/crates/git-tidy), but you can still get `1.0.0` from the following places: 52 | 53 | #### Snapcraft 54 | 55 | ```bash 56 | $ sudo snap install git-tidy 57 | ``` 58 | 59 | #### Go 60 | 61 | ```bash 62 | $ go get -u github.com/drewwyatt/git-tidy 63 | ``` 64 | 65 | ## Usage 66 | 67 | ```bash 68 | $ git tidy # executes "git branch -d" on ": gone" branches 69 | ``` 70 | 71 | ### With force delete 72 | 73 | ```bash 74 | $ git tidy -f # same as above, but with "-D" instead of "-d" 75 | # or 76 | $ git tidy --force 77 | ``` 78 | 79 | ### Interactive 80 | 81 | Present all stale (": gone") branches in a checkbox list, allowing user to opt-in to deletions. 82 | 83 | ```bash 84 | $ git tidy -i 85 | # or 86 | $ git tidy --interactive 87 | # with force 88 | $ git tidy -if 89 | # or 90 | $ git tidy --interactive --force 91 | ``` 92 | -------------------------------------------------------------------------------- /src/git/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | 3 | use regex::Regex; 4 | 5 | use models::{GitError, GitExec}; 6 | 7 | pub struct Git 8 | where 9 | F: Fn(&str) -> (), 10 | { 11 | gone_branch_regex: Regex, 12 | output: Option, 13 | _report_progress: F, 14 | } 15 | 16 | impl Git 17 | where 18 | F: Fn(&str) -> (), 19 | { 20 | pub fn new(report_progress: F) -> Self { 21 | Git { 22 | _report_progress: report_progress, 23 | 24 | output: None, 25 | gone_branch_regex: Regex::new( 26 | r"(?m)^(?:\*| ) ([^\s]+)\s+[a-z0-9]+ \[[^:\n]+: gone\].*$", 27 | ) 28 | .unwrap(), 29 | } 30 | } 31 | 32 | pub fn delete(&mut self, force: bool, branch_name: &str) -> Result<&mut Self, GitError> { 33 | // TODO: figure out how to prevent getting the delete arg in 2 places 34 | let delete_arg = if force { "-D" } else { "-d" }; 35 | self.report_progress(&format!( 36 | "running 'git branch {} {}'", 37 | delete_arg, branch_name 38 | )); 39 | self.output = Some(GitExec::delete(force, branch_name)?); 40 | Ok(self) 41 | } 42 | 43 | pub fn fetch(&mut self) -> Result<&mut Self, GitError> { 44 | self.report_progress("running 'git fetch'..."); 45 | self.output = Some(GitExec::fetch()?); 46 | Ok(self) 47 | } 48 | 49 | pub fn prune(&mut self) -> Result<&mut Self, GitError> { 50 | self.report_progress("running 'git remote prune origin'..."); 51 | self.output = Some(GitExec::prune()?); 52 | Ok(self) 53 | } 54 | 55 | pub fn list_branches(&mut self) -> Result<&mut Self, GitError> { 56 | self.report_progress("running 'git branch -vv'..."); 57 | self.output = Some(GitExec::list_branches()?); 58 | Ok(self) 59 | } 60 | 61 | pub fn branch_names(&mut self) -> Result, GitError> { 62 | self.output 63 | .as_ref() 64 | .map(|str| { 65 | self.gone_branch_regex 66 | .captures_iter(&str) 67 | .map(|cap| String::from(&cap[1])) 68 | .collect::>() 69 | }) 70 | .ok_or(GitError::UnknownError) 71 | } 72 | 73 | fn report_progress(&mut self, message: &str) { 74 | let rp = &self._report_progress; 75 | rp(message); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/git/models.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum GitError { 3 | CommandError(String), 4 | ExecError(String), 5 | ParseError(String), 6 | UnknownError, 7 | } 8 | 9 | impl From for GitError { 10 | fn from(err: std::io::Error) -> Self { 11 | Self::ExecError(err.to_string()) 12 | } 13 | } 14 | 15 | impl From for GitError { 16 | fn from(err: std::string::FromUtf8Error) -> Self { 17 | Self::ParseError(err.to_string()) 18 | } 19 | } 20 | 21 | impl From for GitError { 22 | fn from(output: std::process::Output) -> Self { 23 | String::from_utf8(output.stderr) 24 | .map(Self::CommandError) 25 | .unwrap_or(Self::UnknownError) 26 | } 27 | } 28 | 29 | use std::process::Command; 30 | 31 | pub struct GitExec {} 32 | 33 | impl GitExec { 34 | pub fn delete(force: bool, branch_name: &str) -> Result { 35 | let delete_arg = if force { "-D" } else { "-d" }; 36 | let output = Command::new("git") 37 | .arg("branch") 38 | .arg(delete_arg) 39 | .arg(branch_name) 40 | .output()?; 41 | 42 | if output.status.success() { 43 | return Ok(String::from_utf8(output.stdout)?); 44 | } 45 | 46 | Err(GitError::from(output)) 47 | } 48 | 49 | pub fn fetch() -> Result { 50 | let output = Command::new("git").arg("fetch").output()?; 51 | if output.status.success() { 52 | return Ok(String::from_utf8(output.stdout)?); 53 | } 54 | 55 | Err(GitError::from(output)) 56 | } 57 | 58 | pub fn prune() -> Result { 59 | let output = Command::new("git") 60 | .arg("remote") 61 | .arg("prune") 62 | .arg("origin") 63 | .output()?; 64 | if output.status.success() { 65 | return Ok(String::from_utf8(output.stdout)?); 66 | } 67 | 68 | Err(GitError::from(output)) 69 | } 70 | 71 | pub fn list_branches() -> Result { 72 | let output = Command::new("git").arg("branch").arg("-vv").output()?; 73 | if output.status.success() { 74 | return Ok(String::from_utf8(output.stdout)?); 75 | } 76 | 77 | Err(GitError::from(output)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/i18n.rs: -------------------------------------------------------------------------------- 1 | use std::string::ToString; 2 | 3 | pub enum Text<'a> { 4 | BranchesDeleted, 5 | BranchesToDelete, 6 | DeletingBranch(&'a str), 7 | DeletingBranches, 8 | DryRunEnabled, 9 | FinishedWithErrors, 10 | NoBranchesDeleted, 11 | NothingToDo, 12 | StartupMessage, 13 | UnknownErrorEncountered, 14 | } 15 | 16 | impl<'a> ToString for Text<'a> { 17 | fn to_string(&self) -> String { 18 | match self { 19 | Text::BranchesDeleted => "Branches deleted:".into(), 20 | Text::BranchesToDelete => "Branches to delete:".into(), 21 | Text::DeletingBranch(name) => format!("Deleting ‘{}’...", name), 22 | Text::DeletingBranches => "Deleting branches...".into(), 23 | Text::DryRunEnabled => { 24 | "\n📣 NOTE: --dry-run enabled, no branches will be deleted.\n".into() 25 | } 26 | Text::FinishedWithErrors => "Finished with errors:".into(), 27 | Text::NoBranchesDeleted => "No branches were deleted.".into(), 28 | Text::NothingToDo => "Nothing to do!".into(), 29 | Text::StartupMessage => "Tidying up...".into(), 30 | Text::UnknownErrorEncountered => "An unknown error was encountered".into(), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod git; 2 | mod i18n; 3 | mod prompt; 4 | 5 | use indicatif::ProgressBar; 6 | use structopt::StructOpt; 7 | 8 | use git::models::GitError; 9 | use git::Git; 10 | use i18n::Text; 11 | use prompt::Prompt; 12 | 13 | #[derive(StructOpt)] 14 | #[structopt( 15 | about = "Tidy up stale git branches.", 16 | author = "Drew Wyatt ", 17 | name = "git-tidy" 18 | )] 19 | struct Cli { 20 | #[structopt( 21 | short, 22 | long, 23 | help = "Allow deleting branches irrespective of their apparent merged status" 24 | )] 25 | force: bool, 26 | 27 | #[structopt( 28 | short, 29 | long, 30 | help = r#"Present all ": gone" branches in list form, allowing opt-in to deletions"# 31 | )] 32 | interactive: bool, 33 | 34 | #[structopt(short, long, help = "Print output, but don't delete any branches")] 35 | dry_run: bool, 36 | } 37 | 38 | fn main() -> Result<(), GitError> { 39 | let args = Cli::from_args(); 40 | 41 | if args.dry_run { 42 | println!("{}", Text::DryRunEnabled.to_string()); 43 | } 44 | 45 | let spinner = ProgressBar::new_spinner(); 46 | spinner.set_message(&Text::StartupMessage.to_string()); 47 | spinner.enable_steady_tick(160); 48 | 49 | let mut git = Git::new(|m| spinner.set_message(m)); 50 | let gone_branches = git.fetch()?.prune()?.list_branches()?.branch_names()?; 51 | 52 | if gone_branches.is_empty() { 53 | spinner.finish_with_message(&Text::NothingToDo.to_string()); 54 | return Ok(()); 55 | } 56 | 57 | spinner.finish_and_clear(); 58 | let mut stale_branches = gone_branches; 59 | 60 | if args.interactive { 61 | stale_branches = Prompt::with(stale_branches); 62 | if stale_branches.is_empty() { 63 | println!("{}", Text::NothingToDo.to_string()); 64 | return Ok(()); 65 | } 66 | } 67 | 68 | if args.dry_run { 69 | println!("{}", Text::BranchesToDelete.to_string()); 70 | for branch in stale_branches { 71 | println!(" - {}", branch); 72 | } 73 | println!("") 74 | } else { 75 | let spinner = ProgressBar::new_spinner(); 76 | spinner.set_message(&Text::DeletingBranches.to_string()); 77 | spinner.enable_steady_tick(160); 78 | 79 | let (deleted_branches, deletion_errors) = 80 | stale_branches 81 | .into_iter() 82 | .fold((vec![], vec![]), |(mut del, mut err), branch_name| { 83 | spinner.set_message(&Text::DeletingBranch(&branch_name).to_string()); 84 | match git.delete(args.force, &branch_name) { 85 | Err(GitError::CommandError(msg)) => err.push((branch_name, msg)), 86 | Err(GitError::ExecError(msg)) => err.push((branch_name, msg)), 87 | Err(GitError::ParseError(msg)) => err.push((branch_name, msg)), 88 | Err(GitError::UnknownError) => { 89 | err.push((branch_name, Text::UnknownErrorEncountered.to_string())) 90 | } 91 | _ => del.push(branch_name), 92 | }; 93 | 94 | (del, err) 95 | }); 96 | 97 | spinner.finish_and_clear(); 98 | if deleted_branches.is_empty() { 99 | println!("{}", Text::NoBranchesDeleted.to_string()); 100 | } else { 101 | println!("{}", Text::BranchesDeleted.to_string()); 102 | for branch_name in deleted_branches { 103 | println!(" - {}", branch_name); 104 | } 105 | } 106 | 107 | if !deletion_errors.is_empty() { 108 | println!("{}", Text::FinishedWithErrors.to_string()); 109 | for (branch_name, error) in deletion_errors { 110 | println!(" - {}: {}", branch_name, error); 111 | } 112 | } 113 | } 114 | 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /src/prompt.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, MultiSelect}; 2 | 3 | pub struct Prompt {} 4 | 5 | impl Prompt { 6 | pub fn with(branches: Vec) -> Vec { 7 | let mut branches = branches; 8 | let selections = MultiSelect::with_theme(&ColorfulTheme::default()) 9 | .with_prompt("Stale branches") 10 | .items(&branches) 11 | .interact() 12 | .unwrap(); 13 | 14 | selections 15 | .into_iter() 16 | .map(|idx| branches.swap_remove(idx)) 17 | .rev() // sort back to ascending order 18 | .collect() 19 | } 20 | } 21 | --------------------------------------------------------------------------------