├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── dependency-review.yml │ ├── rust.yml │ └── scorecards.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── doc └── git-global.1 ├── rustfmt.toml ├── src ├── cli.rs ├── config.rs ├── errors.rs ├── generate_manpage.rs ├── lib.rs ├── main.rs ├── repo.rs ├── report.rs ├── subcommands.rs └── subcommands │ ├── ahead.rs │ ├── info.rs │ ├── install_manpage.rs │ ├── list.rs │ ├── scan.rs │ ├── staged.rs │ ├── stashed.rs │ ├── status.rs │ └── unstaged.rs └── tests ├── cli.rs ├── repo.rs ├── subcommands.rs └── utils.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | 7 | [README.md] 8 | trim_trailing_whitespace = true 9 | 10 | [*.rs] 11 | indent_size = 4 12 | indent_style = space 13 | max_line_length = 80 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | day: wednesday 8 | 9 | - package-ecosystem: cargo 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | day: wednesday 14 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 28 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build-and-test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 21 | with: 22 | egress-policy: audit 23 | 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - name: Build 26 | run: cargo build --verbose 27 | - name: Run tests 28 | run: cargo test --verbose 29 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ["master"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | 32 | steps: 33 | - name: Harden Runner 34 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 35 | with: 36 | egress-policy: audit 37 | 38 | - name: "Checkout code" 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | persist-credentials: false 42 | 43 | - name: "Run analysis" 44 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 49 | # - you want to enable the Branch-Protection check on a *public* repository, or 50 | # - you are installing Scorecards on a *private* repository 51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 53 | 54 | # Public repositories: 55 | # - Publish results to OpenSSF REST API for easy access by consumers 56 | # - Allows the repository to include the Scorecard badge. 57 | # - See https://github.com/ossf/scorecard-action#publishing-results. 58 | # For private repositories: 59 | # - `publish_results` will always be set to `false`, regardless 60 | # of the value entered here. 61 | publish_results: true 62 | 63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 64 | # format to the repository Actions tab. 65 | - name: "Upload artifact" 66 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 67 | with: 68 | name: SARIF file 69 | path: results.sarif 70 | retention-days: 5 71 | 72 | # Upload the results to GitHub's code scanning dashboard. 73 | - name: "Upload to code-scanning" 74 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 75 | with: 76 | sarif_file: results.sarif 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.8" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.9.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 69 | 70 | [[package]] 71 | name = "cc" 72 | version = "1.2.25" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" 75 | dependencies = [ 76 | "jobserver", 77 | "libc", 78 | "shlex", 79 | ] 80 | 81 | [[package]] 82 | name = "cfg-if" 83 | version = "1.0.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 86 | 87 | [[package]] 88 | name = "clap" 89 | version = "4.5.39" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 92 | dependencies = [ 93 | "clap_builder", 94 | ] 95 | 96 | [[package]] 97 | name = "clap_builder" 98 | version = "4.5.39" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 101 | dependencies = [ 102 | "anstream", 103 | "anstyle", 104 | "clap_lex", 105 | "strsim", 106 | ] 107 | 108 | [[package]] 109 | name = "clap_lex" 110 | version = "0.7.4" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 113 | 114 | [[package]] 115 | name = "colorchoice" 116 | version = "1.0.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 119 | 120 | [[package]] 121 | name = "directories" 122 | version = "6.0.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" 125 | dependencies = [ 126 | "dirs-sys", 127 | ] 128 | 129 | [[package]] 130 | name = "dirs-sys" 131 | version = "0.5.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 134 | dependencies = [ 135 | "libc", 136 | "option-ext", 137 | "redox_users", 138 | "windows-sys", 139 | ] 140 | 141 | [[package]] 142 | name = "displaydoc" 143 | version = "0.2.5" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 146 | dependencies = [ 147 | "proc-macro2", 148 | "quote", 149 | "syn", 150 | ] 151 | 152 | [[package]] 153 | name = "errno" 154 | version = "0.3.12" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 157 | dependencies = [ 158 | "libc", 159 | "windows-sys", 160 | ] 161 | 162 | [[package]] 163 | name = "fastrand" 164 | version = "2.3.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 167 | 168 | [[package]] 169 | name = "form_urlencoded" 170 | version = "1.2.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 173 | dependencies = [ 174 | "percent-encoding", 175 | ] 176 | 177 | [[package]] 178 | name = "getrandom" 179 | version = "0.2.16" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 182 | dependencies = [ 183 | "cfg-if", 184 | "libc", 185 | "wasi 0.11.0+wasi-snapshot-preview1", 186 | ] 187 | 188 | [[package]] 189 | name = "getrandom" 190 | version = "0.3.3" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 193 | dependencies = [ 194 | "cfg-if", 195 | "libc", 196 | "r-efi", 197 | "wasi 0.14.2+wasi-0.2.4", 198 | ] 199 | 200 | [[package]] 201 | name = "git-global" 202 | version = "0.6.6" 203 | dependencies = [ 204 | "clap", 205 | "directories", 206 | "git2", 207 | "man", 208 | "regex", 209 | "serde", 210 | "serde_json", 211 | "tempfile", 212 | "termsize", 213 | "walkdir", 214 | ] 215 | 216 | [[package]] 217 | name = "git2" 218 | version = "0.20.2" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" 221 | dependencies = [ 222 | "bitflags", 223 | "libc", 224 | "libgit2-sys", 225 | "log", 226 | "url", 227 | ] 228 | 229 | [[package]] 230 | name = "icu_collections" 231 | version = "2.0.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 234 | dependencies = [ 235 | "displaydoc", 236 | "potential_utf", 237 | "yoke", 238 | "zerofrom", 239 | "zerovec", 240 | ] 241 | 242 | [[package]] 243 | name = "icu_locale_core" 244 | version = "2.0.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 247 | dependencies = [ 248 | "displaydoc", 249 | "litemap", 250 | "tinystr", 251 | "writeable", 252 | "zerovec", 253 | ] 254 | 255 | [[package]] 256 | name = "icu_normalizer" 257 | version = "2.0.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 260 | dependencies = [ 261 | "displaydoc", 262 | "icu_collections", 263 | "icu_normalizer_data", 264 | "icu_properties", 265 | "icu_provider", 266 | "smallvec", 267 | "zerovec", 268 | ] 269 | 270 | [[package]] 271 | name = "icu_normalizer_data" 272 | version = "2.0.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 275 | 276 | [[package]] 277 | name = "icu_properties" 278 | version = "2.0.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 281 | dependencies = [ 282 | "displaydoc", 283 | "icu_collections", 284 | "icu_locale_core", 285 | "icu_properties_data", 286 | "icu_provider", 287 | "potential_utf", 288 | "zerotrie", 289 | "zerovec", 290 | ] 291 | 292 | [[package]] 293 | name = "icu_properties_data" 294 | version = "2.0.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 297 | 298 | [[package]] 299 | name = "icu_provider" 300 | version = "2.0.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 303 | dependencies = [ 304 | "displaydoc", 305 | "icu_locale_core", 306 | "stable_deref_trait", 307 | "tinystr", 308 | "writeable", 309 | "yoke", 310 | "zerofrom", 311 | "zerotrie", 312 | "zerovec", 313 | ] 314 | 315 | [[package]] 316 | name = "idna" 317 | version = "1.0.3" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 320 | dependencies = [ 321 | "idna_adapter", 322 | "smallvec", 323 | "utf8_iter", 324 | ] 325 | 326 | [[package]] 327 | name = "idna_adapter" 328 | version = "1.2.1" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 331 | dependencies = [ 332 | "icu_normalizer", 333 | "icu_properties", 334 | ] 335 | 336 | [[package]] 337 | name = "is_terminal_polyfill" 338 | version = "1.70.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 341 | 342 | [[package]] 343 | name = "itoa" 344 | version = "1.0.15" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 347 | 348 | [[package]] 349 | name = "jobserver" 350 | version = "0.1.33" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 353 | dependencies = [ 354 | "getrandom 0.3.3", 355 | "libc", 356 | ] 357 | 358 | [[package]] 359 | name = "libc" 360 | version = "0.2.172" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 363 | 364 | [[package]] 365 | name = "libgit2-sys" 366 | version = "0.18.1+1.9.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" 369 | dependencies = [ 370 | "cc", 371 | "libc", 372 | "libz-sys", 373 | "pkg-config", 374 | ] 375 | 376 | [[package]] 377 | name = "libredox" 378 | version = "0.1.3" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 381 | dependencies = [ 382 | "bitflags", 383 | "libc", 384 | ] 385 | 386 | [[package]] 387 | name = "libz-sys" 388 | version = "1.1.22" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" 391 | dependencies = [ 392 | "cc", 393 | "libc", 394 | "pkg-config", 395 | "vcpkg", 396 | ] 397 | 398 | [[package]] 399 | name = "linux-raw-sys" 400 | version = "0.9.4" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 403 | 404 | [[package]] 405 | name = "litemap" 406 | version = "0.8.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 409 | 410 | [[package]] 411 | name = "log" 412 | version = "0.4.27" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 415 | 416 | [[package]] 417 | name = "man" 418 | version = "0.3.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "ebf5fa795187a80147b1ac10aaedcf5ffd3bbeb1838bda61801a1c9ad700a1c9" 421 | dependencies = [ 422 | "roff", 423 | ] 424 | 425 | [[package]] 426 | name = "memchr" 427 | version = "2.7.4" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 430 | 431 | [[package]] 432 | name = "once_cell" 433 | version = "1.21.3" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 436 | 437 | [[package]] 438 | name = "once_cell_polyfill" 439 | version = "1.70.1" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 442 | 443 | [[package]] 444 | name = "option-ext" 445 | version = "0.2.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 448 | 449 | [[package]] 450 | name = "percent-encoding" 451 | version = "2.3.1" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 454 | 455 | [[package]] 456 | name = "pkg-config" 457 | version = "0.3.32" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 460 | 461 | [[package]] 462 | name = "potential_utf" 463 | version = "0.1.2" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 466 | dependencies = [ 467 | "zerovec", 468 | ] 469 | 470 | [[package]] 471 | name = "proc-macro2" 472 | version = "1.0.95" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 475 | dependencies = [ 476 | "unicode-ident", 477 | ] 478 | 479 | [[package]] 480 | name = "quote" 481 | version = "1.0.40" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 484 | dependencies = [ 485 | "proc-macro2", 486 | ] 487 | 488 | [[package]] 489 | name = "r-efi" 490 | version = "5.2.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 493 | 494 | [[package]] 495 | name = "redox_users" 496 | version = "0.5.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 499 | dependencies = [ 500 | "getrandom 0.2.16", 501 | "libredox", 502 | "thiserror", 503 | ] 504 | 505 | [[package]] 506 | name = "regex" 507 | version = "1.11.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 510 | dependencies = [ 511 | "aho-corasick", 512 | "memchr", 513 | "regex-automata", 514 | "regex-syntax", 515 | ] 516 | 517 | [[package]] 518 | name = "regex-automata" 519 | version = "0.4.9" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 522 | dependencies = [ 523 | "aho-corasick", 524 | "memchr", 525 | "regex-syntax", 526 | ] 527 | 528 | [[package]] 529 | name = "regex-syntax" 530 | version = "0.8.5" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 533 | 534 | [[package]] 535 | name = "roff" 536 | version = "0.1.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "e33e4fb37ba46888052c763e4ec2acfedd8f00f62897b630cadb6298b833675e" 539 | 540 | [[package]] 541 | name = "rustix" 542 | version = "1.0.7" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 545 | dependencies = [ 546 | "bitflags", 547 | "errno", 548 | "libc", 549 | "linux-raw-sys", 550 | "windows-sys", 551 | ] 552 | 553 | [[package]] 554 | name = "ryu" 555 | version = "1.0.20" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 558 | 559 | [[package]] 560 | name = "same-file" 561 | version = "1.0.6" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 564 | dependencies = [ 565 | "winapi-util", 566 | ] 567 | 568 | [[package]] 569 | name = "serde" 570 | version = "1.0.219" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 573 | dependencies = [ 574 | "serde_derive", 575 | ] 576 | 577 | [[package]] 578 | name = "serde_derive" 579 | version = "1.0.219" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 582 | dependencies = [ 583 | "proc-macro2", 584 | "quote", 585 | "syn", 586 | ] 587 | 588 | [[package]] 589 | name = "serde_json" 590 | version = "1.0.140" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 593 | dependencies = [ 594 | "itoa", 595 | "memchr", 596 | "ryu", 597 | "serde", 598 | ] 599 | 600 | [[package]] 601 | name = "shlex" 602 | version = "1.3.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 605 | 606 | [[package]] 607 | name = "smallvec" 608 | version = "1.15.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 611 | 612 | [[package]] 613 | name = "stable_deref_trait" 614 | version = "1.2.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 617 | 618 | [[package]] 619 | name = "strsim" 620 | version = "0.11.1" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 623 | 624 | [[package]] 625 | name = "syn" 626 | version = "2.0.101" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 629 | dependencies = [ 630 | "proc-macro2", 631 | "quote", 632 | "unicode-ident", 633 | ] 634 | 635 | [[package]] 636 | name = "synstructure" 637 | version = "0.13.2" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 640 | dependencies = [ 641 | "proc-macro2", 642 | "quote", 643 | "syn", 644 | ] 645 | 646 | [[package]] 647 | name = "tempfile" 648 | version = "3.20.0" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 651 | dependencies = [ 652 | "fastrand", 653 | "getrandom 0.3.3", 654 | "once_cell", 655 | "rustix", 656 | "windows-sys", 657 | ] 658 | 659 | [[package]] 660 | name = "termsize" 661 | version = "0.1.9" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "6f11ff5c25c172608d5b85e2fb43ee9a6d683a7f4ab7f96ae07b3d8b590368fd" 664 | dependencies = [ 665 | "libc", 666 | "winapi", 667 | ] 668 | 669 | [[package]] 670 | name = "thiserror" 671 | version = "2.0.12" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 674 | dependencies = [ 675 | "thiserror-impl", 676 | ] 677 | 678 | [[package]] 679 | name = "thiserror-impl" 680 | version = "2.0.12" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 683 | dependencies = [ 684 | "proc-macro2", 685 | "quote", 686 | "syn", 687 | ] 688 | 689 | [[package]] 690 | name = "tinystr" 691 | version = "0.8.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 694 | dependencies = [ 695 | "displaydoc", 696 | "zerovec", 697 | ] 698 | 699 | [[package]] 700 | name = "unicode-ident" 701 | version = "1.0.18" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 704 | 705 | [[package]] 706 | name = "url" 707 | version = "2.5.4" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 710 | dependencies = [ 711 | "form_urlencoded", 712 | "idna", 713 | "percent-encoding", 714 | ] 715 | 716 | [[package]] 717 | name = "utf8_iter" 718 | version = "1.0.4" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 721 | 722 | [[package]] 723 | name = "utf8parse" 724 | version = "0.2.2" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 727 | 728 | [[package]] 729 | name = "vcpkg" 730 | version = "0.2.15" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 733 | 734 | [[package]] 735 | name = "walkdir" 736 | version = "2.5.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 739 | dependencies = [ 740 | "same-file", 741 | "winapi-util", 742 | ] 743 | 744 | [[package]] 745 | name = "wasi" 746 | version = "0.11.0+wasi-snapshot-preview1" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 749 | 750 | [[package]] 751 | name = "wasi" 752 | version = "0.14.2+wasi-0.2.4" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 755 | dependencies = [ 756 | "wit-bindgen-rt", 757 | ] 758 | 759 | [[package]] 760 | name = "winapi" 761 | version = "0.3.9" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 764 | dependencies = [ 765 | "winapi-i686-pc-windows-gnu", 766 | "winapi-x86_64-pc-windows-gnu", 767 | ] 768 | 769 | [[package]] 770 | name = "winapi-i686-pc-windows-gnu" 771 | version = "0.4.0" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 774 | 775 | [[package]] 776 | name = "winapi-util" 777 | version = "0.1.9" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 780 | dependencies = [ 781 | "windows-sys", 782 | ] 783 | 784 | [[package]] 785 | name = "winapi-x86_64-pc-windows-gnu" 786 | version = "0.4.0" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 789 | 790 | [[package]] 791 | name = "windows-sys" 792 | version = "0.59.0" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 795 | dependencies = [ 796 | "windows-targets", 797 | ] 798 | 799 | [[package]] 800 | name = "windows-targets" 801 | version = "0.52.6" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 804 | dependencies = [ 805 | "windows_aarch64_gnullvm", 806 | "windows_aarch64_msvc", 807 | "windows_i686_gnu", 808 | "windows_i686_gnullvm", 809 | "windows_i686_msvc", 810 | "windows_x86_64_gnu", 811 | "windows_x86_64_gnullvm", 812 | "windows_x86_64_msvc", 813 | ] 814 | 815 | [[package]] 816 | name = "windows_aarch64_gnullvm" 817 | version = "0.52.6" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 820 | 821 | [[package]] 822 | name = "windows_aarch64_msvc" 823 | version = "0.52.6" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 826 | 827 | [[package]] 828 | name = "windows_i686_gnu" 829 | version = "0.52.6" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 832 | 833 | [[package]] 834 | name = "windows_i686_gnullvm" 835 | version = "0.52.6" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 838 | 839 | [[package]] 840 | name = "windows_i686_msvc" 841 | version = "0.52.6" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 844 | 845 | [[package]] 846 | name = "windows_x86_64_gnu" 847 | version = "0.52.6" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 850 | 851 | [[package]] 852 | name = "windows_x86_64_gnullvm" 853 | version = "0.52.6" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 856 | 857 | [[package]] 858 | name = "windows_x86_64_msvc" 859 | version = "0.52.6" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 862 | 863 | [[package]] 864 | name = "wit-bindgen-rt" 865 | version = "0.39.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 868 | dependencies = [ 869 | "bitflags", 870 | ] 871 | 872 | [[package]] 873 | name = "writeable" 874 | version = "0.6.1" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 877 | 878 | [[package]] 879 | name = "yoke" 880 | version = "0.8.0" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 883 | dependencies = [ 884 | "serde", 885 | "stable_deref_trait", 886 | "yoke-derive", 887 | "zerofrom", 888 | ] 889 | 890 | [[package]] 891 | name = "yoke-derive" 892 | version = "0.8.0" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 895 | dependencies = [ 896 | "proc-macro2", 897 | "quote", 898 | "syn", 899 | "synstructure", 900 | ] 901 | 902 | [[package]] 903 | name = "zerofrom" 904 | version = "0.1.6" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 907 | dependencies = [ 908 | "zerofrom-derive", 909 | ] 910 | 911 | [[package]] 912 | name = "zerofrom-derive" 913 | version = "0.1.6" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 916 | dependencies = [ 917 | "proc-macro2", 918 | "quote", 919 | "syn", 920 | "synstructure", 921 | ] 922 | 923 | [[package]] 924 | name = "zerotrie" 925 | version = "0.2.2" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 928 | dependencies = [ 929 | "displaydoc", 930 | "yoke", 931 | "zerofrom", 932 | ] 933 | 934 | [[package]] 935 | name = "zerovec" 936 | version = "0.11.2" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 939 | dependencies = [ 940 | "yoke", 941 | "zerofrom", 942 | "zerovec-derive", 943 | ] 944 | 945 | [[package]] 946 | name = "zerovec-derive" 947 | version = "0.11.1" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 950 | dependencies = [ 951 | "proc-macro2", 952 | "quote", 953 | "syn", 954 | ] 955 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "git-global" 4 | version = "0.6.6" 5 | authors = ["Eric Petersen "] 6 | description = "Keep track of all the git repositories on your machine." 7 | 8 | homepage = "https://github.com/peap/git-global" 9 | repository = "https://github.com/peap/git-global" 10 | documentation = "https://docs.rs/git-global" 11 | 12 | license = "MIT" 13 | readme = "README.md" 14 | 15 | keywords = ["git"] 16 | categories = ["command-line-utilities", "development-tools", "filesystem"] 17 | 18 | edition = "2021" 19 | 20 | default-run = "git-global" 21 | 22 | [[bin]] 23 | name = "git-global" 24 | path = "src/main.rs" 25 | doc = false 26 | 27 | [[bin]] 28 | name = "generate-manpage" 29 | path = "src/generate_manpage.rs" 30 | doc = false 31 | required-features = ["manpage"] 32 | 33 | [features] 34 | manpage = ["man"] 35 | 36 | [dependencies] 37 | directories = "6" 38 | serde_json = "1" 39 | termsize = "0.1" 40 | walkdir = "2" 41 | 42 | [dev-dependencies] 43 | regex = "1" 44 | tempfile = "3" 45 | 46 | [dependencies.clap] 47 | version = "4" 48 | features = ["cargo"] 49 | 50 | [dependencies.git2] 51 | version = "0.20" 52 | default-features = false # don't need SSH/HTTPS 53 | 54 | [dependencies.man] 55 | version = "0.3" 56 | optional = true 57 | 58 | [dependencies.serde] 59 | version = "1" 60 | features = ["derive"] 61 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Eric A. Petersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-global 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/git-global.svg)](https://crates.io/crates/git-global) 4 | [![Crates.io](https://img.shields.io/crates/d/git-global.svg)](https://crates.io/crates/git-global) 5 | [![Build](https://github.com/peap/git-global/actions/workflows/rust.yml/badge.svg)](https://github.com/peap/git-global/actions) 6 | 7 | Use `git-global` to keep track of all the git repositories on your machine. 8 | 9 | This is a Rust program that you can install with `cargo install git-global`. 10 | (To obtain `cargo` and Rust, see https://rustup.rs.) Once installed, you can 11 | optionally install the manpage with `git global install-manpage` 12 | 13 | Once installed, you gain an extra git subcommand that you can run from anywhere 14 | to check up on all your git repos: `git global`. Use `git global ` 15 | to: 16 | 17 | * `git global ahead`: show repos where branches contain commits that are not 18 | present on any of the remotes 19 | * `git global info`: show meta-information about git-global itself 20 | (configuration, number of known repos, etc.) 21 | * `git global install-manpage`: (non-functional) attempt to install 22 | git-global's manpage 23 | * `git global list`: show list of all known repos 24 | * `git global scan`: update the cache of known repos by searching your 25 | filesystem 26 | * `git global staged`: show status of the git index for repos with such changes 27 | * `git global stashed`: show stashes for all repos that have them 28 | * `git global status`: show `git status -s` for all your repos with any changes 29 | * `git global unstaged`: show status of the working directory for repos with 30 | such changes 31 | 32 | ## Command-line flags 33 | 34 | In addition to config-file-based options, there are a set of global 35 | command-line flags that take precedence: 36 | 37 | * `--json`: Print subcommand results in a JSON format. 38 | * `--untracked`: Show untracked files in subcommand results, e.g., for the 39 | `status`, `staged`, and `unstaged` subcommands. 40 | * `--nountracked`: Don't show untracked files in subcommand results, e.g., for 41 | the `status`, `staged`, and `unstaged` subcommands. 42 | 43 | ## Configuration 44 | 45 | To change the default behavior of `git-global`, you can do so with --- wait for 46 | it --- [git's global 47 | configuration](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration)! 48 | 49 | To set the root directory for repo discovery to something other than your home 50 | directory: 51 | ``` 52 | git config --global global.basedir /some/path 53 | ``` 54 | 55 | To add patterns to exclude while walking directories: 56 | ``` 57 | git config --global global.ignore .cargo,.vim,Library 58 | ``` 59 | 60 | The full list of configuration options supported in the `global` section of 61 | `.gitconfig` is: 62 | 63 | * `basedir`: The root directory for repo discovery (default: `$HOME`) 64 | * `follow-symlinks`: Whether to follow symbolic links during repo discovery 65 | (default: `true`) 66 | * `same-filesystem`: Whether to stay on the same filesystem as `basedir` 67 | during repo discovery 68 | ([on Unix or Windows only](https://docs.rs/walkdir/2.2.8/walkdir/struct.WalkDir.html#method.same_file_system)) 69 | (default: `true` on Windows or Unix, `false` otherwise) 70 | * `ignore`: Comma-separated list of patterns to exclude while walking 71 | directories (default: none) 72 | * `default-cmd`: The default subcommand to run if unspecified, i.e., when 73 | running `git global` (default: `status`) 74 | * `show-untracked`: Whether to include untracked files in output (default: 75 | `true`) 76 | 77 | ## Manpage generation 78 | 79 | An up-to-date copy of the manpage lives in the repository at 80 | [doc/git-global.1](doc/git-global.1). To generate it from a local clone of the 81 | repo, run: 82 | 83 | ``` 84 | cargo run --bin generate-manpage --features=manpage > doc/git-global.1 85 | ``` 86 | 87 | ## Ideas 88 | 89 | The following are some ideas about future subcommands and features: 90 | 91 | * `git global dirty`: show all repos that have changes of any kind 92 | * `git global branched`: show all repos not on `master` (TODO: or a different 93 | default branch in .gitconfig) 94 | * `git global duplicates`: show repos that are checked out to multiple places 95 | * `git global remotes`: show all remotes (TODO: why? maybe filter by hostname?) 96 | 97 | * `git global add `: add a git repo to the cache that would not be found in a scan 98 | * `git global ignore `: ignore a git repo and remove it from the cache 99 | * `git global ignored`: show which git repos are currently being ignored 100 | * `git global monitor`: launch a daemon to watch git dirs with inotify 101 | * `git global pull`: pull down changes from default tracking branch for clean repos 102 | 103 | * `git global cd `: change to the directory of the matched repo (#6) 104 | 105 | * stream results to `STDOUT` as the come in (from `git global status`, for 106 | example, so we don't have to wait until they're all collected) 107 | * use `locate .git` if the DB is populated, instead of walking the filesystem 108 | * make a `Subcommand` trait 109 | * do concurrency generically, not just for the `status` subcommand 110 | 111 | ## Release Notes 112 | 113 | * 0.6.6 (2025-02-07) 114 | * Fix an alignment issue with the `--verbose` flag's output. 115 | * 0.6.5 (2025-02-07) 116 | * Add a `-v`/`--verbose` flag, so far just used to indicate progress during 117 | `scan`'s directory walking. Useful for identifying patterns that should be 118 | omitted from scans. 119 | * 0.6.4 (2025-01-01) 120 | * Various dependency updates. 121 | * 0.6.3 (2024-08-10) 122 | * Make the `ahead` subcommand work with corrupted references (#105). Thanks, 123 | koalp! 124 | * Various dependency updates. 125 | * 0.6.2 (2024-06-08) 126 | * Various dependency updates, including `json` --> `serde_json`. 127 | * 0.6.1 (2023-08-10) 128 | * Various dependency updates. 129 | * 0.6.0 (2023-05-10) 130 | * Update to Rust 2021 edition. 131 | * Update, replace, or remove several dependencies. 132 | * 0.5.1 (2022-03-17) 133 | * Add the `generate-manpage` binary and (non-functional) `install-manpage` 134 | subcommand. 135 | * 0.5.0 (2021-07-12) 136 | * Add the `ahead` subcommand - thanks, koalp!. 137 | * 0.4.1 (2021-06-03) 138 | * Fix crashes when a cached repo has been deleted. 139 | * 0.4.0 (2021-04-19) 140 | * Update to Rust 2018 edition (Thanks, koalp!). 141 | * Replace the `dirs` and `app_dirs` crates with `directories`. 142 | * Previously created cache files may be ignored after upgrading to this 143 | version, so the cache might need to regenerated during the first command 144 | run after upgrading to this version. However, we no longer panic if the 145 | cache file can't be created. 146 | * 0.3.2 (2020-11-13) 147 | * Update dependencies. 148 | * 0.3.1 (2020-04-25) 149 | * Update dependencies. 150 | * 0.3.0 (2019-08-04) 151 | * Add subcommands: 152 | * `staged` 153 | * `stashed` 154 | * `unstaged` 155 | * Add config options: 156 | * `default-cmd` 157 | * `show-untracked` 158 | * `follow-symlinks` 159 | * `same-filesystem` 160 | * Add command-line flags: 161 | * `--untracked` 162 | * `--nountracked` 163 | * Add options to follow symlinks and stay on the same filesystem while 164 | scanning directories; both are `true` by default. (Thanks, pka!) 165 | * 0.2.0 (2019-03-18) 166 | * Include untracked files in status output. 167 | * Expand documentation and package metadata. 168 | * Update and change several dependencies. 169 | * Add some tests. 170 | * Several public API changes, such as: 171 | * Rename `GitGlobalConfig` to `Config`. 172 | * Rename `GitGlobalResult` to `Report`. 173 | * Move `get_repos` `find_repos`, and `cache_repos` functions to `Config`. 174 | * Split the `core` module into `config`, `repo`, and `report`. 175 | * Merge bug fix for scanning directories when nothing is configured to be 176 | ignored ([#1](https://github.com/peap/git-global/pull/1)). 177 | * 0.1.0 (2017-01-31) 178 | * Initial release with these subcommands: help, info, list, scan, status. 179 | -------------------------------------------------------------------------------- /doc/git-global.1: -------------------------------------------------------------------------------- 1 | .TH GIT-GLOBAL 1 2 | .SH NAME 3 | git\-global \- Keep track of all the git repositories on your machine. 4 | .SH SYNOPSIS 5 | \fBgit\-global\fR [FLAGS] 6 | .SH FLAGS 7 | .TP 8 | \fBv\fR, \fBverbose\fR 9 | Enable verbose mode. 10 | 11 | .TP 12 | \fBj\fR, \fBjson\fR 13 | Output subcommand results in JSON. 14 | 15 | .TP 16 | \fBu\fR, \fBuntracked\fR 17 | Show untracked files in output. 18 | 19 | .TP 20 | \fBt\fR, \fBnountracked\fR 21 | Don't show untracked files in output. 22 | .SH VERSION 23 | Crate version 0.6.6 24 | 25 | 26 | .SH SUBCOMMANDS 27 | The following subcommands are supported by git global; use git's global config to set your default choice. 28 | 29 | ahead: Shows repos with changes that are not pushed to a remote 30 | 31 | info: Shows meta\-information about git\-global 32 | 33 | install\-manpage: Attempts to install git\-global's man page 34 | 35 | list: Lists all known repos 36 | 37 | scan: Updates cache of known repos 38 | 39 | staged: Shows git index status for repos with staged changes 40 | 41 | stashed: Shows repos with stashed changes 42 | 43 | status: Shows status (`git status \-s`) for repos with any changes 44 | 45 | unstaged: Shows working dir status for repos with unstaged changes 46 | 47 | 48 | .SH EXIT STATUS 49 | .TP 50 | \fB0\fR 51 | Successful program execution. 52 | 53 | .TP 54 | \fB1\fR 55 | Unsuccessful program execution. 56 | 57 | .TP 58 | \fB101\fR 59 | The program panicked. 60 | .SH AUTHOR 61 | .P 62 | .RS 2 63 | .nf 64 | Eric Petersen 65 | 66 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | control_brace_style = "AlwaysSameLine" 3 | struct_lit_single_line = false 4 | format_strings = false 5 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! The command line interface for git-global. 2 | 3 | use std::io::{stderr, stdout, Write}; 4 | 5 | use clap::{command, Arg, ArgAction, ArgMatches, Command}; 6 | use serde_json::json; 7 | 8 | use crate::config::Config; 9 | use crate::subcommands; 10 | 11 | /// Returns the definitive clap::Command instance for git-global. 12 | pub fn get_clap_app() -> Command { 13 | command!() 14 | .arg( 15 | Arg::new("verbose") 16 | .short('v') 17 | .long("verbose") 18 | .action(ArgAction::SetTrue) 19 | .global(true) 20 | .help("Enable verbose mode."), 21 | ) 22 | .arg( 23 | Arg::new("json") 24 | .short('j') 25 | .long("json") 26 | .action(ArgAction::SetTrue) 27 | .global(true) 28 | .help("Output subcommand results in JSON."), 29 | ) 30 | .arg( 31 | Arg::new("untracked") 32 | .short('u') 33 | .long("untracked") 34 | .action(ArgAction::SetTrue) 35 | .conflicts_with("nountracked") 36 | .global(true) 37 | .help("Show untracked files in output."), 38 | ) 39 | .arg( 40 | Arg::new("nountracked") 41 | .short('t') 42 | .long("nountracked") 43 | .action(ArgAction::SetTrue) 44 | .conflicts_with("untracked") 45 | .global(true) 46 | .help("Don't show untracked files in output."), 47 | ) 48 | .subcommands( 49 | subcommands::get_subcommands() 50 | .iter() 51 | .map(|(cmd, desc)| Command::new(*cmd).about(*desc)), 52 | ) 53 | } 54 | 55 | /// Merge command-line arguments from an ArgMatches object with a Config. 56 | fn merge_args_with_config(config: &mut Config, matches: &ArgMatches) { 57 | if matches.get_flag("verbose") { 58 | config.verbose = true; 59 | } 60 | if matches.get_flag("untracked") { 61 | config.show_untracked = true; 62 | } 63 | if matches.get_flag("nountracked") { 64 | config.show_untracked = false; 65 | } 66 | } 67 | 68 | /// Runs the appropriate git-global subcommand based on command line arguments. 69 | /// 70 | /// As the effective binary entry point for `git-global`, prints results to 71 | /// `STDOUT` (or errors to `STDERR`) and returns an exit code. 72 | pub fn run_from_command_line() -> i32 { 73 | let clap_app = get_clap_app(); 74 | let matches = clap_app.get_matches(); 75 | let mut config = Config::new(); 76 | merge_args_with_config(&mut config, &matches); 77 | let report = subcommands::run(matches.subcommand_name(), config); 78 | let use_json = matches.get_flag("json"); 79 | match report { 80 | Ok(rep) => { 81 | if use_json { 82 | rep.print_json(&mut stdout()); 83 | } else { 84 | rep.print(&mut stdout()); 85 | } 86 | 0 87 | } 88 | Err(err) => { 89 | if use_json { 90 | let out = json!({ 91 | "error": true, 92 | "message": format!("{}", err) 93 | }); 94 | writeln!(&mut stderr(), "{:#}", out).unwrap(); 95 | } else { 96 | writeln!(&mut stderr(), "{}", err).unwrap(); 97 | } 98 | 1 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration of git-global. 2 | //! 3 | //! Exports the `Config` struct, which defines the base path for finding git 4 | //! repos on the machine, path patterns to ignore when scanning for repos, the 5 | //! location of a cache file, and other config options for running git-global. 6 | 7 | use std::env; 8 | use std::fs::{create_dir_all, remove_file, File}; 9 | use std::io::{BufRead, BufReader, Write}; 10 | use std::path::{Path, PathBuf}; 11 | 12 | use directories::{ProjectDirs, UserDirs}; 13 | use walkdir::{DirEntry, WalkDir}; 14 | 15 | use crate::repo::Repo; 16 | 17 | const QUALIFIER: &str = ""; 18 | const ORGANIZATION: &str = "peap"; 19 | const APPLICATION: &str = "git-global"; 20 | const CACHE_FILE: &str = "repos.txt"; 21 | 22 | const DEFAULT_CMD: &str = "status"; 23 | const DEFAULT_FOLLOW_SYMLINKS: bool = true; 24 | const DEFAULT_SAME_FILESYSTEM: bool = cfg!(any(unix, windows)); 25 | const DEFAULT_VERBOSE: bool = false; 26 | const DEFAULT_SHOW_UNTRACKED: bool = true; 27 | 28 | const SETTING_BASEDIR: &str = "global.basedir"; 29 | const SETTING_FOLLOW_SYMLINKS: &str = "global.follow-symlinks"; 30 | const SETTING_SAME_FILESYSTEM: &str = "global.same-filesystem"; 31 | const SETTING_IGNORE: &str = "global.ignore"; 32 | const SETTING_DEFAULT_CMD: &str = "global.default-cmd"; 33 | const SETTING_SHOW_UNTRACKED: &str = "global.show-untracked"; 34 | const SETTING_VERBOSE: &str = "global.verbose"; 35 | 36 | /// A container for git-global configuration options. 37 | pub struct Config { 38 | /// The base directory to walk when searching for git repositories. 39 | /// 40 | /// Default: $HOME. 41 | pub basedir: PathBuf, 42 | 43 | /// Whether to follow symbolic links when searching for git repos. 44 | /// 45 | /// Default: true 46 | pub follow_symlinks: bool, 47 | 48 | /// Whether to stay on the same filesystem (as `basedir`) when searching 49 | /// for git repos on Unix or Windows. 50 | /// 51 | /// Default: true [on supported platforms] 52 | pub same_filesystem: bool, 53 | 54 | /// Path patterns to ignore when searching for git repositories. 55 | /// 56 | /// Default: none 57 | pub ignored_patterns: Vec, 58 | 59 | /// The git-global subcommand to run when unspecified. 60 | /// 61 | /// Default: `status` 62 | pub default_cmd: String, 63 | 64 | /// Whether to enable verbose mode. 65 | /// 66 | /// Default: false 67 | pub verbose: bool, 68 | 69 | /// Whether to show untracked files in output. 70 | /// 71 | /// Default: true 72 | pub show_untracked: bool, 73 | 74 | /// Optional path to a cache file for git-global's usage. 75 | /// 76 | /// Default: `repos.txt` in the user's XDG cache directory, if we understand 77 | /// XDG for the host system. 78 | pub cache_file: Option, 79 | 80 | /// Optional path to our manpage, regardless of whether it's installed. 81 | /// 82 | /// Default: `git-global.1` in the relevant manpages directory, if we 83 | /// understand where that should be for the host system. 84 | pub manpage_file: Option, 85 | } 86 | 87 | impl Default for Config { 88 | fn default() -> Self { 89 | Config::new() 90 | } 91 | } 92 | 93 | impl Config { 94 | /// Create a new `Config` with the default behavior, first checking global 95 | /// git config options in ~/.gitconfig, then using defaults: 96 | pub fn new() -> Self { 97 | // Find the user's home directory. 98 | let homedir = UserDirs::new() 99 | .expect("Could not determine home directory.") 100 | .home_dir() 101 | .to_path_buf(); 102 | // Set the options that aren't user-configurable. 103 | let cache_file = 104 | ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) 105 | .map(|project_dirs| project_dirs.cache_dir().join(CACHE_FILE)); 106 | let manpage_file = match env::consts::OS { 107 | // Consider ~/.local/share/man/man1/, too. 108 | "linux" => Some(PathBuf::from("/usr/share/man/man1/git-global.1")), 109 | "macos" => Some(PathBuf::from("/usr/share/man/man1/git-global.1")), 110 | "windows" => env::var("MSYSTEM").ok().and_then(|val| { 111 | (val == "MINGW64").then(|| { 112 | PathBuf::from("/mingw64/share/doc/git-doc/git-global.html") 113 | }) 114 | }), 115 | _ => None, 116 | }; 117 | match ::git2::Config::open_default() { 118 | Ok(cfg) => Config { 119 | basedir: cfg.get_path(SETTING_BASEDIR).unwrap_or(homedir), 120 | follow_symlinks: cfg 121 | .get_bool(SETTING_FOLLOW_SYMLINKS) 122 | .unwrap_or(DEFAULT_FOLLOW_SYMLINKS), 123 | same_filesystem: cfg 124 | .get_bool(SETTING_SAME_FILESYSTEM) 125 | .unwrap_or(DEFAULT_SAME_FILESYSTEM), 126 | ignored_patterns: cfg 127 | .get_string(SETTING_IGNORE) 128 | .unwrap_or_default() 129 | .split(',') 130 | .map(|p| p.trim().to_string()) 131 | .collect(), 132 | default_cmd: cfg 133 | .get_string(SETTING_DEFAULT_CMD) 134 | .unwrap_or_else(|_| String::from(DEFAULT_CMD)), 135 | verbose: cfg 136 | .get_bool(SETTING_VERBOSE) 137 | .unwrap_or(DEFAULT_VERBOSE), 138 | show_untracked: cfg 139 | .get_bool(SETTING_SHOW_UNTRACKED) 140 | .unwrap_or(DEFAULT_SHOW_UNTRACKED), 141 | cache_file, 142 | manpage_file, 143 | }, 144 | Err(_) => { 145 | // Build the default configuration. 146 | Config { 147 | basedir: homedir, 148 | follow_symlinks: DEFAULT_FOLLOW_SYMLINKS, 149 | same_filesystem: DEFAULT_SAME_FILESYSTEM, 150 | ignored_patterns: vec![], 151 | default_cmd: String::from(DEFAULT_CMD), 152 | verbose: DEFAULT_VERBOSE, 153 | show_untracked: DEFAULT_SHOW_UNTRACKED, 154 | cache_file, 155 | manpage_file, 156 | } 157 | } 158 | } 159 | } 160 | 161 | /// Returns all known git repos, populating the cache first, if necessary. 162 | pub fn get_repos(&mut self) -> Vec { 163 | if !self.has_cache() { 164 | let repos = self.find_repos(); 165 | self.cache_repos(&repos); 166 | } 167 | self.get_cached_repos() 168 | } 169 | 170 | /// Clears the cache of known git repos, forcing a re-scan on the next 171 | /// `get_repos()` call. 172 | pub fn clear_cache(&mut self) { 173 | if self.has_cache() { 174 | if let Some(file) = &self.cache_file { 175 | remove_file(file).expect("Failed to delete cache file."); 176 | } 177 | } 178 | } 179 | 180 | /// Returns `true` if this directory entry should be included in scans. 181 | fn filter(&self, entry: &DirEntry) -> bool { 182 | if let Some(entry_path) = entry.path().to_str() { 183 | self.ignored_patterns 184 | .iter() 185 | .filter(|p| p != &"") 186 | .all(|pattern| !entry_path.contains(pattern)) 187 | } else { 188 | // Skip invalid file name 189 | false 190 | } 191 | } 192 | 193 | /// Walks the configured base directory, looking for git repos. 194 | fn find_repos(&self) -> Vec { 195 | let mut repos = Vec::new(); 196 | println!( 197 | "Scanning for git repos under {}; this may take a while...", 198 | self.basedir.display() 199 | ); 200 | let mut n_dirs = 0; 201 | let walker = WalkDir::new(&self.basedir) 202 | .follow_links(self.follow_symlinks) 203 | .same_file_system(self.same_filesystem); 204 | for entry in walker 205 | .into_iter() 206 | .filter_entry(|e| self.filter(e)) 207 | .flatten() 208 | { 209 | if entry.file_type().is_dir() { 210 | n_dirs += 1; 211 | if entry.file_name() == ".git" { 212 | let parent_path = entry 213 | .path() 214 | .parent() 215 | .expect("Could not determine parent."); 216 | if let Some(path) = parent_path.to_str() { 217 | repos.push(Repo::new(path.to_string())); 218 | } 219 | } 220 | if self.verbose { 221 | if let Some(size) = termsize::get() { 222 | let prefix = format!( 223 | "\r... found {} repos; scanning directory #{}: ", 224 | repos.len(), 225 | n_dirs 226 | ); 227 | let width = size.cols as usize - prefix.len() - 1; 228 | let mut cur_path = 229 | String::from(entry.path().to_str().unwrap()); 230 | let byte_width = 231 | match cur_path.char_indices().nth(width) { 232 | None => &cur_path, 233 | Some((idx, _)) => &cur_path[..idx], 234 | } 235 | .len(); 236 | cur_path.truncate(byte_width); 237 | print!("{}{: bool { 251 | self.cache_file.as_ref().is_some_and(|f| f.exists()) 252 | } 253 | 254 | /// Writes the given repo paths to the cache file. 255 | fn cache_repos(&self, repos: &[Repo]) { 256 | if let Some(file) = &self.cache_file { 257 | if !file.exists() { 258 | if let Some(parent) = &file.parent() { 259 | create_dir_all(parent) 260 | .expect("Could not create cache directory.") 261 | } 262 | } 263 | let mut f = 264 | File::create(file).expect("Could not create cache file."); 265 | for repo in repos.iter() { 266 | match writeln!(f, "{}", repo.path()) { 267 | Ok(_) => (), 268 | Err(e) => panic!("Problem writing cache file: {}", e), 269 | } 270 | } 271 | } 272 | } 273 | 274 | /// Returns the list of repos found in the cache file. 275 | fn get_cached_repos(&self) -> Vec { 276 | let mut repos = Vec::new(); 277 | if let Some(file) = &self.cache_file { 278 | if file.exists() { 279 | let f = File::open(file).expect("Could not open cache file."); 280 | let reader = BufReader::new(f); 281 | for repo_path in reader.lines().map_while(Result::ok) { 282 | if !Path::new(&repo_path).exists() { 283 | continue; 284 | } 285 | repos.push(Repo::new(repo_path)) 286 | } 287 | } 288 | } 289 | repos 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Error handling for git-global. 2 | 3 | use std::error::Error; 4 | use std::fmt; 5 | use std::io; 6 | use std::result; 7 | 8 | /// An error. 9 | #[derive(Debug)] 10 | pub enum GitGlobalError { 11 | BadSubcommand(String), 12 | Generic, 13 | } 14 | 15 | /// Our `Result` alias with `GitGlobalError` as the error type. 16 | pub type Result = result::Result; 17 | 18 | impl fmt::Display for GitGlobalError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | use GitGlobalError::*; 21 | match *self { 22 | BadSubcommand(ref cmd) => { 23 | write!(f, "Unknown subcommand \"{}\".", cmd) 24 | } 25 | Generic => write!(f, "An error occured :(."), 26 | } 27 | } 28 | } 29 | 30 | impl Error for GitGlobalError { 31 | fn description(&self) -> &str { 32 | use GitGlobalError::*; 33 | match *self { 34 | BadSubcommand(_) => "unknown subcommand", 35 | Generic => "an error occurred :(", 36 | } 37 | } 38 | } 39 | 40 | impl From for GitGlobalError { 41 | #[allow(unused_variables)] 42 | fn from(err: io::Error) -> GitGlobalError { 43 | GitGlobalError::Generic 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/generate_manpage.rs: -------------------------------------------------------------------------------- 1 | use man::prelude::*; 2 | 3 | fn main() { 4 | // TODO(peap): Consider switching to clap_mangen. 5 | let app = git_global::get_clap_app(); 6 | let name_and_email: Vec<&str> = 7 | app.get_author().unwrap().split(" <").collect(); 8 | let name = name_and_email[0]; 9 | let email = name_and_email[1].strip_suffix(">").unwrap(); 10 | let mut page = Manual::new(app.get_name()) 11 | .about(app.get_about().unwrap().to_string()) 12 | .author(Author::new(name).email(email)) 13 | .custom(Section::new("version").paragraph(&format!( 14 | "Crate version {}", 15 | app.get_version().unwrap() 16 | ))); 17 | for arg in app.get_arguments() { 18 | page = page.flag( 19 | Flag::new() 20 | .short(&arg.get_short().unwrap().to_string()) 21 | .long(arg.get_long().unwrap()) 22 | .help(&arg.get_help().unwrap().to_string()), 23 | ); 24 | } 25 | let mut commands_section = Section::new("subcommands").paragraph( 26 | "The following subcommands are supported by git global; \ 27 | use git's global config to set your default choice.", 28 | ); 29 | for cmd in app.get_subcommands() { 30 | commands_section = commands_section.paragraph(&format!( 31 | "{}: {}", 32 | cmd.get_name(), 33 | cmd.get_about().unwrap() 34 | )); 35 | } 36 | page = page.custom(commands_section); 37 | println!("{}", page.render()); 38 | } 39 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Keep track of all the git repositories on your machine. 2 | //! 3 | //! This crate houses the binary and library for the git-global subcommand, a 4 | //! way to find, query statuses, and gain other insights about all the git repos 5 | //! on your machine. The binary can be installed with cargo: `cargo install 6 | //! git-global`. 7 | //! 8 | //! # Command-line Usage 9 | //! 10 | //! ```bash 11 | //! $ git global [status] # show `git status -s` for all your git repos 12 | //! $ git global info # show information about git-global itself 13 | //! $ git global list # show all git repos git-global knows about 14 | //! $ git global scan # search your filesystem for git repos and update cache 15 | //! # ... 16 | //! $ git global help # show usage and all subcommands 17 | //! ``` 18 | //! 19 | //! # Public Interface 20 | //! 21 | //! The git-global project's primary goal is to produce a useful binary. There's 22 | //! no driving force to provide a very good library for other Rust projects to 23 | //! use, so this documentation primarily serves to illustrate how the codebase 24 | //! is structured. (If a library use-case arises, however, that would be fine.) 25 | //! 26 | //! The [`Repo`] struct is a git repository that is identified by the full path 27 | //! to its base directory (instead of, say, its `.git` directory). 28 | //! 29 | //! The [`Config`] struct holds a user's git-global configuration information, 30 | //! which usually merges some default values with values in the `[global]` 31 | //! section of the user's global `.gitconfig` file. It provides access to the 32 | //! list of known `Repo`s via the `get_repos()` method, which reads from a cache 33 | //! file, populating it for the first time after performing a filesystem scan, 34 | //! if necessary. 35 | //! 36 | //! A [`Report`] contains messages added by a subcommand about the overall 37 | //! results of what it did, as well as messages about the specific `Repo`s to 38 | //! which that subcommand applies. All subcommand modules expose an `execute()` 39 | //! function that takes ownership of a `Config` struct and returns a 40 | //! `Result`. These subcommands live in the [`subcommands`][subcommands] 41 | //! module. 42 | //! 43 | //! The [`run_from_command_line()`][rfcl] function handles running git-global 44 | //! from the command line and serves as the entry point for the binary. 45 | //! 46 | //! [`Config`]: struct.Config.html 47 | //! [`Repo`]: struct.Repo.html 48 | //! [`Report`]: struct.Report.html 49 | //! [rfcl]: fn.run_from_command_line.html 50 | //! [subcommands]: subcommands/index.html 51 | 52 | mod cli; 53 | mod config; 54 | mod errors; 55 | mod repo; 56 | mod report; 57 | pub mod subcommands; // Using `pub mod` so we see the docs. 58 | 59 | pub use cli::{get_clap_app, run_from_command_line}; 60 | pub use config::Config; 61 | pub use errors::{GitGlobalError, Result}; 62 | pub use repo::Repo; 63 | pub use report::Report; 64 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Entry point for the binary. 2 | 3 | use std::process::exit; 4 | 5 | /// Runs git-global from the command line, exiting with its return value. 6 | fn main() { 7 | exit(git_global::run_from_command_line()) 8 | } 9 | -------------------------------------------------------------------------------- /src/repo.rs: -------------------------------------------------------------------------------- 1 | //! Git repository representation for git-global. 2 | 3 | use std::fmt; 4 | use std::path::PathBuf; 5 | 6 | use serde::Serialize; 7 | 8 | /// A git repository, represented by the full path to its base directory. 9 | #[derive(Clone, Eq, Hash, PartialEq, Serialize)] 10 | pub struct Repo { 11 | path: PathBuf, 12 | } 13 | 14 | impl Repo { 15 | pub fn new(path: String) -> Repo { 16 | Repo { 17 | path: PathBuf::from(path), 18 | } 19 | } 20 | 21 | /// Returns the `git2::Repository` equivalent of this repo. 22 | pub fn as_git2_repo(&self) -> ::git2::Repository { 23 | ::git2::Repository::open(&self.path).unwrap_or_else(|_| { 24 | panic!( 25 | "Could not open {} as a git repo. Perhaps you should run \ 26 | `git global scan` again.", 27 | &self.path.as_path().to_str().unwrap() 28 | ) 29 | }) 30 | } 31 | 32 | /// Returns the full path to the repo as a `String`. 33 | pub fn path(&self) -> String { 34 | self.path.to_str().unwrap().to_string() 35 | } 36 | 37 | /// Returns "short format" status output. 38 | pub fn get_status_lines( 39 | &self, 40 | mut status_opts: ::git2::StatusOptions, 41 | ) -> Vec { 42 | let git2_repo = self.as_git2_repo(); 43 | let statuses = git2_repo 44 | .statuses(Some(&mut status_opts)) 45 | .unwrap_or_else(|_| panic!("Could not get statuses for {}.", self)); 46 | statuses 47 | .iter() 48 | .map(|entry| { 49 | let path = entry.path().unwrap(); 50 | let status = entry.status(); 51 | let status_for_path = get_short_format_status(status); 52 | format!("{} {}", status_for_path, path) 53 | }) 54 | .collect() 55 | } 56 | 57 | /// Transforms a git2::Branch into a git2::Commit 58 | fn branch_to_commit(branch: git2::Branch) -> Option { 59 | branch.into_reference().peel_to_commit().ok() 60 | } 61 | 62 | /// Walks through revisions, returning all ancestor Oids of a Commit 63 | fn get_log( 64 | repo: &git2::Repository, 65 | commit: git2::Commit, 66 | ) -> Vec { 67 | let mut revwalk = repo.revwalk().unwrap(); 68 | revwalk.push(commit.id()).unwrap(); 69 | revwalk.filter_map(|id| id.ok()).collect::>() 70 | } 71 | 72 | /// Returns true if commits of local branches are ahead of those on remote branches 73 | pub fn is_ahead(&self) -> bool { 74 | let repo = self.as_git2_repo(); 75 | let local_branches = match repo.branches(Some(git2::BranchType::Local)) 76 | { 77 | Ok(branches) => branches, 78 | Err(_) => return false, 79 | }; 80 | let remote_branches = 81 | match repo.branches(Some(git2::BranchType::Remote)) { 82 | Ok(branches) => branches, 83 | Err(_) => return false, 84 | }; 85 | 86 | let remote_commit_ids = remote_branches 87 | .filter_map(|branch| branch.ok().map(|b| b.0)) 88 | .filter_map(Self::branch_to_commit) 89 | .flat_map(|commit| Self::get_log(&repo, commit)) 90 | .collect::>(); 91 | 92 | #[allow(clippy::let_and_return)] 93 | let is_ahead = local_branches 94 | .filter_map(|branch| branch.ok().map(|b| b.0)) 95 | .any(|branch| match Self::branch_to_commit(branch) { 96 | Some(commit) => !remote_commit_ids.contains(&commit.id()), 97 | None => false, 98 | }); 99 | is_ahead 100 | } 101 | 102 | /// Returns the list of stash entries for the repo. 103 | pub fn get_stash_list(&self) -> Vec { 104 | let mut stash = vec![]; 105 | self.as_git2_repo() 106 | .stash_foreach(|index, name, _oid| { 107 | stash.push(format!("stash@{{{}}}: {}", index, name)); 108 | true 109 | }) 110 | .unwrap(); 111 | stash 112 | } 113 | } 114 | 115 | impl fmt::Display for Repo { 116 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 117 | write!(f, "{}", self.path()) 118 | } 119 | } 120 | 121 | /// Translates a file's status flags to their "short format" representation. 122 | /// 123 | /// Follows an example in the git2-rs crate's `examples/status.rs`. 124 | fn get_short_format_status(status: ::git2::Status) -> String { 125 | let mut istatus = match status { 126 | s if s.is_index_new() => 'A', 127 | s if s.is_index_modified() => 'M', 128 | s if s.is_index_deleted() => 'D', 129 | s if s.is_index_renamed() => 'R', 130 | s if s.is_index_typechange() => 'T', 131 | _ => ' ', 132 | }; 133 | let mut wstatus = match status { 134 | s if s.is_wt_new() => { 135 | if istatus == ' ' { 136 | istatus = '?'; 137 | } 138 | '?' 139 | } 140 | s if s.is_wt_modified() => 'M', 141 | s if s.is_wt_deleted() => 'D', 142 | s if s.is_wt_renamed() => 'R', 143 | s if s.is_wt_typechange() => 'T', 144 | _ => ' ', 145 | }; 146 | if status.is_ignored() { 147 | istatus = '!'; 148 | wstatus = '!'; 149 | } 150 | if status.is_conflicted() { 151 | istatus = 'C'; 152 | wstatus = 'C'; 153 | } 154 | // TODO: handle submodule statuses? 155 | format!("{}{}", istatus, wstatus) 156 | } 157 | -------------------------------------------------------------------------------- /src/report.rs: -------------------------------------------------------------------------------- 1 | //! Reporting for git-global. 2 | 3 | use std::collections::HashMap; 4 | use std::io::Write; 5 | 6 | use serde_json::json; 7 | 8 | use crate::repo::Repo; 9 | 10 | /// A report containing the results of a git-global subcommand. 11 | /// 12 | /// Contains overall messages and per-repo messages. 13 | pub struct Report { 14 | messages: Vec, 15 | repo_messages: HashMap>, 16 | repos: Vec, 17 | pad_repo_output: bool, 18 | } 19 | 20 | impl Report { 21 | /// Create a new `Report` for the given `Repo`s.. 22 | pub fn new(repos: &[Repo]) -> Report { 23 | let mut repo_messages: HashMap> = HashMap::new(); 24 | for repo in repos { 25 | repo_messages.insert(repo.clone(), Vec::new()); 26 | } 27 | Report { 28 | messages: Vec::new(), 29 | repos: repos.to_owned(), 30 | repo_messages, 31 | pad_repo_output: false, 32 | } 33 | } 34 | 35 | /// Declares the desire to separate output when showing per-repo messages. 36 | /// 37 | /// Sets flag that indicates a blank line should be inserted between 38 | /// messages for different repos when printing per-repo output. 39 | pub fn pad_repo_output(&mut self) { 40 | self.pad_repo_output = true; 41 | } 42 | 43 | /// Adds a message that applies to the overall operation. 44 | pub fn add_message(&mut self, message: String) { 45 | self.messages.push(message); 46 | } 47 | 48 | /// Adds a message that applies to the given repo. 49 | pub fn add_repo_message(&mut self, repo: &Repo, data_line: String) { 50 | if let Some(item) = self.repo_messages.get_mut(repo) { 51 | item.push(data_line) 52 | } 53 | } 54 | 55 | /// Writes all result messages to the given writer, as text. 56 | pub fn print(&self, writer: &mut W) { 57 | for msg in self.messages.iter() { 58 | writeln!(writer, "{}", msg).unwrap(); 59 | } 60 | for repo in self.repos.iter() { 61 | let messages = self.repo_messages.get(repo).unwrap(); 62 | if !messages.is_empty() { 63 | writeln!(writer, "{}", repo).unwrap(); 64 | for line in messages.iter().filter(|l| !l.is_empty()) { 65 | writeln!(writer, "{}", line).unwrap(); 66 | } 67 | if self.pad_repo_output { 68 | writeln!(writer).unwrap(); 69 | } 70 | } 71 | } 72 | } 73 | 74 | /// Writes all result messages to the given writer, as JSON. 75 | pub fn print_json(&self, writer: &mut W) { 76 | let mut repo_messages: HashMap> = HashMap::new(); 77 | for (repo, messages) in self.repo_messages.iter() { 78 | let msgs = messages.iter().filter(|l| !l.is_empty()); 79 | repo_messages.insert(repo.path(), msgs.collect()); 80 | } 81 | let json = json!({ 82 | "error": false, 83 | "messages": self.messages, 84 | "repo_messages": repo_messages 85 | }); 86 | writeln!(writer, "{:#}", json).unwrap(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/subcommands.rs: -------------------------------------------------------------------------------- 1 | //! Subcommand implementations and dispatch function `run()`. 2 | pub mod ahead; 3 | pub mod info; 4 | pub mod install_manpage; 5 | pub mod list; 6 | pub mod scan; 7 | pub mod staged; 8 | pub mod stashed; 9 | pub mod status; 10 | pub mod unstaged; 11 | 12 | use crate::config::Config; 13 | use crate::errors::{GitGlobalError, Result}; 14 | use crate::report::Report; 15 | 16 | /// Run a subcommand, returning a `Report`. 17 | /// 18 | /// If `None` is given for the optional subcommand, run `config.default_cmd`. 19 | /// Else, try to match the given `&str` to a known subcommand. 20 | pub fn run(maybe_subcmd: Option<&str>, config: Config) -> Result { 21 | let command = maybe_subcmd.unwrap_or(&config.default_cmd); 22 | match command { 23 | "info" => info::execute(config), 24 | "list" => list::execute(config), 25 | "scan" => scan::execute(config), 26 | "staged" => staged::execute(config), 27 | "stashed" => stashed::execute(config), 28 | "status" => status::execute(config), 29 | "unstaged" => unstaged::execute(config), 30 | "ahead" => ahead::execute(config), 31 | "install-manpage" => install_manpage::execute(config), 32 | cmd => Err(GitGlobalError::BadSubcommand(cmd.to_string())), 33 | } 34 | } 35 | 36 | /// Return the list of all subcommand names and descriptions. 37 | /// 38 | /// Used for building the clap::Command in the cli module. 39 | pub fn get_subcommands() -> Vec<(&'static str, &'static str)> { 40 | vec![ 41 | ( 42 | "ahead", 43 | "Shows repos with changes that are not pushed to a remote", 44 | ), 45 | ("info", "Shows meta-information about git-global"), 46 | ( 47 | "install-manpage", 48 | "Attempts to install git-global's man page", 49 | ), 50 | ("list", "Lists all known repos"), 51 | ("scan", "Updates cache of known repos"), 52 | ( 53 | "staged", 54 | "Shows git index status for repos with staged changes", 55 | ), 56 | ("stashed", "Shows repos with stashed changes"), 57 | ( 58 | "status", 59 | "Shows status (`git status -s`) for repos with any changes", 60 | ), 61 | ( 62 | "unstaged", 63 | "Shows working dir status for repos with unstaged changes", 64 | ), 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/subcommands/ahead.rs: -------------------------------------------------------------------------------- 1 | //! The `ahead` subcommand: shows repositories that have commits not pushed to a remote 2 | 3 | use std::sync::{mpsc, Arc}; 4 | use std::thread; 5 | 6 | use crate::config::Config; 7 | use crate::errors::Result; 8 | use crate::repo::Repo; 9 | use crate::report::Report; 10 | 11 | /// Runs the `ahead` subcommand. 12 | pub fn execute(mut config: Config) -> Result { 13 | let repos = config.get_repos(); 14 | let n_repos = repos.len(); 15 | let mut report = Report::new(&repos); 16 | // TODO: limit number of threads, perhaps with mpsc::sync_channel(n)? 17 | let (tx, rx) = mpsc::channel(); 18 | for repo in repos { 19 | let tx = tx.clone(); 20 | let repo = Arc::new(repo); 21 | thread::spawn(move || { 22 | let path = repo.path(); 23 | let ahead = repo.is_ahead(); 24 | tx.send((path, ahead)).unwrap(); 25 | }); 26 | } 27 | for _ in 0..n_repos { 28 | let (path, ahead) = rx.recv().unwrap(); 29 | let repo = Repo::new(path.to_string()); 30 | if ahead { 31 | report.add_repo_message(&repo, String::new()); 32 | } 33 | } 34 | Ok(report) 35 | } 36 | -------------------------------------------------------------------------------- /src/subcommands/info.rs: -------------------------------------------------------------------------------- 1 | //! The `info` subcommand: shows metadata about the git-global installation. 2 | 3 | use clap::crate_version; 4 | 5 | use std::env; 6 | use std::path::PathBuf; 7 | use std::time::SystemTime; 8 | 9 | use crate::config::Config; 10 | use crate::errors::Result; 11 | use crate::report::Report; 12 | 13 | /// Returns the age of a file in terms of days, hours, minutes, and seconds. 14 | fn get_age(filename: PathBuf) -> Option { 15 | filename 16 | .metadata() 17 | .ok() 18 | .and_then(|metadata| metadata.modified().ok()) 19 | .and_then(|mtime| SystemTime::now().duration_since(mtime).ok()) 20 | .map(|dur| { 21 | let ts = dur.as_secs(); 22 | let days = ts / (24 * 60 * 60); 23 | let hours = (ts / (60 * 60)) - (days * 24); 24 | let mins = (ts / 60) - (days * 24 * 60) - (hours * 60); 25 | let secs = 26 | ts - (days * 24 * 60 * 60) - (hours * 60 * 60) - (mins * 60); 27 | format!("{}d, {}h, {}m, {}s", days, hours, mins, secs) 28 | }) 29 | } 30 | 31 | /// Gathers metadata about the git-global installation. 32 | pub fn execute(mut config: Config) -> Result { 33 | let repos = config.get_repos(); 34 | let mut report = Report::new(&repos); 35 | let version = crate_version!().to_string(); 36 | // beginning of underline: git-global x.x.x 37 | let mut underline = "===========".to_string(); 38 | for _ in 0..version.len() { 39 | underline.push('='); 40 | } 41 | report.add_message(format!("git-global {}", version)); 42 | report.add_message(underline); 43 | report.add_message(format!("Number of repos: {}", repos.len())); 44 | report.add_message(format!("Base directory: {}", config.basedir.display())); 45 | report.add_message("Ignored patterns:".to_string()); 46 | for pat in config.ignored_patterns.iter() { 47 | report.add_message(format!(" {}", pat)); 48 | } 49 | report.add_message(format!("Default command: {}", config.default_cmd)); 50 | report.add_message(format!("Verbose: {}", config.verbose)); 51 | report.add_message(format!("Show untracked: {}", config.show_untracked)); 52 | if let Some(cache_file) = config.cache_file { 53 | report.add_message(format!("Cache file: {}", cache_file.display())); 54 | if let Some(age) = get_age(cache_file) { 55 | report.add_message(format!("Cache file age: {}", age)); 56 | } 57 | } else { 58 | report.add_message("Cache file: ".to_string()); 59 | } 60 | if let Some(manpage_file) = config.manpage_file { 61 | report.add_message(format!("Manpage file: {}", manpage_file.display())); 62 | } else { 63 | report.add_message("Manpage file: ".to_string()); 64 | } 65 | report.add_message(format!("Detected OS: {}", env::consts::OS)); 66 | Ok(report) 67 | } 68 | -------------------------------------------------------------------------------- /src/subcommands/install_manpage.rs: -------------------------------------------------------------------------------- 1 | //! The `install-manpage` subcommand: attempts to install a man page. 2 | 3 | use crate::config::Config; 4 | use crate::errors::Result; 5 | use crate::report::Report; 6 | 7 | // TODO(peap): Add option to just generate the file for the user to stick somewhere? 8 | 9 | /// Attempts to install git-global's man page to the relevant directory. 10 | /// This is a work-around to not maintaining distribution-specific packages 11 | /// and Cargo not providing this functionality for crates. 12 | pub fn execute(mut config: Config) -> Result { 13 | let repos = config.get_repos(); 14 | let mut report = Report::new(&repos); 15 | report.add_message("This feature is a work-in-progress.".to_string()); 16 | report.add_message( 17 | "In the meantime, you can find the manpage at \ 18 | https://raw.githubusercontent.com/peap/git-global/master/doc/git-global.1".to_string() 19 | ); 20 | if let Some(manpage_file) = config.manpage_file { 21 | report.add_message(format!( 22 | "...would write file to {}", 23 | manpage_file.display() 24 | )); 25 | } else { 26 | report.add_message("...not sure where to put it!".to_string()); 27 | } 28 | Ok(report) 29 | } 30 | -------------------------------------------------------------------------------- /src/subcommands/list.rs: -------------------------------------------------------------------------------- 1 | //! The `list` subcommand: lists all repos known to git-global. 2 | 3 | use crate::config::Config; 4 | use crate::errors::Result; 5 | use crate::report::Report; 6 | 7 | /// Forces the display of each repo path, without any extra output. 8 | pub fn execute(mut config: Config) -> Result { 9 | let repos = config.get_repos(); 10 | let mut report = Report::new(&repos); 11 | for repo in repos.iter() { 12 | // Report.print() already prints out the repo name if it has any 13 | // messages, so just add an empty string to force display of the repo 14 | // name. 15 | report.add_repo_message(repo, String::new()); 16 | } 17 | Ok(report) 18 | } 19 | -------------------------------------------------------------------------------- /src/subcommands/scan.rs: -------------------------------------------------------------------------------- 1 | //! The `scan` subcommand: scans the filesystem for git repos. 2 | //! 3 | //! By default, the user's home directory is walked, but this starting point can 4 | //! be configured in `~/.gitconfig`: 5 | //! 6 | //! ```bash 7 | //! $ git config --global global.basedir /some/path 8 | //! ``` 9 | //! 10 | //! The `scan` subcommand caches the list of git repos paths it finds, and can 11 | //! be rerun at any time to refresh the list. 12 | 13 | use crate::config::Config; 14 | use crate::errors::Result; 15 | use crate::report::Report; 16 | 17 | /// Clears the cache, forces a rescan, and says how many repos were found. 18 | pub fn execute(mut config: Config) -> Result { 19 | config.clear_cache(); 20 | let repos = config.get_repos(); 21 | let mut report = Report::new(&repos); 22 | report.add_message(format!( 23 | "Found {} repos. Use `git global list` to show them.", 24 | repos.len() 25 | )); 26 | Ok(report) 27 | } 28 | -------------------------------------------------------------------------------- /src/subcommands/staged.rs: -------------------------------------------------------------------------------- 1 | //! The `staged` subcommand: shows `git status -s` for staged changes in all 2 | //! known repos with such changes. 3 | 4 | use std::sync::{mpsc, Arc}; 5 | use std::thread; 6 | 7 | use crate::config::Config; 8 | use crate::errors::Result; 9 | use crate::repo::Repo; 10 | use crate::report::Report; 11 | 12 | /// Runs the `staged` subcommand. 13 | pub fn execute(mut config: Config) -> Result { 14 | let include_untracked = config.show_untracked; 15 | let repos = config.get_repos(); 16 | let n_repos = repos.len(); 17 | let mut report = Report::new(&repos); 18 | report.pad_repo_output(); 19 | // TODO: limit number of threads, perhaps with mpsc::sync_channel(n)? 20 | let (tx, rx) = mpsc::channel(); 21 | for repo in repos { 22 | let tx = tx.clone(); 23 | let repo = Arc::new(repo); 24 | thread::spawn(move || { 25 | let path = repo.path(); 26 | let mut status_opts = ::git2::StatusOptions::new(); 27 | status_opts 28 | .show(::git2::StatusShow::Index) 29 | .include_untracked(include_untracked) 30 | .include_ignored(false); 31 | let lines = repo.get_status_lines(status_opts); 32 | tx.send((path, lines)).unwrap(); 33 | }); 34 | } 35 | for _ in 0..n_repos { 36 | let (path, lines) = rx.recv().unwrap(); 37 | let repo = Repo::new(path.to_string()); 38 | for line in lines { 39 | report.add_repo_message(&repo, line); 40 | } 41 | } 42 | Ok(report) 43 | } 44 | -------------------------------------------------------------------------------- /src/subcommands/stashed.rs: -------------------------------------------------------------------------------- 1 | //! The `stashed` subcommand: shows stash list for all known repos with stashes 2 | 3 | use std::sync::{mpsc, Arc}; 4 | use std::thread; 5 | 6 | use crate::config::Config; 7 | use crate::errors::Result; 8 | use crate::repo::Repo; 9 | use crate::report::Report; 10 | 11 | /// Runs the `stashed` subcommand. 12 | pub fn execute(mut config: Config) -> Result { 13 | let repos = config.get_repos(); 14 | let n_repos = repos.len(); 15 | let mut report = Report::new(&repos); 16 | report.pad_repo_output(); 17 | // TODO: limit number of threads, perhaps with mpsc::sync_channel(n)? 18 | let (tx, rx) = mpsc::channel(); 19 | for repo in repos { 20 | let tx = tx.clone(); 21 | let repo = Arc::new(repo); 22 | thread::spawn(move || { 23 | let path = repo.path(); 24 | let stash = repo.get_stash_list(); 25 | tx.send((path, stash)).unwrap(); 26 | }); 27 | } 28 | for _ in 0..n_repos { 29 | let (path, stash) = rx.recv().unwrap(); 30 | let repo = Repo::new(path.to_string()); 31 | for line in stash { 32 | report.add_repo_message(&repo, line); 33 | } 34 | } 35 | Ok(report) 36 | } 37 | -------------------------------------------------------------------------------- /src/subcommands/status.rs: -------------------------------------------------------------------------------- 1 | //! The `status` subcommand: shows `git status -s` for all known repos with any 2 | //! changes to the index or working directory. 3 | 4 | use std::sync::{mpsc, Arc}; 5 | use std::thread; 6 | 7 | use crate::config::Config; 8 | use crate::errors::Result; 9 | use crate::repo::Repo; 10 | use crate::report::Report; 11 | 12 | /// Runs the `status` subcommand. 13 | pub fn execute(mut config: Config) -> Result { 14 | let include_untracked = config.show_untracked; 15 | let repos = config.get_repos(); 16 | let n_repos = repos.len(); 17 | let mut report = Report::new(&repos); 18 | report.pad_repo_output(); 19 | // TODO: limit number of threads, perhaps with mpsc::sync_channel(n)? 20 | let (tx, rx) = mpsc::channel(); 21 | for repo in repos { 22 | let tx = tx.clone(); 23 | let repo = Arc::new(repo); 24 | thread::spawn(move || { 25 | let path = repo.path(); 26 | let mut status_opts = ::git2::StatusOptions::new(); 27 | status_opts 28 | .show(::git2::StatusShow::IndexAndWorkdir) 29 | .include_untracked(include_untracked) 30 | .include_ignored(false); 31 | let lines = repo.get_status_lines(status_opts); 32 | tx.send((path, lines)).unwrap(); 33 | }); 34 | } 35 | for _ in 0..n_repos { 36 | let (path, lines) = rx.recv().unwrap(); 37 | let repo = Repo::new(path.to_string()); 38 | for line in lines { 39 | report.add_repo_message(&repo, line); 40 | } 41 | } 42 | Ok(report) 43 | } 44 | -------------------------------------------------------------------------------- /src/subcommands/unstaged.rs: -------------------------------------------------------------------------------- 1 | //! The `unstaged` subcommand: shows `git status -s` for unstaged changes in all 2 | //! known repos with such changes. 3 | 4 | use std::sync::{mpsc, Arc}; 5 | use std::thread; 6 | 7 | use crate::config::Config; 8 | use crate::errors::Result; 9 | use crate::repo::Repo; 10 | use crate::report::Report; 11 | 12 | /// Runs the `unstaged` subcommand. 13 | pub fn execute(mut config: Config) -> Result { 14 | let include_untracked = config.show_untracked; 15 | let repos = config.get_repos(); 16 | let n_repos = repos.len(); 17 | let mut report = Report::new(&repos); 18 | report.pad_repo_output(); 19 | // TODO: limit number of threads, perhaps with mpsc::sync_channel(n)? 20 | let (tx, rx) = mpsc::channel(); 21 | for repo in repos { 22 | let tx = tx.clone(); 23 | let repo = Arc::new(repo); 24 | thread::spawn(move || { 25 | let path = repo.path(); 26 | let mut status_opts = ::git2::StatusOptions::new(); 27 | status_opts 28 | .show(::git2::StatusShow::Workdir) 29 | .include_untracked(include_untracked) 30 | .include_ignored(false); 31 | let lines = repo.get_status_lines(status_opts); 32 | tx.send((path, lines)).unwrap(); 33 | }); 34 | } 35 | for _ in 0..n_repos { 36 | let (path, lines) = rx.recv().unwrap(); 37 | let repo = Repo::new(path.to_string()); 38 | for line in lines { 39 | report.add_repo_message(&repo, line); 40 | } 41 | } 42 | Ok(report) 43 | } 44 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn verify_cli() { 3 | let app = git_global::get_clap_app(); 4 | app.debug_assert(); 5 | } 6 | -------------------------------------------------------------------------------- /tests/repo.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | #[test] 4 | /// Test that we get an actual git repo, we can get a git2::Repository 5 | /// reference to it, and it's not bare. 6 | fn test_repo_initialization() { 7 | utils::with_temp_repo(|repo| { 8 | let git2_repo = repo.as_git2_repo(); 9 | assert!(!git2_repo.is_bare()); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /tests/subcommands.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use std::env; 4 | use std::io::Cursor; 5 | use std::path::PathBuf; 6 | 7 | use clap::crate_version; 8 | use regex::{escape, Regex}; 9 | 10 | use git_global::{subcommands, Report}; 11 | 12 | fn report_to_string(report: &Report) -> String { 13 | let mut out = Cursor::new(Vec::new()); 14 | report.print(&mut out); 15 | String::from_utf8(out.into_inner()).unwrap() 16 | } 17 | 18 | #[test] 19 | fn test_info() { 20 | utils::with_base_dir_of_three_repos(|mut config| { 21 | let basedir = config.basedir.clone(); 22 | let cache = config 23 | .cache_file 24 | .clone() 25 | .unwrap() 26 | .to_str() 27 | .unwrap() 28 | .to_string(); 29 | if config.manpage_file.is_none() { 30 | config.manpage_file = Some(PathBuf::from("/test")); 31 | } 32 | let manpage = config 33 | .manpage_file 34 | .clone() 35 | .unwrap() 36 | .to_str() 37 | .unwrap() 38 | .to_string(); 39 | let report = subcommands::info::execute(config).unwrap(); 40 | let expected = vec![ 41 | format!(r"^git-global {}$", crate_version!()), 42 | format!(r"^============+"), 43 | format!(r"^Number of repos: 3$"), 44 | format!(r"^Base directory: {}$", escape(basedir.to_str().unwrap())), 45 | format!(r"^Ignored patterns:$"), 46 | format!(r"^Default command: status$"), 47 | format!(r"^Verbose: false$"), 48 | format!(r"^Show untracked: true$"), 49 | format!(r"^Cache file: {}$", escape(&cache)), 50 | format!(r"^Cache file age: 0d, 0h, 0m, .s$"), 51 | format!(r"^Manpage file: {}$", escape(&manpage)), 52 | format!(r"^Detected OS: {}$", escape(env::consts::OS)), 53 | format!(r"^$"), 54 | ]; 55 | let output = report_to_string(&report); 56 | for (i, line) in output.lines().enumerate() { 57 | let pattern = &expected[i]; 58 | let re = Regex::new(pattern).unwrap(); 59 | assert!( 60 | re.is_match(line), 61 | "Line {} didn't match; got {}, want {}", 62 | i + 1, 63 | line, 64 | pattern 65 | ) 66 | } 67 | }); 68 | } 69 | 70 | #[test] 71 | fn test_list() { 72 | utils::with_base_dir_of_three_repos(|config| { 73 | let basedir = config.basedir.clone(); 74 | let report = subcommands::list::execute(config).unwrap(); 75 | // There are no global messages; the per-repo messages are simply a list 76 | // of the repo paths themselves. 77 | let expected = vec![ 78 | PathBuf::from(&basedir).join("a"), 79 | PathBuf::from(&basedir).join("b"), 80 | PathBuf::from(&basedir).join("c"), 81 | ]; 82 | let output = report_to_string(&report); 83 | for (i, line) in output.lines().enumerate() { 84 | assert_eq!(expected[i].to_str().unwrap(), line); 85 | } 86 | }); 87 | } 88 | 89 | #[test] 90 | fn test_scan() { 91 | utils::with_base_dir_of_three_repos(|config| { 92 | let report = subcommands::scan::execute(config).unwrap(); 93 | // There is one global message about the three repos we found. 94 | assert_eq!( 95 | report_to_string(&report), 96 | "Found 3 repos. Use `git global list` to show them.\n" 97 | ); 98 | }); 99 | } 100 | 101 | #[test] 102 | fn test_staged() { 103 | utils::with_base_dir_of_three_repos(|config| { 104 | let report = subcommands::staged::execute(config).unwrap(); 105 | // There are no global messages. 106 | assert_eq!(report_to_string(&report), ""); 107 | }); 108 | } 109 | 110 | #[test] 111 | fn test_stashes() { 112 | utils::with_base_dir_of_three_repos(|config| { 113 | let report = subcommands::stashed::execute(config).unwrap(); 114 | // There are no global messages. 115 | assert_eq!(report_to_string(&report), ""); 116 | }); 117 | } 118 | 119 | #[test] 120 | fn test_status() { 121 | utils::with_base_dir_of_three_repos(|config| { 122 | let report = subcommands::status::execute(config).unwrap(); 123 | // There are no global messages. 124 | assert_eq!(report_to_string(&report), ""); 125 | }); 126 | } 127 | 128 | #[test] 129 | fn test_unstaged() { 130 | utils::with_base_dir_of_three_repos(|config| { 131 | let report = subcommands::unstaged::execute(config).unwrap(); 132 | // There are no global messages. 133 | assert_eq!(report_to_string(&report), ""); 134 | }); 135 | } 136 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use git_global::{Config, Repo}; 4 | 5 | /// Initialize an empty git repo in a temporary directory, then run a closure 6 | /// that takes that Repo instance. 7 | #[allow(dead_code)] 8 | pub fn with_temp_repo(test: T) -> () 9 | where 10 | T: FnOnce(Repo) -> (), 11 | { 12 | let tempdir = tempfile::tempdir().unwrap(); 13 | let repo_path = tempdir.path(); 14 | git2::Repository::init(repo_path).unwrap(); 15 | let repo = Repo::new(repo_path.to_str().unwrap().to_string()); 16 | test(repo); 17 | } 18 | 19 | /// Create a temporary directory with three empty git repos within, a, b, and c, 20 | /// then run a closure that takes a Config initialized for that temporary 21 | /// directory. 22 | #[allow(dead_code)] 23 | pub fn with_base_dir_of_three_repos(test: T) -> () 24 | where 25 | T: FnOnce(Config) -> (), 26 | { 27 | let tempdir = tempfile::tempdir().unwrap(); 28 | let base_path = tempdir.path(); 29 | for repo_name in ["a", "b", "c"].iter() { 30 | let mut repo_path = PathBuf::from(base_path); 31 | repo_path.push(repo_name); 32 | git2::Repository::init(repo_path).unwrap(); 33 | } 34 | let config = Config { 35 | basedir: base_path.to_path_buf(), 36 | follow_symlinks: true, 37 | same_filesystem: true, 38 | ignored_patterns: vec![], 39 | default_cmd: String::from("status"), 40 | verbose: false, 41 | show_untracked: true, 42 | cache_file: Some(base_path.join("test-cache-file.txt").to_path_buf()), 43 | manpage_file: None, 44 | }; 45 | test(config); 46 | } 47 | --------------------------------------------------------------------------------