├── .cargo └── config.toml ├── .envrc ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── demo.gif ├── docs ├── CHANGELOG_PRE_CALVER_POST_V4.md ├── CHANGELOG_PRE_V4.md ├── COMMUNITY_THANKS.md ├── DEMO.md ├── MANUAL_INSTALL.md ├── RELEASE.md └── VERSIONING_SCHEME.md ├── flake.lock ├── flake.nix ├── justfile ├── rust-toolchain.toml └── src ├── args.rs ├── collector.rs ├── collector └── target.rs ├── config.rs ├── display.rs ├── display └── color.rs ├── main.rs ├── repository_view.rs ├── repository_view └── submodule_view.rs └── status.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustdocflags = "--document-private-items" 3 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake . 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | paths-ignore: 7 | - "**/*.md" 8 | - "LICENSE" 9 | - "assets/*.gif" 10 | pull_request: 11 | branches: 12 | - "main" 13 | paths-ignore: 14 | - "**/*.md" 15 | - "LICENSE" 16 | - "assets/*.gif" 17 | concurrency: 18 | group: "${{ github.workflow }}-${{ github.ref }}" 19 | jobs: 20 | ci: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: dtolnay/rust-toolchain@stable 25 | with: 26 | toolchain: stable 27 | components: rustfmt, clippy 28 | - uses: taiki-e/install-action@just 29 | - run: just ci 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | release: 13 | name: "Release" 14 | runs-on: ${{ matrix.job.os }} 15 | strategy: 16 | matrix: 17 | job: 18 | # Linux 19 | - target: aarch64-unknown-linux-gnu 20 | os: ubuntu-latest 21 | cross: true 22 | asset_name: gfold-linux-gnu-aarch64 23 | - target: x86_64-unknown-linux-gnu 24 | os: ubuntu-latest 25 | cross: true 26 | asset_name: gfold-linux-gnu-x86-64 27 | - target: x86_64-unknown-linux-musl 28 | os: ubuntu-latest 29 | cross: true 30 | asset_name: gfold-linux-musl-x86-64 31 | - target: aarch64-unknown-linux-musl 32 | os: ubuntu-latest 33 | cross: true 34 | asset_name: gfold-linux-musl-aarch64 35 | - target: powerpc64le-unknown-linux-gnu 36 | os: ubuntu-latest 37 | cross: true 38 | asset_name: gfold-linux-gnu-powerpc64le 39 | # Windows 40 | - target: x86_64-pc-windows-msvc 41 | os: windows-latest 42 | asset_name: gfold-windows-x86-64.exe 43 | # macOS 44 | - target: aarch64-apple-darwin 45 | os: macos-latest 46 | asset_name: gfold-darwin-aarch64 47 | - target: x86_64-apple-darwin 48 | os: macos-13 49 | asset_name: gfold-darwin-x86-64 50 | 51 | env: 52 | CARGO: cargo 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | 57 | - name: Set up cross compiling 58 | if: matrix.job.cross 59 | uses: taiki-e/install-action@v2 60 | with: 61 | tool: cross 62 | 63 | - name: Configure cross compiling 64 | if: matrix.job.cross 65 | run: echo "CARGO=cross" >> $GITHUB_ENV 66 | 67 | - name: Install Rust toolchain 68 | uses: dtolnay/rust-toolchain@stable 69 | with: 70 | targets: ${{ matrix.job.target }} 71 | 72 | - name: Build 73 | run: $CARGO build --release --locked --target ${{ matrix.job.target }} 74 | 75 | - shell: bash 76 | run: | 77 | if [ $(echo ${{ github.ref }} | grep "rc") ]; then 78 | echo "PRERELEASE=true" >> $GITHUB_ENV 79 | echo "PRERELEASE=true" 80 | else 81 | echo "PRERELEASE=false" >> $GITHUB_ENV 82 | echo "PRERELEASE=false" 83 | fi 84 | echo $PRERELEASE 85 | 86 | mv target/${{ matrix.job.target }}/release/gfold${{ startsWith(matrix.job.os, 'windows-') && '.exe' || '' }} ${{ matrix.job.asset_name }} 87 | - uses: softprops/action-gh-release@v2 88 | with: 89 | files: ${{ matrix.job.asset_name }} 90 | prerelease: ${{ env.PRERELEASE }} 91 | body: "Please refer to **[CHANGELOG.md](https://github.com/nickgerace/gfold/blob/main/CHANGELOG.md)** for information on this release." 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | **/.DS_Store 3 | **/.direnv 4 | **/.idea/ 5 | **/.vs/ 6 | **/.vscode/ 7 | **/target/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - All notable, released changes to this project from a user's perspective will be documented in this file 4 | - All changes are from [@nickgerace](https://github.com/nickgerace) unless otherwise specified 5 | - The format was inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | - This project follows the [CalVer](https://calver.org) versioning scheme (more details in the [VERSIONING_SCHEME](docs/VERSIONING_SCHEME.md) docs) 7 | 8 | ## `2025.4.0` - Wed 02 Apr 2025 9 | 10 | - Add many new pre-built binaries, including macOS x86_64, Linux (GNU) aarch64, Linux (GNU) powerpc64le, and Linux (MUSL) aarch64 from [@ofek](https://github.com/ofek) 11 | - Update dependencies 12 | 13 | ## `2025.2.1` - Tue 27 Feb 2025 14 | 15 | - Add MUSL pre-built binaries 16 | - Fix release builds for all platforms 17 | - Yank previous release due to broken release builds 18 | 19 | ## `2025.2.0` (yanked) - Tue 27 Feb 2025 20 | 21 | - Add "paths" configuration option to allow for multiple paths for `gfold` to execute on from [@uncenter](https://github.com/uncenter) 22 | - Move logging verbosity from an environment variable to a flag 23 | - Deprecate "path" configuration option from [@uncenter](https://github.com/uncenter) 24 | - Polish help message, including its formatting 25 | - Remove unused `strum` dependency 26 | - Slightly reduce binary size by no longer relying on formal error types and unneeded abstractions from a multi-crate workspace (i.e. the repository now contains only one crate, yet again) 27 | - Support `~` and `$HOME` for "paths" configuration option from [@uncenter](https://github.com/uncenter) 28 | - Switch to "CalVer" for versioning scheme (no end user action required) 29 | - Update dependencies 30 | 31 | ## Before `2025.2.0` 32 | 33 | Please see [CHANGELOG_PRE_CALVER_POST_V4](./docs/CHANGELOG_PRE_CALVER_POST_V4.md). 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This repository follows and enforces the Rust programming language's [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). 4 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell", 67 | "windows-sys", 68 | ] 69 | 70 | [[package]] 71 | name = "anyhow" 72 | version = "1.0.97" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 75 | dependencies = [ 76 | "backtrace", 77 | ] 78 | 79 | [[package]] 80 | name = "backtrace" 81 | version = "0.3.74" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 84 | dependencies = [ 85 | "addr2line", 86 | "cfg-if", 87 | "libc", 88 | "miniz_oxide", 89 | "object", 90 | "rustc-demangle", 91 | "windows-targets", 92 | ] 93 | 94 | [[package]] 95 | name = "bitflags" 96 | version = "2.9.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 99 | 100 | [[package]] 101 | name = "cc" 102 | version = "1.2.17" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" 105 | dependencies = [ 106 | "jobserver", 107 | "libc", 108 | "shlex", 109 | ] 110 | 111 | [[package]] 112 | name = "cfg-if" 113 | version = "1.0.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 116 | 117 | [[package]] 118 | name = "clap" 119 | version = "4.5.35" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 122 | dependencies = [ 123 | "clap_builder", 124 | "clap_derive", 125 | ] 126 | 127 | [[package]] 128 | name = "clap-verbosity-flag" 129 | version = "3.0.2" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" 132 | dependencies = [ 133 | "clap", 134 | "log", 135 | ] 136 | 137 | [[package]] 138 | name = "clap_builder" 139 | version = "4.5.35" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 142 | dependencies = [ 143 | "anstream", 144 | "anstyle", 145 | "clap_lex", 146 | "strsim", 147 | ] 148 | 149 | [[package]] 150 | name = "clap_derive" 151 | version = "4.5.32" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 154 | dependencies = [ 155 | "heck", 156 | "proc-macro2", 157 | "quote", 158 | "syn", 159 | ] 160 | 161 | [[package]] 162 | name = "clap_lex" 163 | version = "0.7.4" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 166 | 167 | [[package]] 168 | name = "colorchoice" 169 | version = "1.0.3" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 172 | 173 | [[package]] 174 | name = "crossbeam-deque" 175 | version = "0.8.6" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 178 | dependencies = [ 179 | "crossbeam-epoch", 180 | "crossbeam-utils", 181 | ] 182 | 183 | [[package]] 184 | name = "crossbeam-epoch" 185 | version = "0.9.18" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 188 | dependencies = [ 189 | "crossbeam-utils", 190 | ] 191 | 192 | [[package]] 193 | name = "crossbeam-utils" 194 | version = "0.8.21" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 197 | 198 | [[package]] 199 | name = "diff" 200 | version = "0.1.13" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 203 | 204 | [[package]] 205 | name = "displaydoc" 206 | version = "0.2.5" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 209 | dependencies = [ 210 | "proc-macro2", 211 | "quote", 212 | "syn", 213 | ] 214 | 215 | [[package]] 216 | name = "either" 217 | version = "1.15.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 220 | 221 | [[package]] 222 | name = "env_filter" 223 | version = "0.1.3" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 226 | dependencies = [ 227 | "log", 228 | ] 229 | 230 | [[package]] 231 | name = "env_logger" 232 | version = "0.11.8" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 235 | dependencies = [ 236 | "env_filter", 237 | "jiff", 238 | "log", 239 | ] 240 | 241 | [[package]] 242 | name = "equivalent" 243 | version = "1.0.2" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 246 | 247 | [[package]] 248 | name = "errno" 249 | version = "0.3.10" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 252 | dependencies = [ 253 | "libc", 254 | "windows-sys", 255 | ] 256 | 257 | [[package]] 258 | name = "fastrand" 259 | version = "2.3.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 262 | 263 | [[package]] 264 | name = "form_urlencoded" 265 | version = "1.2.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 268 | dependencies = [ 269 | "percent-encoding", 270 | ] 271 | 272 | [[package]] 273 | name = "getrandom" 274 | version = "0.3.2" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 277 | dependencies = [ 278 | "cfg-if", 279 | "libc", 280 | "r-efi", 281 | "wasi", 282 | ] 283 | 284 | [[package]] 285 | name = "gfold" 286 | version = "2025.4.0" 287 | dependencies = [ 288 | "anyhow", 289 | "clap", 290 | "clap-verbosity-flag", 291 | "env_logger", 292 | "git2", 293 | "log", 294 | "pretty_assertions", 295 | "rayon", 296 | "remain", 297 | "serde", 298 | "serde_json", 299 | "tempfile", 300 | "termcolor", 301 | "toml", 302 | "user_dirs", 303 | ] 304 | 305 | [[package]] 306 | name = "gimli" 307 | version = "0.31.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 310 | 311 | [[package]] 312 | name = "git2" 313 | version = "0.20.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9" 316 | dependencies = [ 317 | "bitflags", 318 | "libc", 319 | "libgit2-sys", 320 | "log", 321 | "url", 322 | ] 323 | 324 | [[package]] 325 | name = "hashbrown" 326 | version = "0.15.2" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 329 | 330 | [[package]] 331 | name = "heck" 332 | version = "0.5.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 335 | 336 | [[package]] 337 | name = "home" 338 | version = "0.5.11" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 341 | dependencies = [ 342 | "windows-sys", 343 | ] 344 | 345 | [[package]] 346 | name = "icu_collections" 347 | version = "1.5.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 350 | dependencies = [ 351 | "displaydoc", 352 | "yoke", 353 | "zerofrom", 354 | "zerovec", 355 | ] 356 | 357 | [[package]] 358 | name = "icu_locid" 359 | version = "1.5.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 362 | dependencies = [ 363 | "displaydoc", 364 | "litemap", 365 | "tinystr", 366 | "writeable", 367 | "zerovec", 368 | ] 369 | 370 | [[package]] 371 | name = "icu_locid_transform" 372 | version = "1.5.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 375 | dependencies = [ 376 | "displaydoc", 377 | "icu_locid", 378 | "icu_locid_transform_data", 379 | "icu_provider", 380 | "tinystr", 381 | "zerovec", 382 | ] 383 | 384 | [[package]] 385 | name = "icu_locid_transform_data" 386 | version = "1.5.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 389 | 390 | [[package]] 391 | name = "icu_normalizer" 392 | version = "1.5.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 395 | dependencies = [ 396 | "displaydoc", 397 | "icu_collections", 398 | "icu_normalizer_data", 399 | "icu_properties", 400 | "icu_provider", 401 | "smallvec", 402 | "utf16_iter", 403 | "utf8_iter", 404 | "write16", 405 | "zerovec", 406 | ] 407 | 408 | [[package]] 409 | name = "icu_normalizer_data" 410 | version = "1.5.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 413 | 414 | [[package]] 415 | name = "icu_properties" 416 | version = "1.5.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 419 | dependencies = [ 420 | "displaydoc", 421 | "icu_collections", 422 | "icu_locid_transform", 423 | "icu_properties_data", 424 | "icu_provider", 425 | "tinystr", 426 | "zerovec", 427 | ] 428 | 429 | [[package]] 430 | name = "icu_properties_data" 431 | version = "1.5.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 434 | 435 | [[package]] 436 | name = "icu_provider" 437 | version = "1.5.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 440 | dependencies = [ 441 | "displaydoc", 442 | "icu_locid", 443 | "icu_provider_macros", 444 | "stable_deref_trait", 445 | "tinystr", 446 | "writeable", 447 | "yoke", 448 | "zerofrom", 449 | "zerovec", 450 | ] 451 | 452 | [[package]] 453 | name = "icu_provider_macros" 454 | version = "1.5.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 457 | dependencies = [ 458 | "proc-macro2", 459 | "quote", 460 | "syn", 461 | ] 462 | 463 | [[package]] 464 | name = "idna" 465 | version = "1.0.3" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 468 | dependencies = [ 469 | "idna_adapter", 470 | "smallvec", 471 | "utf8_iter", 472 | ] 473 | 474 | [[package]] 475 | name = "idna_adapter" 476 | version = "1.2.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 479 | dependencies = [ 480 | "icu_normalizer", 481 | "icu_properties", 482 | ] 483 | 484 | [[package]] 485 | name = "indexmap" 486 | version = "2.8.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 489 | dependencies = [ 490 | "equivalent", 491 | "hashbrown", 492 | ] 493 | 494 | [[package]] 495 | name = "is_terminal_polyfill" 496 | version = "1.70.1" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 499 | 500 | [[package]] 501 | name = "itoa" 502 | version = "1.0.15" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 505 | 506 | [[package]] 507 | name = "jiff" 508 | version = "0.2.5" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" 511 | dependencies = [ 512 | "jiff-static", 513 | "log", 514 | "portable-atomic", 515 | "portable-atomic-util", 516 | "serde", 517 | ] 518 | 519 | [[package]] 520 | name = "jiff-static" 521 | version = "0.2.5" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" 524 | dependencies = [ 525 | "proc-macro2", 526 | "quote", 527 | "syn", 528 | ] 529 | 530 | [[package]] 531 | name = "jobserver" 532 | version = "0.1.33" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 535 | dependencies = [ 536 | "getrandom", 537 | "libc", 538 | ] 539 | 540 | [[package]] 541 | name = "libc" 542 | version = "0.2.171" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 545 | 546 | [[package]] 547 | name = "libgit2-sys" 548 | version = "0.18.1+1.9.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" 551 | dependencies = [ 552 | "cc", 553 | "libc", 554 | "libz-sys", 555 | "pkg-config", 556 | ] 557 | 558 | [[package]] 559 | name = "libz-sys" 560 | version = "1.1.22" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" 563 | dependencies = [ 564 | "cc", 565 | "libc", 566 | "pkg-config", 567 | "vcpkg", 568 | ] 569 | 570 | [[package]] 571 | name = "linux-raw-sys" 572 | version = "0.9.3" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 575 | 576 | [[package]] 577 | name = "litemap" 578 | version = "0.7.5" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 581 | 582 | [[package]] 583 | name = "log" 584 | version = "0.4.27" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 587 | 588 | [[package]] 589 | name = "memchr" 590 | version = "2.7.4" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 593 | 594 | [[package]] 595 | name = "miniz_oxide" 596 | version = "0.8.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 599 | dependencies = [ 600 | "adler2", 601 | ] 602 | 603 | [[package]] 604 | name = "object" 605 | version = "0.36.7" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 608 | dependencies = [ 609 | "memchr", 610 | ] 611 | 612 | [[package]] 613 | name = "once_cell" 614 | version = "1.21.3" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 617 | 618 | [[package]] 619 | name = "percent-encoding" 620 | version = "2.3.1" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 623 | 624 | [[package]] 625 | name = "pkg-config" 626 | version = "0.3.32" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 629 | 630 | [[package]] 631 | name = "portable-atomic" 632 | version = "1.11.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 635 | 636 | [[package]] 637 | name = "portable-atomic-util" 638 | version = "0.2.4" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 641 | dependencies = [ 642 | "portable-atomic", 643 | ] 644 | 645 | [[package]] 646 | name = "pretty_assertions" 647 | version = "1.4.1" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 650 | dependencies = [ 651 | "diff", 652 | "yansi", 653 | ] 654 | 655 | [[package]] 656 | name = "proc-macro2" 657 | version = "1.0.94" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 660 | dependencies = [ 661 | "unicode-ident", 662 | ] 663 | 664 | [[package]] 665 | name = "quote" 666 | version = "1.0.40" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 669 | dependencies = [ 670 | "proc-macro2", 671 | ] 672 | 673 | [[package]] 674 | name = "r-efi" 675 | version = "5.2.0" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 678 | 679 | [[package]] 680 | name = "rayon" 681 | version = "1.10.0" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 684 | dependencies = [ 685 | "either", 686 | "rayon-core", 687 | ] 688 | 689 | [[package]] 690 | name = "rayon-core" 691 | version = "1.12.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 694 | dependencies = [ 695 | "crossbeam-deque", 696 | "crossbeam-utils", 697 | ] 698 | 699 | [[package]] 700 | name = "remain" 701 | version = "0.2.15" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "d7ef12e84481ab4006cb942f8682bba28ece7270743e649442027c5db87df126" 704 | dependencies = [ 705 | "proc-macro2", 706 | "quote", 707 | "syn", 708 | ] 709 | 710 | [[package]] 711 | name = "rustc-demangle" 712 | version = "0.1.24" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 715 | 716 | [[package]] 717 | name = "rustix" 718 | version = "1.0.5" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 721 | dependencies = [ 722 | "bitflags", 723 | "errno", 724 | "libc", 725 | "linux-raw-sys", 726 | "windows-sys", 727 | ] 728 | 729 | [[package]] 730 | name = "ryu" 731 | version = "1.0.20" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 734 | 735 | [[package]] 736 | name = "serde" 737 | version = "1.0.219" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 740 | dependencies = [ 741 | "serde_derive", 742 | ] 743 | 744 | [[package]] 745 | name = "serde_derive" 746 | version = "1.0.219" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 749 | dependencies = [ 750 | "proc-macro2", 751 | "quote", 752 | "syn", 753 | ] 754 | 755 | [[package]] 756 | name = "serde_json" 757 | version = "1.0.140" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 760 | dependencies = [ 761 | "itoa", 762 | "memchr", 763 | "ryu", 764 | "serde", 765 | ] 766 | 767 | [[package]] 768 | name = "serde_spanned" 769 | version = "0.6.8" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 772 | dependencies = [ 773 | "serde", 774 | ] 775 | 776 | [[package]] 777 | name = "shlex" 778 | version = "1.3.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 781 | 782 | [[package]] 783 | name = "smallvec" 784 | version = "1.14.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 787 | 788 | [[package]] 789 | name = "stable_deref_trait" 790 | version = "1.2.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 793 | 794 | [[package]] 795 | name = "strsim" 796 | version = "0.11.1" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 799 | 800 | [[package]] 801 | name = "syn" 802 | version = "2.0.100" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 805 | dependencies = [ 806 | "proc-macro2", 807 | "quote", 808 | "unicode-ident", 809 | ] 810 | 811 | [[package]] 812 | name = "synstructure" 813 | version = "0.13.1" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 816 | dependencies = [ 817 | "proc-macro2", 818 | "quote", 819 | "syn", 820 | ] 821 | 822 | [[package]] 823 | name = "tempfile" 824 | version = "3.19.1" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 827 | dependencies = [ 828 | "fastrand", 829 | "getrandom", 830 | "once_cell", 831 | "rustix", 832 | "windows-sys", 833 | ] 834 | 835 | [[package]] 836 | name = "termcolor" 837 | version = "1.4.1" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 840 | dependencies = [ 841 | "winapi-util", 842 | ] 843 | 844 | [[package]] 845 | name = "tinystr" 846 | version = "0.7.6" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 849 | dependencies = [ 850 | "displaydoc", 851 | "zerovec", 852 | ] 853 | 854 | [[package]] 855 | name = "toml" 856 | version = "0.8.20" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 859 | dependencies = [ 860 | "serde", 861 | "serde_spanned", 862 | "toml_datetime", 863 | "toml_edit", 864 | ] 865 | 866 | [[package]] 867 | name = "toml_datetime" 868 | version = "0.6.8" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 871 | dependencies = [ 872 | "serde", 873 | ] 874 | 875 | [[package]] 876 | name = "toml_edit" 877 | version = "0.22.24" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 880 | dependencies = [ 881 | "indexmap", 882 | "serde", 883 | "serde_spanned", 884 | "toml_datetime", 885 | "winnow", 886 | ] 887 | 888 | [[package]] 889 | name = "unicode-ident" 890 | version = "1.0.18" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 893 | 894 | [[package]] 895 | name = "url" 896 | version = "2.5.4" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 899 | dependencies = [ 900 | "form_urlencoded", 901 | "idna", 902 | "percent-encoding", 903 | ] 904 | 905 | [[package]] 906 | name = "user_dirs" 907 | version = "0.2.0" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "25e0d5bff51f431ac0e51881aa83a00d96561878caca262c04157857a4b02083" 910 | dependencies = [ 911 | "home", 912 | ] 913 | 914 | [[package]] 915 | name = "utf16_iter" 916 | version = "1.0.5" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 919 | 920 | [[package]] 921 | name = "utf8_iter" 922 | version = "1.0.4" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 925 | 926 | [[package]] 927 | name = "utf8parse" 928 | version = "0.2.2" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 931 | 932 | [[package]] 933 | name = "vcpkg" 934 | version = "0.2.15" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 937 | 938 | [[package]] 939 | name = "wasi" 940 | version = "0.14.2+wasi-0.2.4" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 943 | dependencies = [ 944 | "wit-bindgen-rt", 945 | ] 946 | 947 | [[package]] 948 | name = "winapi-util" 949 | version = "0.1.9" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 952 | dependencies = [ 953 | "windows-sys", 954 | ] 955 | 956 | [[package]] 957 | name = "windows-sys" 958 | version = "0.59.0" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 961 | dependencies = [ 962 | "windows-targets", 963 | ] 964 | 965 | [[package]] 966 | name = "windows-targets" 967 | version = "0.52.6" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 970 | dependencies = [ 971 | "windows_aarch64_gnullvm", 972 | "windows_aarch64_msvc", 973 | "windows_i686_gnu", 974 | "windows_i686_gnullvm", 975 | "windows_i686_msvc", 976 | "windows_x86_64_gnu", 977 | "windows_x86_64_gnullvm", 978 | "windows_x86_64_msvc", 979 | ] 980 | 981 | [[package]] 982 | name = "windows_aarch64_gnullvm" 983 | version = "0.52.6" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 986 | 987 | [[package]] 988 | name = "windows_aarch64_msvc" 989 | version = "0.52.6" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 992 | 993 | [[package]] 994 | name = "windows_i686_gnu" 995 | version = "0.52.6" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 998 | 999 | [[package]] 1000 | name = "windows_i686_gnullvm" 1001 | version = "0.52.6" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1004 | 1005 | [[package]] 1006 | name = "windows_i686_msvc" 1007 | version = "0.52.6" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1010 | 1011 | [[package]] 1012 | name = "windows_x86_64_gnu" 1013 | version = "0.52.6" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1016 | 1017 | [[package]] 1018 | name = "windows_x86_64_gnullvm" 1019 | version = "0.52.6" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1022 | 1023 | [[package]] 1024 | name = "windows_x86_64_msvc" 1025 | version = "0.52.6" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1028 | 1029 | [[package]] 1030 | name = "winnow" 1031 | version = "0.7.4" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 1034 | dependencies = [ 1035 | "memchr", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "wit-bindgen-rt" 1040 | version = "0.39.0" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1043 | dependencies = [ 1044 | "bitflags", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "write16" 1049 | version = "1.0.0" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1052 | 1053 | [[package]] 1054 | name = "writeable" 1055 | version = "0.5.5" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1058 | 1059 | [[package]] 1060 | name = "yansi" 1061 | version = "1.0.1" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1064 | 1065 | [[package]] 1066 | name = "yoke" 1067 | version = "0.7.5" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1070 | dependencies = [ 1071 | "serde", 1072 | "stable_deref_trait", 1073 | "yoke-derive", 1074 | "zerofrom", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "yoke-derive" 1079 | version = "0.7.5" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1082 | dependencies = [ 1083 | "proc-macro2", 1084 | "quote", 1085 | "syn", 1086 | "synstructure", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "zerofrom" 1091 | version = "0.1.6" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1094 | dependencies = [ 1095 | "zerofrom-derive", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "zerofrom-derive" 1100 | version = "0.1.6" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1103 | dependencies = [ 1104 | "proc-macro2", 1105 | "quote", 1106 | "syn", 1107 | "synstructure", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "zerovec" 1112 | version = "0.10.4" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1115 | dependencies = [ 1116 | "yoke", 1117 | "zerofrom", 1118 | "zerovec-derive", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "zerovec-derive" 1123 | version = "0.10.3" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1126 | dependencies = [ 1127 | "proc-macro2", 1128 | "quote", 1129 | "syn", 1130 | ] 1131 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gfold" 3 | version = "2025.4.0" 4 | 5 | authors = ["Nick Gerace "] 6 | categories = ["command-line-utilities", "command-line-interface"] 7 | description = "CLI tool to help keep track of your Git repositories." 8 | edition = "2024" 9 | homepage = "https://github.com/nickgerace/gfold" 10 | keywords = ["git", "cli"] 11 | license = "Apache-2.0" 12 | readme = "README.md" 13 | repository = "https://github.com/nickgerace/gfold" 14 | 15 | [profile.release] 16 | codegen-units = 1 17 | lto = true 18 | opt-level = 3 19 | panic = "abort" 20 | strip = true 21 | 22 | [dependencies] 23 | anyhow = { version = "1.0", features = ["backtrace"] } 24 | clap = { version = "4.5", features = ["derive"] } 25 | clap-verbosity-flag = "3.0" 26 | env_logger = { version = "0.11", features = [ "humantime" ], default-features = false } 27 | git2 = { version = "0.20", default-features = false } 28 | log = "0.4" 29 | rayon = "1.10" 30 | remain = "0.2" 31 | serde = { version = "1.0", features = ["derive"] } 32 | serde_json = "1.0" 33 | termcolor = "1.4" 34 | toml = "0.8" 35 | user_dirs = "0.2" 36 | 37 | [dev-dependencies] 38 | pretty_assertions = "1.4" 39 | tempfile = "3.19" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gfold 2 | 3 | [![latest release tag](https://img.shields.io/github/v/tag/nickgerace/gfold?sort=semver&logo=git&logoColor=white&label=version&style=for-the-badge&color=blue)](https://github.com/nickgerace/gfold/releases/latest) 4 | [![crates.io version](https://img.shields.io/crates/v/gfold?style=for-the-badge&logo=rust&color=orange)](https://crates.io/crates/gfold) 5 | [![build status](https://img.shields.io/github/actions/workflow/status/nickgerace/gfold/ci.yml?branch=main&style=for-the-badge&logo=github&logoColor=white)](https://github.com/nickgerace/gfold/actions) 6 | [![calver](https://img.shields.io/badge/calver-YYYY.MM.MICRO-cyan.svg?style=for-the-badge)](https://calver.org) 7 | [![built with nix](https://builtwithnix.org/badge.svg)](https://builtwithnix.org) 8 | 9 | `gfold` is a CLI tool that helps you keep track of multiple Git repositories. 10 | 11 | [![A GIF showcasing gfold in action](https://raw.githubusercontent.com/nickgerace/gfold/main/assets/demo.gif)](https://raw.githubusercontent.com/nickgerace/gfold/main/assets/demo.gif) 12 | 13 | If you'd prefer to use the classic display mode by default, and avoid setting the flag every time, you can set it in the config file (see **Usage** section). 14 | 15 | ## Announcement (February 2025) 16 | 17 | All releases now follow the [CalVer](https://calver.org/) versioning scheme, starting with `2025.2.1`. 18 | This change is both forwards and backwards compatible with the [Semantic Versioning](https://semver.org/spec/v2.0.0.html) versioning scheme, which was used from the first release through version `4.6.0`. 19 | 20 | *No end user action is required specifically for the versioning scheme change itself.* 21 | 22 | This announcement will be eventually removed from this [README](./README.md) and will eventually be moved into the [CHANGELOG](./CHANGELOG.md). 23 | 24 | ## Description 25 | 26 | This app displays relevant information for multiple Git repositories in one to many directories. 27 | It only reads from the filesystem and will never write to it. 28 | While this tool might seem limited in scope and purpose, that is by design. 29 | 30 | By default, `gfold` looks at every Git repository via traversal from the current working directory. 31 | If you would like to target another directory, you can pass its path (relative or absolute) as the first argument or change the default path in the config file. 32 | 33 | After traversal, `gfold` leverages [rayon](https://github.com/rayon-rs/rayon) to perform concurrent, read-only analysis of all Git repositories detected. 34 | Analysis is performed by leveraging the [git2-rs](https://github.com/rust-lang/git2-rs) library. 35 | 36 | ## Usage 37 | 38 | Provide the `-h/--help` flag to see all the options for using this application. 39 | 40 | ```shell 41 | # Operate in the current working directory or in the location provided by a config file, if one exists. 42 | gfold 43 | 44 | # Operate in the parent directory. 45 | gfold .. 46 | 47 | # Operate in the home directory (first method). 48 | gfold $HOME 49 | 50 | # Operate in the home directory (second method). 51 | gfold ~/ 52 | 53 | # Operate with an absolute path. 54 | gfold /this/is/an/absolute/path 55 | 56 | # Operate with a relative path. 57 | gfold ../../this/is/a/relative/path 58 | 59 | # Operate with three paths. 60 | gfold ~/src ~/projects ~/code 61 | ``` 62 | 63 | ### Config File 64 | 65 | If you find yourself providing the same arguments frequently, you can create and use a config file. 66 | `gfold` does not come with a config file by default and config files are entirely optional. 67 | 68 | How does it work? 69 | Upon execution, `gfold` will look for a config file at the following paths (in order): 70 | 71 | - `$XDG_CONFIG_HOME/gfold.toml` 72 | - `$XDG_CONFIG_HOME/gfold/config.toml` 73 | - `$HOME/.config/gfold.toml` 74 | 75 | `$XDG_CONFIG_HOME` refers to the literal `XDG_CONFIG_HOME` environment variable, but will default to the appropriate operating system-specific path if not set (see [`user_dirs`](https://github.com/uncenter/user_dirs) for more information). 76 | 77 | If a config file is found, `gfold` will read it and use the options specified within. 78 | 79 | For config file creation, you can use the `--dry-run` flag to print valid TOML. 80 | Here is an example config file creation workflow on macOS, Linux and similar platforms: 81 | 82 | ```shell 83 | gfold -d classic -c never ~/ --dry-run > $HOME/.config/gfold.toml 84 | ``` 85 | 86 | Here are the contents of the resulting config file: 87 | 88 | ```toml 89 | paths = ['/home/neloth'] 90 | display_mode = 'Classic' 91 | color_mode = 'Never' 92 | ``` 93 | 94 | Let's say you created a config file, but wanted to execute `gfold` with entirely different settings _and_ you want to ensure that 95 | you do not accidentally inherit options from the config file. 96 | In that scenario you can ignore your config file by using the `-i` flag. 97 | 98 | ```shell 99 | gfold -i 100 | ``` 101 | 102 | You can restore the config file to its defaults by using the same flag. 103 | 104 | ```shell 105 | gfold -i > $HOME/.config/gfold.toml 106 | ``` 107 | 108 | In addition, you can ignore the existing config file, configure specific options, and use defaults for unspecified options all at once. 109 | Here is an example where we want to use the classic display mode and override all other settings with their default values: 110 | 111 | ```shell 112 | gfold -i -d classic > $HOME/.config/gfold.toml 113 | ``` 114 | 115 | You can back up a config file and track its history with `git`. 116 | On macOS, Linux, and most systems, you can link the file back to a `git` repository. 117 | 118 | ```shell 119 | ln -s /gfold.toml $HOME/.config/gfold.toml 120 | ``` 121 | 122 | Now, you can update the config file within your repository and include the linking as part of your environment setup workflow. 123 | 124 | ## Installation 125 | 126 | [![Packaging status](https://repology.org/badge/vertical-allrepos/gfold.svg)](https://repology.org/project/gfold/versions) 127 | 128 | ### Homebrew (macOS and Linux) 129 | 130 | You can use [Homebrew](https://brew.sh) to install `gfold` using the [core formulae](https://formulae.brew.sh/formula/gfold). 131 | 132 | However, you may run into a naming collision on macOS if [coreutils](https://formulae.brew.sh/formula/coreutils) is installed via `brew`. 133 | See the [troubleshooting](#troubleshooting-and-known-issues) section for a workaround and more information. 134 | 135 | ```shell 136 | brew install gfold 137 | ``` 138 | 139 | ### Arch Linux 140 | 141 | You can use [pacman](https://wiki.archlinux.org/title/Pacman) to install `gfold` from the [extra repository](https://archlinux.org/packages/extra/x86_64/gfold/). 142 | 143 | ```shell 144 | pacman -S gfold 145 | ``` 146 | 147 | ### Nix and NixOS 148 | 149 | You can install `gfold` from [nixpkgs](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/gfold/default.nix): 150 | 151 | ```shell 152 | nix-env --install gfold 153 | ``` 154 | 155 | If you are using [flakes](https://nixos.wiki/wiki/Flakes), you can install using the `nix` command directly. 156 | 157 | ```shell 158 | nix profile install "nixpkgs#gfold" 159 | ``` 160 | 161 | ### Cargo 162 | 163 | You can use [cargo](https://crates.io) to install the [crate](https://crates.io/crates/gfold) on almost any platform. 164 | 165 | ```shell 166 | cargo install gfold 167 | ``` 168 | 169 | Use the `--locked` flag if you'd like Cargo to use `Cargo.lock`. 170 | 171 | ```shell 172 | cargo install --locked gfold 173 | ``` 174 | 175 | Keeping the crate up to date is easy with [cargo-update](https://crates.io/crates/cargo-update). 176 | 177 | ```shell 178 | cargo install cargo-update 179 | cargo install-update -a 180 | ``` 181 | 182 | ### Download a Binary 183 | 184 | If you do not want to use one of the above installation methods and do not want to clone the repository, you can download a binary from the [releases](https://github.com/nickgerace/gfold/releases) page. 185 | For an example on how to do that, refer to the [manual install](./docs/MANUAL_INSTALL.md) guide. 186 | 187 | ### Build From Source 188 | 189 | If you would like an example on how to build from source, refer to the [manual install](./docs/MANUAL_INSTALL.md) guide. 190 | 191 | ### Deprecated: Homebrew Tap (macOS only) 192 | 193 | The [tap located at nickgerace/homebrew-nickgerace](https://github.com/nickgerace/homebrew-nickgerace/blob/main/Formula/gfold.rb) has been deprecated. 194 | Please use the aforementioned core Homebrew package instead. 195 | 196 | ### Preferred Installation Method Not Listed? 197 | 198 | Please [file an issue](https://github.com/nickgerace/gfold/issues/new)! 199 | 200 | ## Compatibility 201 | 202 | `gfold` is intended to be ran on _any_ tier one Rust 🦀 target. 203 | Please [file an issue](https://github.com/nickgerace/gfold/issues) if your platform is unsupported. 204 | 205 | ## Troubleshooting and Known Issues 206 | 207 | If you encounter unexpected behavior or a bug and would like to see more details, please run with increased verbosity. 208 | 209 | ```shell 210 | gfold -vvv 211 | ``` 212 | 213 | If the issue persists, please [file an issue](https://github.com/nickgerace/gfold/issues). 214 | Please attach relevant logs from execution with _sensitive bits redacted_ in order to help resolve your issue. 215 | 216 | ### Coreutils Collision on macOS 217 | 218 | If `fold` from [GNU Coreutils](https://www.gnu.org/software/coreutils/) is installed on macOS via `brew`, it will be named `gfold`. 219 | You can avoid this collision with shell aliases, shell functions, and/or `PATH` changes. 220 | Here is an example with the `o` dropped from `gfold`: 221 | 222 | ```shell 223 | alias gfld=$HOME/.cargo/bin/gfold 224 | ``` 225 | 226 | ### Upstream `libgit2` Issue 227 | 228 | If you are seeing `unsupported extension name extensions.worktreeconfig` or similar errors, it may be related to 229 | [libgit2/libgit2#6044](https://github.com/libgit2/libgit2/issues/6044). 230 | 231 | This repository's tracking issue is [#205](https://github.com/nickgerace/gfold/issues/205). 232 | 233 | ## Community 234 | 235 | For more information and thanks to users and the "community" at large, please refer to the **[COMMUNITY THANKS](./docs/COMMUNITY_THANKS.md)** file. 236 | 237 | - [Packages for NixOS, Arch Linux and more](https://repology.org/project/gfold/versions) 238 | - ["One Hundred Rust Binaries"](https://www.wezm.net/v2/posts/2020/100-rust-binaries/page2/), an article that featured `gfold` 239 | - [nvim-gfold.lua](https://github.com/AckslD/nvim-gfold.lua), a `neovim` plugin for `gfold` _([announcement Reddit post](https://www.reddit.com/r/neovim/comments/t209wy/introducing_nvimgfoldlua/))_ 240 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickgerace/gfold/da0aca5509dea08dc9b2a8668967e5b995cb835f/assets/demo.gif -------------------------------------------------------------------------------- /docs/CHANGELOG_PRE_CALVER_POST_V4.md: -------------------------------------------------------------------------------- 1 | # Changelog From Version 4.0.0 to CalVer 2 | 3 | For new changes, please see the current [CHANGELOG](../CHANGELOG.md). 4 | 5 | - All notable, released changes to this project from a user's perspective will be documented in this file 6 | - All changes are from [@nickgerace](https://github.com/nickgerace) unless otherwise specified 7 | - The format was inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 8 | - This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 9 | 10 | ## After 4.6.0 11 | 12 | Please see [CHANGELOG](../CHANGELOG.md). 13 | 14 | ## 4.6.0 - 2024-12-10 15 | 16 | ### Added 17 | 18 | - Add XDG-first user directory lookup using [`user_dirs`](https://github.com/uncenter/user_dirs/blob/193547d1d2f190dbc6fbf9f29a4aa2d4318070db/README.md) from [@uncenter](https://github.com/uncenter) 19 | 20 | ### Changed 21 | 22 | - Bump dependencies 23 | - Help message to rely on line wrapping 24 | - Update release binary names for clarity (including fixing the macOS one for the correct architecture) 25 | 26 | ## 4.5.1 - 2024-12-09 27 | 28 | ### Changed 29 | 30 | - Bump dependencies 31 | 32 | ## 4.5.0 - 2024-05-23 33 | 34 | ### Added 35 | 36 | - Ability to use the standard display mode, but the results are purely alphabetical and not sorted by repository status 37 | 38 | ### Changed 39 | 40 | - Bump dependencies 41 | 42 | ## 4.4.1 - 2023-12-23 43 | 44 | ### Changed 45 | 46 | - Bump dependencies 47 | 48 | ## 4.4.0 - 2023-06-26 49 | 50 | ### Changed 51 | 52 | - Bump dependencies 53 | 54 | ### Notes 55 | 56 | - Bump the minor version field instead of the patch field because the dependency tree has significantly changed 57 | - Technically, the sole user-facing change is that the external dependencies have been bumped 58 | - For context, `libgfold` was newly introduced and contains the majority of the original `gfold` source code 59 | - Only run CI checks on merge 60 | - Publish and use `libgfold` for the first time 61 | - Split `gfold` into two crates: a library and a binary 62 | - Use cargo workspace dependencies 63 | 64 | ## 4.3.3 - 2023-04-07 65 | 66 | ### Changed 67 | 68 | - Bump dependencies 69 | 70 | ### Notes 71 | 72 | - Remove `flake.nix` due to lack of use (might return in the future) 73 | - Fix missing `4.3.2` title in the `CHANGELOG` 74 | 75 | ## 4.3.2 - 2023-03-09 76 | 77 | ### Changed 78 | 79 | - Bump dependencies 80 | 81 | ### Notes 82 | 83 | - Add `flake.nix` for more local development options 84 | 85 | ## 4.3.1 - 2023-02-05 86 | 87 | ### Changed 88 | 89 | - Bump dependencies 90 | - Bump LICENSE year 91 | 92 | ## 4.3.0 - 2023-02-05 93 | 94 | ### Added 95 | 96 | - Add submodule information to the `json` display mode (i.e. `gfold -d json`) 97 | - This information is not yet accessible in other display modes 98 | 99 | ### Changed 100 | 101 | - Bump dependencies 102 | 103 | ### Notes 104 | 105 | - Add demo GIF to README 106 | - Performed significant refactor to reduce the usage of "floating" functions 107 | (i.e. ensure functions are members of unit structs at minimum) as well as 108 | remove reliance on a single generic error enum 109 | 110 | ## 4.2.0 - 2022-12-21 111 | 112 | ### Changed 113 | 114 | - Add "unknown" status for repositories hitting the `extensions.worktreeconfig` error 115 | - Bump dependencies 116 | - Change "unpushed" color to blue 117 | - Ignore the `extensions.worktreeconfig` error until the corresponding upstream issue is resolved: https://github.com/libgit2/libgit2/issues/6044 118 | 119 | ## 4.1.2 - 2022-12-20 120 | 121 | ### Changed 122 | 123 | - Bump dependencies 124 | - When checking if "unpushed" and attempting to resolve the reference from a short name, ignore the error and assume we need to push 125 | 126 | ## 4.1.1 - 2022-12-19 127 | 128 | ### Changed 129 | 130 | - Bump dependencies 131 | - Ensure dependencies have their minor version fields locked 132 | 133 | ### Notes 134 | 135 | - This `CHANGELOG` entry was accidentally not included in the `4.1.1` tag 136 | 137 | ## 4.1.0 - 2022-10-20 138 | 139 | - Add debug symbol stripping for `cargo install` users (result: ~79% of the size of `4.0.1`) 140 | 141 | ### Changed 142 | 143 | - Bump dependencies 144 | - Change CLI library from `argh` to `clap v4` 145 | - Ensure integration test artifacts exist in the correct location 146 | - Refactor to use `cargo` workspaces, which should unlock the ability to create "scripts" via sub-crates 147 | 148 | ### Removed 149 | 150 | - Remove ability to print the version as a single JSON field (combine `-V/--version` with `-d/--display json`) 151 | - Normally, this would necessitate a bump of the "major" field in the version, but `-V/--version` is serializable to JSON (just a string) 152 | 153 | ## 4.0.1 - 2022-07-05 154 | 155 | ### Changed 156 | 157 | - Bump dependencies 158 | 159 | ## 4.0.0 - 2022-05-10 160 | 161 | ### Added 162 | 163 | - Add [Bors](https://bors.tech/) to avoid merge skew/semantic merge conflicts 164 | - Add color mode option with the following choices: "always", "compatibility" and "never" 165 | - "always": display with rich colors (default) 166 | - "compatibility": display with portable colors 167 | - "never": display with no color 168 | - Add display flag with the following choices: "standard" (or "default"), "json" and "classic" 169 | - "standard" (or "default") and "classic" output options return from the previous release 170 | - "json" output is a new option that displays all results in valid JSON, which is useful for third party applications, plugins, parsers, etc. 171 | - Add documentation comments almost everywhere for `cargo doc` 172 | - Add [git2-rs](https://github.com/rust-lang/git2-rs), which replaces `git` subcommand usage 173 | - Even though `git` subcommands were used over **git2-rs** to reduce binary size, significant speed increases could only be achieved by using the latter 174 | - More consistent behavior since git2-rs can be tested at a locked version 175 | - Add JSON output flag for both version and results printing 176 | - Add roubleshooting section to CLI help 177 | - Add troubleshooting section to README for using `RUST_LOG` and `RUST_BACKTRACE` 178 | 179 | ### Changed 180 | 181 | - Change config file location from `/gfold/gfold.json` to `/gfold.toml` 182 | - Change config file type from JSON to TOML 183 | - Change CLI help sections to be divided by headers 184 | - Drastically improve performance by moving from sequential target generation to nested, parallel iterators for target generation 185 | - Modify grey color default to avoid a bug where the `stdout` color is not refreshed within `tmux` when using macOS `Terminal.app` 186 | - Refactor module layout 187 | - `display` now contains its child, `color` 188 | - `report` now contains its child, `target` 189 | - Refactor testing for the entire crate 190 | - All tests have been replaced in favor on one integration test 191 | - The old tests relied on developer's environment, which is highly variable 192 | - The new test creates multiple files, directories, and repositories in the `target` directory to simulate an actual development environment 193 | - Use a harness for the `color` module instead of individual functions 194 | 195 | ### Removed 196 | 197 | - Remove debug flag in favor of using `RUST_LOG` 198 | - Remove display of `none` fields for the standard (default) display of result (i.e. now, if an optional field was not found, it is not shown) 199 | - Remove git path option for CLI and config file 200 | - Remove `git` subcommand usage 201 | 202 | ### Notes 203 | 204 | - Substantial performance gains should be noticeable in certain scenarios 205 | - Observed range in _loose_ benchmarking "real world" usage: ~1.2x to ~5.1x faster than `gfold 3.0.0` on macOS 12.3.1 206 | - Binary size has increased, but speed has taken priority for this release 207 | - Using `RUST_LOG` and `RUST_BACKTRACE` should be more helpful when debugging unexpected output, performance or suspected bugs 208 | 209 | ## Before 4.0.0 210 | 211 | Please see [CHANGELOG_PRE_V4](./CHANGELOG_PRE_V4.md). 212 | -------------------------------------------------------------------------------- /docs/CHANGELOG_PRE_V4.md: -------------------------------------------------------------------------------- 1 | # Changelog Before Version 4.0.0 2 | 3 | For new changes, please see the current [CHANGELOG](../CHANGELOG.md). 4 | 5 | - All notable changes to this project will be documented in this file 6 | - All changes are from [@nickgerace](https://github.com/nickgerace) unless otherwise specified 7 | - The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 8 | 9 | ## [After 3.0.0] 10 | 11 | Please see [CHANGELOG_PRE_CALVER_POST_V4](./CHANGELOG_PRE_CALVER_POST_V4.md). 12 | 13 | ## [3.0.0] - 2022-01-06 14 | 15 | ### Added 16 | 17 | - Ability to ignore config file options 18 | - Ability to print merged config options 19 | - Ability to specify path to `git` 20 | - Ability to store default path target in config file (defaults to current working directory) 21 | - Ability to use config file in `$HOME/.config/gfold/gfold.json` and `{FOLDERID_Profile}\.config\gfold\gfold.json` 22 | - Ability to use old display mode with `--classic` flag and store preference in config file 23 | - Formal CLI parsing library, `argh` 24 | - Install and uninstall scripts 25 | - New display mode that avoids grouping repositories (API-breaking since this is the new default display mode) 26 | 27 | ### Changed 28 | 29 | - Codebase to a domain-driven architecture (major refactor) 30 | 31 | ### Removed 32 | 33 | - Mention of the [deprecated, old Homebrew tap](https://github.com/nickgerace/homebrew-gfold) in the README 34 | - Short `-h` flag due to CLI crate addition (`argh`) 35 | 36 | ### Notes 37 | 38 | - Evaluated using `tracing` and `tracing-subscriber` over `log` and `env_logger`, but due to their combined larger size, the logging crates remain the same as before. 39 | - The config file can be non-existent, empty, partially filled out or completely filled out. There's also an option to ignore the config file completely and only use CLI options. 40 | - This crate has used other CLI parsing libraries in the past, and recently did not use any, but with manual testing and [publicly available benchmarks](https://github.com/rust-cli/argparse-benchmarks-rs/blob/c37e78aabdaa4384a9c49be3735a686803d0e37a/README.md#results), `argh` is now in use. 41 | 42 | ## [2.0.2] - 2021-12-02 43 | 44 | ### Changed 45 | 46 | - Misc package bumps 47 | 48 | ## [2.0.1] - 2021-10-29 49 | 50 | ### Added 51 | 52 | - Logger that can be set via an environment variable (`RUST_LOG`) 53 | 54 | ### Changed 55 | 56 | - Permission denied errors to be logged rather than displayed to `stderr` 57 | 58 | ### Misc 59 | 60 | - Ensure `crates.io` and `git` tag are in sync (very slight and accidental derivation for `2.0.0`) 61 | 62 | ## [2.0.0] - 2021-10-29 63 | 64 | ### Added 65 | 66 | - Discrete Code of Conduct file 67 | - Unpushed commit checking by default (greedy static analysis, so this may need to be tuned for edge cases) 68 | - `git` CLI wrapper instead of Git library usage due to security, Git already being installed, inconsistencies between the library and the CLI, and more 69 | 70 | ### Changed 71 | 72 | - Codebase re-write geared towards data-efficiency and parallelism 73 | - Dramatic runtime speed and binary size improvements (consistently able to reproduce, but heavily variable based on payload, OS, etc.) 74 | - Entire structure from library-driven to application-driven (no `lib.rs`) 75 | 76 | ### Removed 77 | 78 | - All CLI flags except for `-h/--help` and `-V/--version` 79 | - CLI crate since it is unneeded 80 | - Git library usage in favor of leveraging `git` subcommands due to security, Git already being installed, inconsistencies between the library and the CLI, and more 81 | - `DEVELOPING.md` and `EXTRA.md` since they were outdated/unimportant 82 | - `lib.rs` and the crate's library-based components 83 | 84 | ## [1.4.1] - 2021-08-02 85 | 86 | ### Changed 87 | 88 | - Misc package bumps 89 | 90 | ## [1.4.0] - 2021-06-17 91 | 92 | ### Changed 93 | 94 | - Continue upon PermissionDenied errors rather than exiting 95 | - Documentation to be moved to the new `docs` directory 96 | 97 | ## [1.3.0] - 2021-05-25 98 | 99 | ### Changed 100 | 101 | - Config type to be embedded within the Driver 102 | - Not in public library modules, but this should improve generation efficiency 103 | 104 | ### Removed 105 | 106 | - `-d/--debug` flag since all logging has been removed from the librariy, and `main.rs` does not log 107 | - `env_logger` crate 108 | - `log` crate 109 | - Logging from the entire library in favor of returning errors when needed and handling when possible/preferred 110 | 111 | ## [1.2.1] - 2021-05-23 112 | 113 | ### Changed 114 | 115 | - Bold table headers instead of repo names 116 | 117 | ### Removed 118 | 119 | - Extra newline before first entry when printing all tables 120 | 121 | ## [1.2.0] - 2021-05-16 122 | 123 | ### Removed 124 | 125 | - Middleware `run` function in `lib.rs` since it's unecessary and unintuitive 126 | - Before, you used a type from the `driver` module as a parameter for `run` 127 | - Now, you only use types from `driver` 128 | 129 | ## [1.1.1] - 2021-05-16 130 | 131 | ### Changed 132 | 133 | - Config management to be done by applications consuming the library (moved out of `lib.rs`) 134 | - Driver module to be public 135 | - Printing to STDOUT to be done by applications consuming the library (moved out of `lib.rs`) 136 | - `TableWrapper` type to be a private, internal type 137 | 138 | ## [1.1.0] - 2021-05-15 139 | 140 | ### Added 141 | 142 | - Shallow search flag to disable recursive search (accessible via `-s/--shallow`) 143 | 144 | ### Changed 145 | 146 | - Binary size to be ~60% of the size of `gfold 1.0.4` 147 | - Primarily, this was achieved by removing unused default features from imported crates 148 | - Runtime speed is the same, better, or more consistent 149 | - Default search behavior to be recursive rather than shallow 150 | - Short flag for `--skip-sort` from `-s` to `-x` 151 | - Workspace implementation to a single crate (similar to before `gfld`) 152 | 153 | ### Removed 154 | 155 | - `gfld`, the lightweight version of `gfold` due the following reasons: 156 | - its over ~105% average slower runtime speed (despite it being ~40% of the size) 157 | - printing to STDOUT was not consistent in subcommand scenarios 158 | - Recursive flag since `gfold` uses recursive search by default 159 | 160 | ## [1.0.4] - 2021-04-04 161 | 162 | ### Changed 163 | 164 | - Fixed final output order (sorted by name, then by status) 165 | 166 | ## [1.0.3] - 2021-04-02 167 | 168 | ### Changed 169 | 170 | - Directory name finder to default to the current working directory if empty (`gfld`) 171 | - Misc. optimizations in `gfld` 172 | - Release profile optimizations to be at workspace scope for binaries 173 | 174 | ## [1.0.2] - 2021-04-01 175 | 176 | ### Changed 177 | 178 | - `gfld` output to not include parent root since all results started with the same string 179 | 180 | ## [1.0.1] - 2021-03-30 181 | 182 | ### Added 183 | 184 | - `Cargo.lock` to the workspace to fix AUR builds 185 | 186 | ## Changed 187 | 188 | - CI to use `--locked` for builds 189 | 190 | ## [1.0.0] - 2021-03-29 191 | 192 | ### Added 193 | 194 | - A brand new, minimal CLI: `gfld` 195 | - `DEVELOPING.md` for instructions on building `gfld` 196 | 197 | ### Changed 198 | 199 | - Documentation to include `gfld` 200 | - GitHub issue template to include `gfld` information 201 | - GitHub PR CI to only build for Linux while keeping macOS and Windows for release CI 202 | - The repository to be split into two crates: `gfold` and `gfld` 203 | - Unnecessary `PathBuf` usages to `Path` when possible in `util.rs` 204 | 205 | ### Removed 206 | 207 | - Release workflow for GitHub actions (now, it is "merge only") 208 | - Uploaded binaries due to lack of checksums and maintenance 209 | 210 | ## [0.9.1] - 2021-03-16 211 | 212 | ### Added 213 | 214 | - RELEASE file for releasing `gfold` 215 | 216 | ### Changed 217 | 218 | - README installation section to be condensed 219 | - LICENSE to not use copyright dates or name (reduce maintenance) 220 | 221 | ### Removed 222 | 223 | - Makefile in order to be cross-platform-friendly 224 | 225 | ## [0.9.0] - 2021-02-15 226 | 227 | ### Added 228 | 229 | - Email display feature 230 | - Include standard directory feature 231 | - Shorthand flag for all features without one 232 | 233 | ### Changed 234 | 235 | - Directory walking to skip hidden directories 236 | - Repository opening check to log error in debug mode rather than panic 237 | 238 | ### Removed 239 | 240 | - File header comments 241 | - Prettytable macros 242 | 243 | ## [0.8.4] - 2021-01-26 244 | 245 | ### Added 246 | 247 | - Dependencies section to CHANGELOG 248 | - `paru` to suggested AUR helpers in README 249 | 250 | ### Changed 251 | 252 | - All CRLF files to LF 253 | - Condense tests into loops where possible 254 | - Label `unpush_check` as an experimental feature 255 | - `macos-amd64` to `darwin-amd64` 256 | - `unpush_check` from `disable` to `enable` 257 | 258 | ## [0.8.3] - 2020-12-15 259 | 260 | ### Added 261 | 262 | - Disable unpushed commit check flag and functionality 263 | - Logging for origin and local reference names for unpushed commit check 264 | 265 | ## [0.8.2] - 2020-12-14 266 | 267 | ### Added 268 | 269 | - `gfold --version` to issue template 270 | - Unpush functionality (again) 271 | 272 | ### Changed 273 | 274 | - Unpush function to only return boolean 275 | 276 | ### Removed 277 | 278 | - Contributing section from README to reduce requirements 279 | - Empty results message since it was potentially misleading 280 | 281 | ## [0.8.1] - 2020-12-01 282 | 283 | ### Added 284 | 285 | - Condition enum for adding rows to final table 286 | - Debug flag 287 | - Many debug statements for the new debug flag 288 | 289 | ### Changed 290 | 291 | - Bare repository checking to original behavior 292 | - `util.rs` results generation to include Condition enum 293 | 294 | ### Removed 295 | 296 | - Carets from `Cargo.toml` to maintain stability 297 | - Unpush functionality temporarily 298 | 299 | ## [0.8.0] - 2020-11-26 300 | 301 | ### Added 302 | 303 | - Debugging calls for general usage and the new unpushed commit code 304 | - Derive debug to the `Config` struct 305 | - Lightweight logging stack with `env_logger` and `log` 306 | - Two files: `driver.rs` and `util.rs` 307 | - Unpushed commit status functionality and output 308 | 309 | ### Changed 310 | 311 | - Bare repository detection to use upstream function 312 | - Library contents into `driver.rs` and `util.rs` through a major refactor 313 | 314 | ## [0.7.1] - 2020-11-18 315 | 316 | ### Added 317 | 318 | - In-depth description of the `run` function 319 | 320 | ### Changed 321 | 322 | - Consolidated boolean test permutations into one test 323 | 324 | ### Removed 325 | 326 | - All non-public comments in `*.rs` files 327 | 328 | ## [0.7.0] - 2020-11-11 329 | 330 | ### Added 331 | 332 | - Crate to crates.io 333 | - Crates.io publishing requirements to `[package]` in `Cargo.toml` 334 | - Homebrew tap 335 | - Library description to `lib.rs` 336 | 337 | ### Changed 338 | 339 | - Dependency versioning to use carets 340 | - README mentions of specific version tags 341 | - README plaintext blocks to single quotes when mixed with formatted text 342 | - README to sort installation method by package managers first 343 | 344 | ### Removed 345 | 346 | - Public structs and functions without only `run` (primary backend driver) remaining 347 | 348 | ## [0.6.2] - 2020-11-03 349 | 350 | ### Added 351 | 352 | - No color flag and functionality 353 | 354 | ### Removed 355 | 356 | - Pull request template 357 | 358 | ## [0.6.1] - 2020-10-12 359 | 360 | ### Added 361 | 362 | - Code of Conduct link 363 | - GitHub issue template 364 | - GitHub pull request template 365 | 366 | ### Changed 367 | 368 | - LICENSE to be extended through 2021 369 | - Match blocks in `lib.rs` to be consolidated 370 | - Nearly all contents of `lib.rs` to return errors back to the calling function in `main.rs` 371 | 372 | ### Removed 373 | 374 | - Duplicate code related to the match block consolidation 375 | 376 | ## [0.6.0] - 2020-10-10 377 | 378 | ### Added 379 | 380 | - Doc comments and `cargo doc` to `release` target 381 | - `eyre` for simple backtrace reporting 382 | - `gfold-bin` to AUR portion of README 383 | - `lib.rs` as part of major refactor 384 | 385 | ### Changed 386 | 387 | - Pre-build Makefile targets to be consolidated 388 | - Refactor source code to be driven by a library, helmed by `lib.rs` 389 | 390 | ### Removed 391 | 392 | - `util.rs` and `gfold.rs` as part of major refactor 393 | 394 | ## [0.5.2] - 2020-10-08 395 | 396 | ### Added 397 | 398 | - GitHub release downloads to README 399 | - Binary publishing workflow to the dummy file 400 | 401 | ### Changed 402 | 403 | - Existing merge workflow to use debug building instead of release building 404 | - Makefile target containing the old default branch name 405 | 406 | ### Removed 407 | 408 | - Makefile target for statically-linked building 409 | 410 | ## [0.5.1] - 2020-10-07 411 | 412 | ### Added 413 | 414 | - Release dummy GitHub Action 415 | - Version README badge 416 | 417 | ### Changed 418 | 419 | - A reduction to CI build time and complexity by combining the test and check steps, 420 | - GitHub workflow "merge" file name to "merge.yml" 421 | - GitHub workflow name to "merge" 422 | - OS compatibility claims in README through a simplified list 423 | - README badges to use shields.io 424 | 425 | ### Removed 426 | 427 | - MUSL mentions in docs 428 | 429 | ## [0.5.0] - 2020-09-02 430 | 431 | ### Added 432 | 433 | - Recursive search feature and flag 434 | - Skip sort feature and flag 435 | - Unit tests for recursive search and skip sort 436 | - AUR PKGBUILD GitHub repository to README 437 | - Results and TableWrapper structs, and relevant functions, 438 | - Three methods for Results struct (printing/sorting/populating results) 439 | - Make targets for `run-recursive` and `install-local` 440 | 441 | ### Changed 442 | 443 | - Switch from `walk_dir` function to object-driven harness for execution 444 | - Move `walk_dir` function logic to `Results` method 445 | - Function `is_git_repo` to its own file 446 | - Any unnecessary match block to use "expect" instead 447 | - Cargo install to use a specific tag 448 | - Version upgrade workflow to Makefile 449 | 450 | ### Removed 451 | 452 | - Leftover "FIXME" comments for recursive search ideas 453 | 454 | ## [0.4.0] - 2020-08-31 455 | 456 | ### Added 457 | 458 | - Changelog 459 | - Tags automation 460 | 461 | ### Changed 462 | 463 | - Example output to use mythical repositories 464 | - Path flag to positional argument 465 | - Switched to structopt library for CLI parsing 466 | 467 | ### Removed 468 | 469 | - Tag v0.3.0 (duplicate of 0.3.0 with the "v" character) 470 | - All GitHub releases before 0.3.1 471 | - Releases information from README 472 | 473 | ## [0.3.1] - 2020-08-30 474 | 475 | ### Added 476 | 477 | - Add AUR installation documentation 478 | - Add AUR packages from [@orhun](https://github.com/orhun) 479 | 480 | ### Changed 481 | 482 | - Switch to Apache 2.0 license from MIT license 483 | - Reorganize build tags, and add test build target 484 | 485 | ## [0.3.0] - 2020-08-24 486 | 487 | ### Changed 488 | 489 | - Handling for bare repositories to print their status to STDOUT with the mentorship of [@yaahc](https://github.com/yaahc) 490 | 491 | ## [0.2.2] - 2020-08-24 492 | 493 | ### Changed 494 | 495 | - "Continue" to the next repository object if the current object is bare 496 | - Release availability in README 497 | 498 | ## [0.2.1] - 2020-06-08 499 | 500 | ### Added 501 | 502 | - Experimental statically-linked, MUSL support 503 | 504 | ## [0.2.0] - 2020-05-10 505 | 506 | ### Changed 507 | 508 | - Switched to prettytable-rs 509 | - Unit tests to work with prettytable-rs 510 | 511 | ## [0.1.1] - 2020-04-10 512 | 513 | ### Added 514 | 515 | - Example output, contributors, and usage in README 516 | - Building for Windows, macOS, and Linux amd64 in CI pipeline from [@jrcichra](https://github.com/jrcichra) 517 | 518 | ## [0.1.0] - 2020-04-08 519 | 520 | ### Added 521 | 522 | - Base contents 523 | -------------------------------------------------------------------------------- /docs/COMMUNITY_THANKS.md: -------------------------------------------------------------------------------- 1 | # Community Thanks 2 | 3 | This document contains "thank you" messages to those who aided or contributed to this project outside of source code changes and issues filed to this repository. 4 | 5 | - [@AcksID](https://github.com/AckslD/nvim-gfold.lua) for creating and maintaining [nvim-gfold.lua](https://github.com/AckslD/nvim-gfold.lua), a `neovim` plugin for `gfold` 6 | - [@jrcichra](https://github.com/jrcichra) for adding multi-OS support to the original, early-stage CI pipeline 7 | - [@ofek](https://github.com/ofek) for adding many more pre-built binaries for releases 8 | - [@orhun](https://github.com/orhun) for adding `gfold` to the Arch Linux community repository ([current location](https://archlinux.org/packages/extra/x86_64/gfold/)) and for maintaining [the original AUR packages](https://github.com/orhun/PKGBUILDs) 9 | - [@shanesveller](https://github.com/shanesveller) for adding `gfold` to nixpkgs ([current location](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/gfold/default.nix)) 10 | - [@uncenter](https://github.com/uncenter) for [several PRs](https://github.com/nickgerace/gfold/pulls?q=is%3Apr+is%3Aclosed+author%3Auncenter) revitalizing the project 11 | - [@wezm](https://github.com/wezm) for featuring `gfold` in the ["One Hundred Rust Binaries"](https://www.wezm.net/v2/posts/2020/100-rust-binaries/page2/) series 12 | - [@yaahc](https://github.com/yaahc) for mentoring during an early refactor 13 | -------------------------------------------------------------------------------- /docs/DEMO.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | This document contains information related to the [demo GIF](https://raw.githubusercontent.com/nickgerace/gfold/main/assets/demo.gif). 4 | 5 | ## Attribution 6 | 7 | The demo was recorded with [asciinema](https://asciinema.org/) and was converted to a GIF using [`agg`](https://github.com/asciinema/agg). 8 | 9 | ## Converting Recordings Into GIFs 10 | 11 | Using `agg`, convert the recording into a GIF. 12 | 13 | ```shell 14 | agg --theme monokai --rows 32 --font-size 15 --idle-time-limit 2 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/MANUAL_INSTALL.md: -------------------------------------------------------------------------------- 1 | # Manual Install 2 | 3 | This document contains methods on how to install `gfold` "manually" (i.e. without a package manager or registry). 4 | 5 | ## Download and Install a Binary on macOS and Linux 6 | 7 | Executing the commands in this section requires the following: 8 | 9 | - macOS `aarch64` or Linux (GNU) `x86_64` system 10 | - `bash` shell (or compatible) 11 | - `jq`, `wget` and `curl` installed and in `PATH` 12 | 13 | First, let's ensure we have our prerequisite binaries installed. 14 | 15 | ```bash 16 | for BINARY in "jq" "wget" "curl"; do 17 | if ! [ "$(command -v ${BINARY})" ]; then 18 | echo "\"$BINARY\" must be installed and in PATH" 19 | return 20 | fi 21 | done 22 | ``` 23 | 24 | Now, we need to determine to latest tag to build our release URL. 25 | 26 | ```bash 27 | LATEST=$(curl -s https://api.github.com/repos/nickgerace/gfold/releases/latest | jq -r ".tag_name") 28 | ``` 29 | 30 | Choose our platform. 31 | 32 | ```bash 33 | # If we are using Linux (GNU) x86_64... 34 | INSTALL_PLATFORM=linux-gnu-x84-64 35 | 36 | # If we are using macOS aarch64 (i.e. Apple Silicon or arm64) 37 | INSTALL_PLATFORM=darwin-aarch64 38 | ``` 39 | 40 | With the latest tag and platform in hand, we can download and install `gfold` to `/usr/local/bin/`. 41 | 42 | ```bash 43 | # Remove gfold if it is already in /tmp. 44 | if [ -f /tmp/gfold ]; then 45 | rm /tmp/gfold 46 | fi 47 | 48 | # Perform the download. 49 | wget -O /tmp/gfold https://github.com/nickgerace/gfold/releases/download/$LATEST/gfold-$INSTALL_PLATFORM 50 | 51 | # Set executable permissions. 52 | chmod +x /tmp/gfold 53 | 54 | # Remove gfold if it is already in /usr/local/bin/. 55 | if [ -f /usr/local/bin/gfold ]; then 56 | rm /usr/local/bin/gfold 57 | fi 58 | 59 | # Move gfold into /usr/local/bin/. 60 | mv /tmp/gfold /usr/local/bin/gfold 61 | ``` 62 | 63 | ### Uninstalling and Cleaning Up 64 | 65 | If you would like to uninstall `gfold` and remove potential artifacts from the method above, execute the following: 66 | 67 | ```bash 68 | # Remove potential installed and/or downloaded artifacts. 69 | rm /tmp/gfold 70 | rm /usr/local/bin/gfold 71 | 72 | # (Optional) remove the configuration file. 73 | rm $HOME/.config/gfold.toml 74 | ``` 75 | 76 | ## Build From Source Locally On All Platforms 77 | 78 | If you want to install from source locally, and not from [crates.io](https://crates.io/crates/gfold), you can clone the repository and build `gfold`. 79 | This should work on all major platforms. 80 | 81 | ```bash 82 | git clone https://github.com/nickgerace/gfold.git; cd gfold; cargo install 83 | ``` 84 | 85 | The commands above were tested on macOS. 86 | Slight modification may be required for your platform, but the flow should be the same: clone, change directory and run `cargo install`. 87 | -------------------------------------------------------------------------------- /docs/RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | This document contains all information related to release. 4 | 5 | ## Versioning Scheme 6 | 7 | See the [CHANGELOG](../CHANGELOG.md) for more information. 8 | 9 | ## Checklist 10 | 11 | Steps should be executed in sequential order. 12 | 13 | - [ ] Checkout and rebase `main` to its latest commit, then checkout a new branch 14 | - [ ] Change the `version` field in [`Cargo.toml`](../Cargo.toml) to the new tag 15 | - [ ] Open a web browser tab to the following link: `https://github.com/nickgerace/gfold/compare/...main` 16 | - [ ] Add a new section the version in [`CHANGELOG.md`](../CHANGELOG.md) with the current date 17 | - [ ] Using the diff, commit messages and commit title, populate the new section with all user-relevant changes 18 | - [ ] Once the section is finalized, determine what field should be bumped (alongside the section title) using aforementioned Semantic Versioning best practices 19 | - [ ] Verify that everything looks/works as expected: 20 | 21 | ```shell 22 | just ci 23 | ``` 24 | 25 | - [ ] Create and _do not merge_ a commit with the following message: `Update to ` 26 | - [ ] Test and verify the publishing workflow: 27 | 28 | ```shell 29 | cargo publish --dry-run 30 | ``` 31 | 32 | - [ ] Merge the preparation commit into `main` 33 | - [ ] Checkout and rebase `main` to its latest commit, which should be the aforementioned commit 34 | - [ ] Tag and push the tag: 35 | 36 | ```shell 37 | git tag 38 | git push --tags origin main 39 | ``` 40 | 41 | - [ ] Publish the crate: 42 | 43 | ```shell 44 | cargo publish 45 | ``` 46 | 47 | - [ ] Verify that the [crate](https://crates.io/crates/gfold) on `crates.io` looks correct 48 | - [ ] Download and install the crate: 49 | 50 | ```shell 51 | cargo install --locked gfold 52 | ``` 53 | 54 | - [ ] Verify that the [GitHub release](https://github.com/nickgerace/gfold/releases) on the repository's releases page looks correct 55 | -------------------------------------------------------------------------------- /docs/VERSIONING_SCHEME.md: -------------------------------------------------------------------------------- 1 | # Versioning Scheme 2 | 3 | This project follows [CalVer](https://calver.org/) for its versioning scheme, starting with `2025.2.1`. 4 | It used to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) from the first release through version `4.6.0`. 5 | This versioning approach is both backwards and forwards compatible with Semantic Versioning. 6 | 7 | Here is the template for the scheme: 8 | 9 | ``` 10 | .. 11 | ``` 12 | 13 | - The first field, `YYYY`, refers to the year of release, specified via four digits. 14 | - The second field, `MM`, refers to the month of release, specified via one (January through September) or two digits (October through December). 15 | - The third field, `RELEASE-NUMBER`, refers to the release number for the given year and month, starting from `0` and incrementing by one for every release. 16 | 17 | Here is an example of a theorhetical first release in January 2025: 18 | 19 | ``` 20 | 2025.1.0 21 | ``` 22 | 23 | Here is an example of a theorhetical third release in December 2024: 24 | 25 | ``` 26 | 2024.12.2 27 | ``` 28 | 29 | In both examples, the exact day of release did not matter. 30 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1743538100, 24 | "narHash": "sha256-Bl/ynRPIb4CdtbEw3gfJYpKiHmRmrKltXc8zipqpO0o=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "b9d43b3fe5152d1dc5783a2ba865b2a03388b741", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1743561237, 52 | "narHash": "sha256-dd97LXek202OWmUXvKYFdYWj0jHrn3p+L5Ojh1SEOqs=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "1de27ae43712a971c1da100dcd84386356f03ec7", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "gfold development environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | rust-overlay = { 8 | url = "github:oxalica/rust-overlay"; 9 | inputs = { 10 | nixpkgs.follows = "nixpkgs"; 11 | }; 12 | }; 13 | }; 14 | 15 | outputs = { 16 | self, 17 | nixpkgs, 18 | flake-utils, 19 | rust-overlay, 20 | ... 21 | }: 22 | flake-utils.lib.eachDefaultSystem (system: let 23 | overlays = [ 24 | (import rust-overlay) 25 | ]; 26 | 27 | rust-version = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 28 | rust-bin-with-overrides = rust-version.override { 29 | extensions = ["rust-analyzer" "rust-src"]; 30 | }; 31 | 32 | pkgs = import nixpkgs {inherit overlays system;}; 33 | in 34 | with pkgs; rec { 35 | devShells.default = mkShell { 36 | packages = [ 37 | alejandra 38 | bash 39 | cargo-audit 40 | cargo-bloat 41 | cargo-outdated 42 | cargo-udeps 43 | coreutils 44 | hyperfine 45 | just 46 | rust-bin-with-overrides 47 | ]; 48 | }; 49 | 50 | formatter = alejandra; 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | _default: 2 | @just --list 3 | 4 | # Build and run at the debug level in the parent directory 5 | run: 6 | cargo run -- -vvv .. 7 | 8 | # Build and run with the help flag 9 | run-help: 10 | cargo run -- -h 11 | 12 | # Scan for potential bloat 13 | bloat: 14 | cargo bloat --release 15 | cargo bloat --release --crates 16 | 17 | # Build all targets 18 | build: 19 | cargo build --all-targets 20 | 21 | # Build release targets 22 | build-release: 23 | cargo build --release 24 | 25 | # Run the ci suite 26 | ci: 27 | cargo fmt --all -- --check 28 | cargo check --all-targets --all-features --workspace 29 | cargo clippy --all-targets --all-features --no-deps --workspace -- -D warnings 30 | cargo doc --all --no-deps 31 | cargo test --all-targets --workspace 32 | cargo build --locked --all-targets 33 | 34 | # Update the nix flake lockfile 35 | update-flake: 36 | nix flake update 37 | 38 | # Update deps, run formatter, and run baseline lints and checks 39 | prepare: 40 | cargo update 41 | cargo fmt 42 | cargo check --all-targets --all-features --workspace 43 | cargo fix --edition-idioms --allow-dirty --allow-staged 44 | cargo clippy --all-features --all-targets --workspace --no-deps --fix --allow-dirty --allow-staged 45 | 46 | # Scan for vulnerabilities 47 | audit: prepare 48 | cargo audit 49 | 50 | # Scan for unused dependencies (requires nightly Rust!) 51 | udeps: 52 | cargo udeps 53 | 54 | # Check which dependencies are outdated 55 | outdated: 56 | cargo outdated 57 | 58 | # Perform a loose benchmark 59 | bench directory=('../'): build-release 60 | hyperfine --warmup 1 'target/release/gfold {{directory}}' 'gfold {{directory}}' 61 | 62 | # Peform a release binary size comparison 63 | size: build-release 64 | #!/usr/bin/env bash 65 | checker=gdu 66 | if ! command -v $checker; then 67 | checker=du 68 | fi 69 | $checker -b target/release/gfold 70 | binary=$(which gfold) 71 | if [[ -n "$binary" ]]; then 72 | $checker -b "$(realpath "$binary")" 73 | fi 74 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 5 | 6 | use crate::config::{ColorMode, DisplayMode}; 7 | 8 | const HELP: &str = "\ 9 | More information: https://github.com/nickgerace/gfold 10 | 11 | Description: this application helps you keep track of multiple Git repositories via CLI. By default, it displays relevant information for all repos in the current working directory. 12 | 13 | Config file usage: while CLI options are prioritized, default options will fallback to the config file if it exists. Here are the config file lookup locations: 14 | 15 | $XDG_CONFIG_HOME/gfold.toml 16 | $XDG_CONFIG_HOME/gfold/config.toml 17 | $HOME/.config/gfold.toml (or {{FOLDERID_Profile}}\\.config\\gfold.toml on Windows)"; 18 | 19 | #[derive(Debug, Parser)] 20 | #[command(version, about = HELP, long_about = None)] 21 | pub struct Cli { 22 | /// Specify path(s) to target directories (defaults to current working directory) 23 | pub paths: Option>, 24 | /// Configure the color settings 25 | #[arg(short, long)] 26 | pub color_mode: Option, 27 | /// Configure how collected information is displayed 28 | #[arg(short, long)] 29 | pub display_mode: Option, 30 | /// Display finalized config options and exit (merged options from an optional config file and command line arguments) 31 | #[arg(long)] 32 | pub dry_run: bool, 33 | /// Ignore config file settings 34 | #[arg(short, long)] 35 | pub ignore_config_file: bool, 36 | #[command(flatten)] 37 | pub verbose: Verbosity, 38 | } 39 | -------------------------------------------------------------------------------- /src/collector.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the functionality for generating reports. 2 | 3 | use std::collections::BTreeMap; 4 | use std::path::Path; 5 | 6 | use anyhow::Result; 7 | use rayon::prelude::*; 8 | use target::TargetCollector; 9 | 10 | use crate::repository_view::RepositoryView; 11 | 12 | mod target; 13 | 14 | /// This type represents a [`BTreeMap`] using an optional [`String`] for keys, which represents the 15 | /// parent directory for a group of reports ([`Vec`]). The values corresponding to those keys 16 | /// are the actual groups of reports. 17 | /// 18 | /// We use a [`BTreeMap`] instead of a [`HashMap`](std::collections::HashMap) in order to have 19 | /// sorted keys. 20 | pub type RepositoryCollection = BTreeMap, Vec>; 21 | 22 | type UnprocessedRepositoryView = Result; 23 | 24 | /// A unit struct that provides [`Self::run()`], which is used to generated [`RepositoryCollection`]. 25 | #[derive(Debug)] 26 | pub struct RepositoryCollector; 27 | 28 | impl RepositoryCollector { 29 | /// Generate [`RepositoryCollection`] for a given path and its children. 30 | pub fn run( 31 | path: &Path, 32 | include_email: bool, 33 | include_submodules: bool, 34 | ) -> Result { 35 | let unprocessed = TargetCollector::run(path.to_path_buf())? 36 | .par_iter() 37 | .map(|path| RepositoryView::new(path, include_email, include_submodules)) 38 | .collect::>(); 39 | 40 | let mut processed = RepositoryCollection::new(); 41 | for maybe_view in unprocessed { 42 | let view = maybe_view?; 43 | if let Some(mut views) = processed.insert(view.parent.clone(), vec![view.clone()]) { 44 | views.push(view.clone()); 45 | processed.insert(view.parent, views); 46 | } 47 | } 48 | Ok(processed) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/collector/target.rs: -------------------------------------------------------------------------------- 1 | //! This module contains target generation logic required for generating 2 | //! [`RepositoryViews`](crate::repository_view::RepositoryView). 3 | 4 | use log::{debug, error, warn}; 5 | use rayon::prelude::*; 6 | use std::fs::DirEntry; 7 | use std::path::PathBuf; 8 | use std::{fs, io}; 9 | 10 | /// An unprocessed target that needs to be disassembled before consumption. 11 | type UnprocessedTarget = io::Result; 12 | 13 | /// A unit struct used to centralizing target collection method(s). 14 | pub(crate) struct TargetCollector; 15 | 16 | impl TargetCollector { 17 | /// Generate targets for a given [`PathBuf`] based on its children (recursively). We use 18 | /// recursion paired with [`rayon`] since we prioritize speed over memory use. 19 | pub(crate) fn run(path: PathBuf) -> io::Result> { 20 | let entries: Vec = match fs::read_dir(&path) { 21 | Ok(read_dir) => read_dir.filter_map(|r| r.ok()).collect(), 22 | Err(e) => { 23 | match e.kind() { 24 | io::ErrorKind::PermissionDenied => warn!("{}: {}", e, &path.display()), 25 | _ => error!("{}: {}", e, &path.display()), 26 | } 27 | return Ok(Vec::with_capacity(0)); 28 | } 29 | }; 30 | 31 | let unprocessed = entries 32 | .par_iter() 33 | .map(Self::determine_target) 34 | .collect::>(); 35 | 36 | let mut results = Vec::new(); 37 | for entry in unprocessed { 38 | let entry = entry?; 39 | if let MaybeTarget::Multiple(targets) = entry { 40 | results.extend(targets); 41 | } else if let MaybeTarget::Single(target) = entry { 42 | results.push(target); 43 | } 44 | } 45 | Ok(results) 46 | } 47 | 48 | /// Ensure the entry is a directory and is not hidden. Then, check if a ".git" sub directory 49 | /// exists, which will indicate if the entry is a repository. If the directory is not a Git 50 | /// repository, then we will recursively call [`Self::run()`]. 51 | fn determine_target(entry: &DirEntry) -> io::Result { 52 | match entry.file_type()?.is_dir() 53 | && !entry 54 | .file_name() 55 | .to_str() 56 | .map(|file_name| file_name.starts_with('.')) 57 | .unwrap_or(false) 58 | { 59 | true => { 60 | let path = entry.path(); 61 | let git_sub_directory = path.join(".git"); 62 | match git_sub_directory.exists() && git_sub_directory.is_dir() { 63 | true => { 64 | debug!("found target: {:?}", &path); 65 | Ok(MaybeTarget::Single(path)) 66 | } 67 | false => Ok(MaybeTarget::Multiple(Self::run(path)?)), 68 | } 69 | } 70 | false => Ok(MaybeTarget::None), 71 | } 72 | } 73 | } 74 | 75 | /// An enum that contains 0 to N targets based on the variant. 76 | #[remain::sorted] 77 | enum MaybeTarget { 78 | /// Contains multiple targets from recursive call(s) of [`TargetCollector::run()`]. 79 | Multiple(Vec), 80 | /// Does not contain a target. 81 | None, 82 | /// Contains a single target. 83 | Single(PathBuf), 84 | } 85 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the config specification and functionality for creating a config. 2 | 3 | use anyhow::Result; 4 | use clap::ValueEnum; 5 | use serde::{Deserialize, Serialize}; 6 | use std::path::{Path, PathBuf}; 7 | use std::{env, fs}; 8 | 9 | /// This struct is the actual config type consumed through the codebase. It is boostrapped via its 10 | /// public methods and uses [`EntryConfig`], a private struct, under the hood in order to 11 | /// deserialize empty, non-existent, partial, and complete config files. 12 | #[derive(Debug, Serialize)] 13 | pub struct Config { 14 | /// The paths that `gfold` will traverse and collect results from. 15 | pub paths: Vec, 16 | /// The display format for results printed to `stdout`. 17 | pub display_mode: DisplayMode, 18 | /// The color mode for results printed to `stdout`. 19 | pub color_mode: ColorMode, 20 | } 21 | 22 | impl Config { 23 | /// This method tries to deserialize the config file (empty, non-existent, partial or complete) 24 | /// and uses [`EntryConfig`] as an intermediary struct. This is the primary method used when 25 | /// creating a config. 26 | pub fn try_config() -> Result { 27 | // Within this method, we check if the config file is empty before deserializing it. Users 28 | // should be able to proceed with empty config files. If empty or not found, then we fall 29 | // back to the "EntryConfig" default before conversion. 30 | let config_dir = user_dirs::config_dir()?; 31 | let home_dir = user_dirs::home_dir()?; 32 | 33 | let paths = [ 34 | config_dir.join("gfold.toml"), 35 | config_dir.join("gfold").join("config.toml"), 36 | home_dir.join(".config").join("gfold.toml"), 37 | ]; 38 | 39 | let path = match paths.into_iter().find(|p| p.exists()) { 40 | Some(path) => path, 41 | None => return Self::try_config_default(), 42 | }; 43 | 44 | let contents = fs::read_to_string(path)?; 45 | let entry_config = if contents.is_empty() { 46 | EntryConfig::default() 47 | } else { 48 | toml::from_str(&contents)? 49 | }; 50 | Self::from_entry_config(&entry_config) 51 | } 52 | 53 | /// This method does not look for the config file and uses [`EntryConfig`]'s defaults instead. 54 | /// Use this method when the user wishes to skip config file lookup. 55 | pub fn try_config_default() -> Result { 56 | Self::from_entry_config(&EntryConfig::default()) 57 | } 58 | 59 | /// This method prints the full config (merged with config file, as needed) as valid, pretty TOML. 60 | pub fn print(self) -> Result<(), toml::ser::Error> { 61 | print!("{}", toml::to_string_pretty(&self)?); 62 | Ok(()) 63 | } 64 | 65 | fn from_entry_config(entry_config: &EntryConfig) -> Result { 66 | if entry_config.path.is_some() && entry_config.paths.is_some() { 67 | return Err(anyhow::anyhow!( 68 | "Cannot have both `path` and `paths` in config" 69 | )); 70 | } 71 | Ok(Config { 72 | paths: if let Some(paths) = &entry_config.paths { 73 | paths 74 | .iter() 75 | .map(|p| normalize_path(p)) 76 | .collect::, _>>()? 77 | } else if let Some(path) = &entry_config.path { 78 | eprintln!( 79 | "WARNING: the `path` configuration option is deprecated. Use `paths` instead." 80 | ); 81 | vec![normalize_path(path)?] 82 | } else { 83 | vec![env::current_dir()?.canonicalize()?] 84 | }, 85 | display_mode: match &entry_config.display_mode { 86 | Some(display_mode) => *display_mode, 87 | None => DisplayMode::Standard, 88 | }, 89 | color_mode: match &entry_config.color_mode { 90 | Some(color_mode) => *color_mode, 91 | None => ColorMode::Always, 92 | }, 93 | }) 94 | } 95 | } 96 | 97 | fn normalize_path(path: &Path) -> Result { 98 | Ok(match path 99 | .strip_prefix("~") 100 | .or_else(|_| path.strip_prefix("$HOME")) 101 | { 102 | Ok(stripped) => user_dirs::home_dir()?.join(stripped), 103 | Err(_) => path.to_path_buf(), 104 | } 105 | .canonicalize()?) 106 | } 107 | 108 | /// This struct is a reflection of [`Config`] with its fields wrapped with [`Option`], which 109 | /// ensures that we can deserialize from partial config file contents and populate empty fields 110 | /// with defaults. Moreover, enum fields cannot set defaults values currently, so we need to 111 | /// manually set defaults for the user. For those reasons, the public methods for [`Config`] use 112 | /// this struct privately. 113 | #[derive(Deserialize, Default)] 114 | struct EntryConfig { 115 | /// Formerly a reflection of the `path` field on [`Config`]. Use `paths` instead. 116 | /// This field is deprecated and will be removed in a future release. 117 | pub path: Option, 118 | /// Reflection of the `paths` field on [`Config`]. 119 | pub paths: Option>, 120 | /// Reflection of the `display_mode` field on [`Config`]. 121 | pub display_mode: Option, 122 | /// Reflection of the `color_mode` field on [`Config`]. 123 | pub color_mode: Option, 124 | } 125 | 126 | /// Dictates how the results gathered should be displayed to the user via `stdout`. Setting this 127 | /// enum is _mostly_ cosmetic, but it is possible that collected data may differ in order to 128 | /// reduce compute load. For example: if one display mode displays more information than another 129 | /// display mode, more data may need to be collected. Conversely, if another display mode requires 130 | /// less information to be displayed, then some commands and functions might get skipped. 131 | /// In summary, while this setting is primarily for cosmetics, it may also affect runtime 132 | /// performance based on what needs to be displayed. 133 | #[remain::sorted] 134 | #[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)] 135 | pub enum DisplayMode { 136 | /// Informs the caller to display results in the classic format. 137 | Classic, 138 | /// Informs the caller to display results in JSON format. 139 | Json, 140 | /// Informs the caller to display results in the standard (default) format. All results are 141 | /// sorted alphabetically and then sorted by status. 142 | Standard, 143 | /// Informs the caller to display results in the standard (default) format with a twist: all 144 | /// results are solely sorted alphabetically (i.e. no additional sort by status). 145 | StandardAlphabetical, 146 | } 147 | 148 | /// Set the color mode of results printed to `stdout`. 149 | #[remain::sorted] 150 | #[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)] 151 | pub enum ColorMode { 152 | /// Attempt to display colors as intended (default behavior). 153 | Always, 154 | /// Display colors using widely-compatible methods at the potential expense of colors being 155 | /// displayed as intended. 156 | Compatibility, 157 | /// Never display colors. 158 | Never, 159 | } 160 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the functionality for displaying reports to `stdout`. 2 | 3 | use std::io; 4 | use std::path::Path; 5 | 6 | use anyhow::{Result, anyhow}; 7 | use color::ColorHarness; 8 | use log::debug; 9 | use log::warn; 10 | 11 | use crate::collector::RepositoryCollection; 12 | use crate::config::{ColorMode, DisplayMode}; 13 | 14 | // TODO(nick): make this module private. 15 | pub mod color; 16 | 17 | const PAD: usize = 2; 18 | const NONE: &str = "none"; 19 | 20 | /// This struct is used for displaying the contents of a [`RepositoryCollection`] to `stdout`. 21 | #[derive(Debug)] 22 | pub struct DisplayHarness { 23 | display_mode: DisplayMode, 24 | color_mode: ColorMode, 25 | } 26 | 27 | impl DisplayHarness { 28 | pub fn new(display_mode: DisplayMode, color_mode: ColorMode) -> Self { 29 | Self { 30 | display_mode, 31 | color_mode, 32 | } 33 | } 34 | 35 | /// This function chooses the display execution function based on the [`DisplayMode`] provided. 36 | pub fn run(&self, reports: &RepositoryCollection) -> Result<()> { 37 | match self.display_mode { 38 | DisplayMode::Standard => Self::standard(reports, self.color_mode, false)?, 39 | DisplayMode::StandardAlphabetical => Self::standard(reports, self.color_mode, true)?, 40 | DisplayMode::Json => Self::json(reports)?, 41 | DisplayMode::Classic => Self::classic(reports, self.color_mode)?, 42 | } 43 | Ok(()) 44 | } 45 | 46 | /// Display [`RepositoryCollection`] to `stdout` in the standard (default) format. 47 | fn standard( 48 | reports: &RepositoryCollection, 49 | color_mode: ColorMode, 50 | alphabetical_sort_only: bool, 51 | ) -> Result<()> { 52 | debug!("detected standard display mode"); 53 | let mut all_reports = Vec::new(); 54 | for grouped_report in reports { 55 | all_reports.append(&mut grouped_report.1.clone()); 56 | } 57 | 58 | all_reports.sort_by(|a, b| a.name.cmp(&b.name)); 59 | if !alphabetical_sort_only { 60 | all_reports.sort_by(|a, b| a.status.as_str().cmp(b.status.as_str())); 61 | } 62 | 63 | let color_harness = ColorHarness::new(color_mode); 64 | 65 | for report in all_reports { 66 | color_harness.write_bold(&report.name, false)?; 67 | 68 | let Some(parent) = report.parent else { 69 | warn!("parent is empty for collector: {}", report.name); 70 | continue; 71 | }; 72 | let full_path = Path::new(&parent).join(&report.name); 73 | let full_path_formatted = format!( 74 | " ~ {}", 75 | full_path.to_str().ok_or(anyhow!( 76 | "could not convert path (Path) to &str: {full_path:?}" 77 | ))? 78 | ); 79 | color_harness.write_gray(&full_path_formatted, true)?; 80 | 81 | print!(" "); 82 | color_harness.write_status(report.status, PAD)?; 83 | println!(" ({})", report.branch); 84 | if let Some(url) = &report.url { 85 | println!(" {url}"); 86 | } 87 | if let Some(email) = &report.email { 88 | println!(" {email}"); 89 | } 90 | } 91 | Ok(()) 92 | } 93 | 94 | /// Display [`RepositoryCollection`] to `stdout` in JSON format. 95 | fn json(reports: &RepositoryCollection) -> serde_json::Result<()> { 96 | debug!("detected json display mode"); 97 | let mut all_reports = Vec::new(); 98 | for grouped_report in reports { 99 | all_reports.append(&mut grouped_report.1.clone()); 100 | } 101 | all_reports.sort_by(|a, b| a.name.cmp(&b.name)); 102 | all_reports.sort_by(|a, b| a.status.as_str().cmp(b.status.as_str())); 103 | println!("{}", serde_json::to_string_pretty(&all_reports)?); 104 | Ok(()) 105 | } 106 | 107 | /// Display [`RepositoryCollection`] to `stdout` in the classic format. 108 | fn classic(reports: &RepositoryCollection, color_mode: ColorMode) -> io::Result<()> { 109 | debug!("detected classic display mode"); 110 | let color_harness = ColorHarness::new(color_mode); 111 | 112 | let length = reports.keys().len(); 113 | let mut first = true; 114 | for (title, group) in reports { 115 | // FIXME(nick): make group title matching less cumbersome. 116 | if length > 1 { 117 | if first { 118 | first = false; 119 | } else { 120 | println!(); 121 | } 122 | color_harness.write_bold( 123 | match &title { 124 | Some(s) => s, 125 | None => NONE, 126 | }, 127 | true, 128 | )?; 129 | } 130 | 131 | let mut name_max = 0; 132 | let mut branch_max = 0; 133 | let mut status_max = 0; 134 | for report in group { 135 | if report.name.len() > name_max { 136 | name_max = report.name.len(); 137 | } 138 | let status_length = report.status.as_str().len(); 139 | if status_length > status_max { 140 | status_max = status_length; 141 | } 142 | if report.branch.len() > branch_max { 143 | branch_max = report.branch.len(); 144 | } 145 | } 146 | 147 | let mut reports = group.clone(); 148 | reports.sort_by(|a, b| a.name.cmp(&b.name)); 149 | reports.sort_by(|a, b| a.status.as_str().cmp(b.status.as_str())); 150 | 151 | for report in reports { 152 | print!("{: s, 159 | None => NONE, 160 | }, 161 | branch_width = branch_max + PAD 162 | ); 163 | } 164 | } 165 | Ok(()) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/display/color.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a harness for non-trivial displays of information to `stdout`. 2 | 3 | use std::io; 4 | use std::io::Write; 5 | use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 6 | 7 | use crate::{config::ColorMode, status::Status}; 8 | 9 | /// This harness provides methods to write to `stdout`. It maps the internal [`ColorMode`] type to 10 | /// our dependency's [`ColorChoice`] type due to discrepancies in behavior and naming. 11 | #[derive(Debug)] 12 | pub struct ColorHarness { 13 | color_choice: ColorChoice, 14 | } 15 | 16 | impl ColorHarness { 17 | /// Creates a new color harness. 18 | pub fn new(color_mode: ColorMode) -> Self { 19 | Self { 20 | color_choice: match &color_mode { 21 | ColorMode::Always => ColorChoice::Always, 22 | ColorMode::Compatibility => ColorChoice::Auto, 23 | ColorMode::Never => ColorChoice::Never, 24 | }, 25 | } 26 | } 27 | 28 | /// Writes the [`Status`] of the Git repository to `stdout`. 29 | pub fn write_status(&self, status: Status, status_width: usize) -> io::Result<()> { 30 | let mut stdout = StandardStream::stdout(self.color_choice); 31 | stdout.set_color(ColorSpec::new().set_fg(Some(match status { 32 | Status::Bare | Status::Unknown => Color::Red, 33 | Status::Clean => Color::Green, 34 | Status::Unpushed => Color::Blue, 35 | Status::Unclean => Color::Yellow, 36 | })))?; 37 | write!( 38 | &mut stdout, 39 | "{: io::Result<()> { 48 | self.write_color(input, newline, ColorSpec::new().set_bold(true)) 49 | } 50 | 51 | /// Writes the input [`&str`] to `stdout` in gray (or cyan if in compatibility mode). 52 | pub fn write_gray(&self, input: &str, newline: bool) -> io::Result<()> { 53 | // FIXME(nick): check why Color::Rg(128, 128, 128) breaks in tmux on macOS Terminal.app. 54 | self.write_color( 55 | input, 56 | newline, 57 | ColorSpec::new().set_fg(Some(match &self.color_choice { 58 | ColorChoice::Auto => Color::Cyan, 59 | _ => Color::Ansi256(242), 60 | })), 61 | ) 62 | } 63 | 64 | fn write_color( 65 | &self, 66 | input: &str, 67 | newline: bool, 68 | color_spec: &mut ColorSpec, 69 | ) -> io::Result<()> { 70 | let mut stdout = StandardStream::stdout(self.color_choice); 71 | stdout.set_color(color_spec)?; 72 | if newline { 73 | writeln!(&mut stdout, "{input}")?; 74 | } else { 75 | write!(&mut stdout, "{input}")?; 76 | } 77 | stdout.reset() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! [gfold](https://github.com/nickgerace/gfold) is a CLI tool that helps you keep track of 2 | //! multiple Git repositories. 3 | 4 | #![warn( 5 | bad_style, 6 | clippy::missing_panics_doc, 7 | clippy::panic, 8 | clippy::panic_in_result_fn, 9 | clippy::unwrap_in_result, 10 | clippy::unwrap_used, 11 | dead_code, 12 | improper_ctypes, 13 | missing_debug_implementations, 14 | // TODO(nick): fix missing docs. 15 | // missing_docs, 16 | no_mangle_generic_items, 17 | non_shorthand_field_patterns, 18 | overflowing_literals, 19 | path_statements, 20 | patterns_in_fns_without_body, 21 | unconditional_recursion, 22 | unreachable_pub, 23 | unused, 24 | unused_allocation, 25 | unused_comparisons, 26 | unused_parens, 27 | while_true 28 | )] 29 | 30 | use std::{env, path::PathBuf}; 31 | 32 | use anyhow::Result; 33 | use args::Cli; 34 | use clap::Parser; 35 | use collector::RepositoryCollector; 36 | use log::debug; 37 | 38 | use crate::config::{Config, DisplayMode}; 39 | use crate::display::DisplayHarness; 40 | 41 | // TODO(nick): investigate module visibility. 42 | pub mod args; 43 | pub mod collector; 44 | pub mod config; 45 | pub mod display; 46 | pub mod repository_view; 47 | pub mod status; 48 | 49 | /// Initializes the logger based on the debug flag and `RUST_LOG` environment variable, then 50 | /// parses CLI arguments and generates a [`Config`] by merging configurations as needed, 51 | /// and finally collects results and displays them. 52 | fn main() -> Result<()> { 53 | let cli = Cli::parse(); 54 | 55 | env_logger::Builder::new() 56 | .filter_level(cli.verbose.log_level_filter()) 57 | .init(); 58 | debug!("initialized logger"); 59 | 60 | let mut config = if cli.ignore_config_file { 61 | Config::try_config_default()? 62 | } else { 63 | Config::try_config()? 64 | }; 65 | debug!("loaded initial config"); 66 | 67 | if let Some(found_display_mode_raw) = &cli.display_mode { 68 | config.display_mode = *found_display_mode_raw; 69 | } 70 | if let Some(found_color_mode) = &cli.color_mode { 71 | config.color_mode = *found_color_mode; 72 | } 73 | if let Some(found_paths) = &cli.paths { 74 | let current_dir = env::current_dir()?; 75 | config.paths = found_paths 76 | .iter() 77 | .map(|p| current_dir.join(p).canonicalize()) 78 | .collect::, _>>()?; 79 | } 80 | debug!("finalized config options"); 81 | 82 | if cli.dry_run { 83 | config.print()?; 84 | } else { 85 | let (include_email, include_submodules) = match config.display_mode { 86 | DisplayMode::Classic => (false, false), 87 | DisplayMode::Json => (true, true), 88 | DisplayMode::Standard | DisplayMode::StandardAlphabetical => (true, false), 89 | }; 90 | for path in &config.paths { 91 | debug!("processing path: {:?}", path); 92 | 93 | let repository_collection = 94 | RepositoryCollector::run(path, include_email, include_submodules)?; 95 | let display_harness = DisplayHarness::new(config.display_mode, config.color_mode); 96 | display_harness.run(&repository_collection)?; 97 | } 98 | } 99 | Ok(()) 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | use collector::RepositoryCollection; 107 | use git2::ErrorCode; 108 | use git2::Oid; 109 | use git2::Repository; 110 | use git2::Signature; 111 | use pretty_assertions::assert_eq; 112 | use repository_view::RepositoryView; 113 | use status::Status; 114 | use std::fs::File; 115 | use std::path::{Path, PathBuf}; 116 | use std::{fs, io}; 117 | use tempfile::tempdir; 118 | 119 | /// This scenario test for `gfold` covers an end-to-end usage scenario. It uses the 120 | /// [`tempfile`](tempfile) crate to create some repositories with varying states and levels 121 | /// of nesting. 122 | #[allow(clippy::panic_in_result_fn)] 123 | #[test] 124 | fn scenario() -> anyhow::Result<()> { 125 | // Temporary directory structure: 126 | // └── root 127 | // ├── one (repo) 128 | // │ └── file 129 | // ├── two (repo) 130 | // ├── three (repo) 131 | // └── nested 132 | // ├── four (repo) 133 | // ├── five (repo) 134 | // │ └── file 135 | // ├── six (repo) 136 | // └── seven (repo) 137 | let root = tempdir()?; 138 | let repo_one = create_directory(&root, "one")?; 139 | let repo_two = create_directory(&root, "two")?; 140 | let repo_three = create_directory(&root, "three")?; 141 | 142 | let nested = create_directory(&root, "nested")?; 143 | let repo_four = create_directory(&nested, "four")?; 144 | let repo_five = create_directory(&nested, "five")?; 145 | let repo_six = create_directory(&nested, "six")?; 146 | let repo_seven = create_directory(&nested, "seven")?; 147 | 148 | // Repo One 149 | Repository::init(&repo_one)?; 150 | create_file(&repo_one)?; 151 | 152 | // Repo Two 153 | Repository::init(&repo_two)?; 154 | 155 | // Repo Three 156 | Repository::init(&repo_three)?; 157 | 158 | // Repo Four 159 | let repository = Repository::init(&repo_four)?; 160 | if let Err(e) = repository.remote("origin", "https://github.com/nickgerace/gfold") { 161 | if e.code() != ErrorCode::Exists { 162 | return Err(e.into()); 163 | } 164 | } 165 | 166 | // Repo Five 167 | Repository::init(&repo_five)?; 168 | create_file(&repo_five)?; 169 | 170 | // Repo Six 171 | let repository = Repository::init(&repo_six)?; 172 | if let Err(e) = repository.remote("fork", "https://github.com/nickgerace/gfold") { 173 | if e.code() != ErrorCode::Exists { 174 | return Err(e.into()); 175 | } 176 | } 177 | commit_head_and_create_branch(&repository, "feat")?; 178 | 179 | // Repo Seven 180 | let repository = Repository::init(&repo_seven)?; 181 | if let Err(e) = repository.remote("origin", "https://github.com/nickgerace/gfold") { 182 | if e.code() != ErrorCode::Exists { 183 | return Err(e.into()); 184 | } 185 | } 186 | commit_head_and_create_branch(&repository, "needtopush")?; 187 | repository.set_head("refs/heads/needtopush")?; 188 | 189 | // Generate the collection directly with a default config and ensure the resulting views 190 | // match what we expect. 191 | let mut expected_collection = RepositoryCollection::new(); 192 | let expected_views_key = root 193 | .path() 194 | .to_str() 195 | .expect("could not convert PathBuf to &str") 196 | .to_string(); 197 | let mut expected_views = vec![ 198 | RepositoryView::finalize( 199 | &repo_one, 200 | Some("HEAD".to_string()), 201 | Status::Unclean, 202 | None, 203 | None, 204 | Vec::with_capacity(0), 205 | )?, 206 | RepositoryView::finalize( 207 | &repo_two, 208 | Some("HEAD".to_string()), 209 | Status::Clean, 210 | None, 211 | None, 212 | Vec::with_capacity(0), 213 | )?, 214 | RepositoryView::finalize( 215 | &repo_three, 216 | Some("HEAD".to_string()), 217 | Status::Clean, 218 | None, 219 | None, 220 | Vec::with_capacity(0), 221 | )?, 222 | ]; 223 | expected_views.sort_by(|a, b| a.name.cmp(&b.name)); 224 | expected_collection.insert(Some(expected_views_key), expected_views); 225 | 226 | // Add nested views to the expected collection. 227 | let nested_expected_views_key = nested 228 | .to_str() 229 | .expect("could not convert PathBuf to &str") 230 | .to_string(); 231 | let mut nested_expected_views_raw = vec![ 232 | RepositoryView::finalize( 233 | &repo_four, 234 | Some("HEAD".to_string()), 235 | Status::Clean, 236 | Some("https://github.com/nickgerace/gfold".to_string()), 237 | None, 238 | Vec::with_capacity(0), 239 | )?, 240 | RepositoryView::finalize( 241 | &repo_five, 242 | Some("HEAD".to_string()), 243 | Status::Unclean, 244 | None, 245 | None, 246 | Vec::with_capacity(0), 247 | )?, 248 | RepositoryView::finalize( 249 | &repo_six, 250 | Some("master".to_string()), 251 | Status::Unpushed, 252 | Some("https://github.com/nickgerace/gfold".to_string()), 253 | None, 254 | Vec::with_capacity(0), 255 | )?, 256 | RepositoryView::finalize( 257 | &repo_seven, 258 | Some("needtopush".to_string()), 259 | Status::Unpushed, 260 | Some("https://github.com/nickgerace/gfold".to_string()), 261 | None, 262 | Vec::with_capacity(0), 263 | )?, 264 | ]; 265 | nested_expected_views_raw.sort_by(|a, b| a.name.cmp(&b.name)); 266 | expected_collection.insert(Some(nested_expected_views_key), nested_expected_views_raw); 267 | 268 | // Generate a collection. 269 | let found_collection = RepositoryCollector::run(root.path(), false, false)?; 270 | 271 | // Ensure the found collection matches our expected one. Sort the collection for the 272 | // assertion. 273 | let mut found_collection_sorted = RepositoryCollection::new(); 274 | for (key, mut value) in found_collection { 275 | value.sort_by(|a, b| a.name.cmp(&b.name)); 276 | found_collection_sorted.insert(key, value); 277 | } 278 | assert_eq!( 279 | expected_collection, // expected 280 | found_collection_sorted // actual 281 | ); 282 | Ok(()) 283 | } 284 | 285 | fn create_directory>(parent: P, name: &str) -> io::Result { 286 | let parent = parent.as_ref(); 287 | let new_directory = parent.join(name); 288 | 289 | if let Err(e) = fs::create_dir(&new_directory) { 290 | if e.kind() != io::ErrorKind::AlreadyExists { 291 | return Err(e); 292 | } 293 | } 294 | Ok(new_directory) 295 | } 296 | 297 | fn create_file>(parent: P) -> io::Result<()> { 298 | let parent = parent.as_ref(); 299 | File::create(parent.join("file"))?; 300 | Ok(()) 301 | } 302 | 303 | fn commit_head_and_create_branch(repository: &Repository, name: &str) -> anyhow::Result<()> { 304 | // We need to commit at least once before branching. 305 | let commit_oid = commit(repository, "HEAD")?; 306 | let commit = repository.find_commit(commit_oid)?; 307 | repository.branch(name, &commit, true)?; 308 | Ok(()) 309 | } 310 | 311 | // Source: https://github.com/rust-lang/git2-rs/pull/885 312 | fn commit(repository: &Repository, update_ref: &str) -> anyhow::Result { 313 | // We will commit the contents of the index. 314 | let mut index = repository.index()?; 315 | let tree_oid = index.write_tree()?; 316 | let tree = repository.find_tree(tree_oid)?; 317 | 318 | // If this is the first commit, there is no parent. If the object returned by 319 | // "revparse_single" cannot be converted into a commit, then it isn't a commit and we know 320 | // there is no parent _commit_. 321 | let maybe_parent = match repository.revparse_single("HEAD") { 322 | Ok(object) => match object.into_commit() { 323 | Ok(commit) => Some(commit), 324 | Err(_) => None, 325 | }, 326 | Err(e) if e.code() == ErrorCode::NotFound => None, 327 | Err(e) => return Err(e.into()), 328 | }; 329 | 330 | let mut parents = Vec::new(); 331 | if let Some(parent) = maybe_parent.as_ref() { 332 | parents.push(parent); 333 | }; 334 | 335 | let signature = Signature::now("Bob", "bob@bob")?; 336 | Ok(repository.commit( 337 | Some(update_ref), 338 | &signature, 339 | &signature, 340 | "hello", 341 | &tree, 342 | parents.as_ref(), 343 | )?) 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/repository_view.rs: -------------------------------------------------------------------------------- 1 | //! This module contains [`RepositoryView`], which provides the [`Status`] 2 | //! and general overview of the state of a given Git repository. 3 | 4 | use std::path::Path; 5 | 6 | use anyhow::Result; 7 | use anyhow::anyhow; 8 | use git2::Repository; 9 | use log::{debug, error, trace}; 10 | use serde::{Deserialize, Serialize}; 11 | use submodule_view::SubmoduleView; 12 | 13 | use crate::status::Status; 14 | 15 | mod submodule_view; 16 | 17 | /// A collection of results for a Git repository at a given path. 18 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 19 | pub struct RepositoryView { 20 | /// The directory name of the Git repository. 21 | pub name: String, 22 | /// The name of the current, open branch. 23 | pub branch: String, 24 | /// The [`Status`] of the working tree. 25 | pub status: Status, 26 | 27 | /// The parent directory of the `path` field. The value will be `None` if a parent is not found. 28 | pub parent: Option, 29 | /// The remote origin URL. The value will be `None` if the URL cannot be found. 30 | pub url: Option, 31 | 32 | /// The email used in either the local or global config for the repository. 33 | pub email: Option, 34 | /// Views of submodules found within the repository. 35 | pub submodules: Vec, 36 | } 37 | 38 | impl RepositoryView { 39 | /// Generates a collector for a given path. 40 | pub fn new( 41 | repo_path: &Path, 42 | include_email: bool, 43 | include_submodules: bool, 44 | ) -> Result { 45 | debug!( 46 | "attempting to generate collector for repository_view at path: {:?}", 47 | repo_path 48 | ); 49 | 50 | let repo = match Repository::open(repo_path) { 51 | Ok(repo) => repo, 52 | Err(e) if e.message() == "unsupported extension name extensions.worktreeconfig" => { 53 | error!( 54 | "skipping error ({e}) until upstream libgit2 issue is resolved: https://github.com/libgit2/libgit2/issues/6044" 55 | ); 56 | let unknown_report = RepositoryView::finalize( 57 | repo_path, 58 | None, 59 | Status::Unknown, 60 | None, 61 | None, 62 | Vec::with_capacity(0), 63 | )?; 64 | return Ok(unknown_report); 65 | } 66 | Err(e) => return Err(e.into()), 67 | }; 68 | let (status, head, remote) = Status::find(&repo)?; 69 | 70 | let submodules = if include_submodules { 71 | SubmoduleView::list(&repo)? 72 | } else { 73 | Vec::with_capacity(0) 74 | }; 75 | 76 | let branch = match &head { 77 | Some(head) => head 78 | .shorthand() 79 | .ok_or(anyhow!("full shorthand for Git reference is invalid UTF-8"))?, 80 | None => "HEAD", 81 | }; 82 | 83 | let url = match remote { 84 | Some(remote) => remote.url().map(|s| s.to_string()), 85 | None => None, 86 | }; 87 | 88 | let email = match include_email { 89 | true => Self::get_email(&repo), 90 | false => None, 91 | }; 92 | 93 | debug!( 94 | "finalized collector collection for repository_view at path: {:?}", 95 | repo_path 96 | ); 97 | RepositoryView::finalize( 98 | repo_path, 99 | Some(branch.to_string()), 100 | status, 101 | url, 102 | email, 103 | submodules, 104 | ) 105 | } 106 | 107 | /// Assemble a [`RepositoryView`] with metadata for a given repository. 108 | pub fn finalize( 109 | path: &Path, 110 | branch: Option, 111 | status: Status, 112 | url: Option, 113 | email: Option, 114 | submodules: Vec, 115 | ) -> Result { 116 | let name = match path.file_name() { 117 | Some(s) => match s.to_str() { 118 | Some(s) => s.to_string(), 119 | None => { 120 | return Err(anyhow!( 121 | "could not convert file name (&OsStr) to &str: {path:?}" 122 | )); 123 | } 124 | }, 125 | None => { 126 | return Err(anyhow!( 127 | "received None (Option<&OsStr>) for file name: {path:?}" 128 | )); 129 | } 130 | }; 131 | let parent = match path.parent() { 132 | Some(s) => match s.to_str() { 133 | Some(s) => Some(s.to_string()), 134 | None => return Err(anyhow!("could not convert path (Path) to &str: {s:?}")), 135 | }, 136 | None => None, 137 | }; 138 | let branch = match branch { 139 | Some(branch) => branch, 140 | None => "unknown".to_string(), 141 | }; 142 | 143 | Ok(Self { 144 | name, 145 | branch, 146 | status, 147 | parent, 148 | url, 149 | email, 150 | submodules, 151 | }) 152 | } 153 | 154 | /// Find the "user.email" value in the local or global Git config. The 155 | /// [`Repository::config()`] method will look for a local config first and fallback to 156 | /// global, as needed. Absorb and log any and all errors as the email field is non-critical to 157 | /// the final results. 158 | fn get_email(repository: &Repository) -> Option { 159 | let config = match repository.config() { 160 | Ok(v) => v, 161 | Err(e) => { 162 | trace!("ignored error: {}", e); 163 | return None; 164 | } 165 | }; 166 | let mut entries = match config.entries(None) { 167 | Ok(v) => v, 168 | Err(e) => { 169 | trace!("ignored error: {}", e); 170 | return None; 171 | } 172 | }; 173 | 174 | // Greedily find our "user.email" value. Return the first result found. 175 | while let Some(entry) = entries.next() { 176 | match entry { 177 | Ok(entry) => { 178 | if let Some(name) = entry.name() { 179 | if name == "user.email" { 180 | if let Some(value) = entry.value() { 181 | return Some(value.to_string()); 182 | } 183 | } 184 | } 185 | } 186 | Err(e) => debug!("ignored error: {}", e), 187 | } 188 | } 189 | None 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/repository_view/submodule_view.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the ability to gather information on submodules for a given [`Repository`]. 2 | 3 | use anyhow::{Result, anyhow}; 4 | use git2::Repository; 5 | use log::error; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::status::Status; 10 | 11 | /// The view of a submodule with a [`Repository`]. 12 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 13 | pub struct SubmoduleView { 14 | pub name: String, 15 | pub status: Status, 16 | } 17 | 18 | impl SubmoduleView { 19 | /// Generate a list of [`submodule view(s)`](Self) for a given [`Repository`]. 20 | pub fn list(repo: &Repository) -> Result> { 21 | let mut submodules = Vec::new(); 22 | for submodule in repo.submodules()? { 23 | match submodule.open() { 24 | Ok(subrepo) => { 25 | let (status, _, _) = Status::find(&subrepo)?; 26 | let name = submodule 27 | .name() 28 | .ok_or(anyhow!("submodule name is invalid UTF-8"))?; 29 | 30 | submodules.push(Self { 31 | name: name.to_string(), 32 | status, 33 | }); 34 | } 35 | Err(e) => error!("could not open submodule as repository: {e}"), 36 | } 37 | } 38 | Ok(submodules) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/status.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the [`crate::status::Status`] type. 2 | 3 | use anyhow::Result; 4 | use git2::{ErrorCode, Reference, Remote, Repository, StatusOptions}; 5 | use log::debug; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// A summarized interpretation of the status of a Git working tree. 9 | #[remain::sorted] 10 | #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] 11 | pub enum Status { 12 | /// Corresponds to a "bare" working tree. 13 | Bare, 14 | /// Corresponds to a "clean" working tree. 15 | Clean, 16 | /// Corresponds to an "unclean" working tree. 17 | Unclean, 18 | /// Provided if the state of the working tree could neither be found nor determined. 19 | Unknown, 20 | /// Indicates that there is at least one commit not pushed to the remote from the working tree. 21 | Unpushed, 22 | } 23 | 24 | impl Status { 25 | /// Converts the enum into a borrowed, static `str`. 26 | pub fn as_str(&self) -> &'static str { 27 | match self { 28 | Self::Bare => "bare", 29 | Self::Clean => "clean", 30 | Self::Unclean => "unclean", 31 | Self::Unknown => "unknown", 32 | Self::Unpushed => "unpushed", 33 | } 34 | } 35 | 36 | /// Find the [`Status`] for a given [`Repository`]. The 37 | /// [`head`](Option) and [`remote`](Option) are also returned. 38 | pub fn find(repo: &Repository) -> Result<(Status, Option>, Option>)> { 39 | let head = match repo.head() { 40 | Ok(head) => Some(head), 41 | Err(ref e) 42 | if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => 43 | { 44 | None 45 | } 46 | Err(e) => return Err(e.into()), 47 | }; 48 | 49 | // Greedily chooses a remote if "origin" is not found. 50 | let (remote, remote_name) = match repo.find_remote("origin") { 51 | Ok(origin) => (Some(origin), Some("origin".to_string())), 52 | Err(e) if e.code() == ErrorCode::NotFound => Self::choose_remote_greedily(repo)?, 53 | Err(e) => return Err(e.into()), 54 | }; 55 | 56 | // We'll include all untracked files and directories in the status options. 57 | let mut opts = StatusOptions::new(); 58 | opts.include_untracked(true).recurse_untracked_dirs(true); 59 | 60 | // If "head" is "None" and statuses are empty, then the repository_view must be clean because there 61 | // are no commits to push. 62 | let status = match repo.statuses(Some(&mut opts)) { 63 | Ok(v) if v.is_empty() => match &head { 64 | Some(head) => match remote_name { 65 | Some(remote_name) => match Self::is_unpushed(repo, head, &remote_name)? { 66 | true => Status::Unpushed, 67 | false => Status::Clean, 68 | }, 69 | None => Status::Clean, 70 | }, 71 | None => Status::Clean, 72 | }, 73 | Ok(_) => Status::Unclean, 74 | Err(e) if e.code() == ErrorCode::BareRepo => Status::Bare, 75 | Err(e) => return Err(e.into()), 76 | }; 77 | 78 | Ok((status, head, remote)) 79 | } 80 | 81 | // Checks if local commit(s) on the current branch have not yet been pushed to the remote. 82 | fn is_unpushed( 83 | repo: &Repository, 84 | head: &Reference<'_>, 85 | remote_name: &str, 86 | ) -> Result { 87 | let local_head = head.peel_to_commit()?; 88 | let remote = format!( 89 | "{}/{}", 90 | remote_name, 91 | match head.shorthand() { 92 | Some(v) => v, 93 | None => { 94 | debug!("assuming unpushed; could not determine shorthand for head"); 95 | return Ok(true); 96 | } 97 | } 98 | ); 99 | let remote_head = match repo.resolve_reference_from_short_name(&remote) { 100 | Ok(reference) => reference.peel_to_commit()?, 101 | Err(e) => { 102 | debug!( 103 | "assuming unpushed; could not resolve remote reference from short name (ignored error: {})", 104 | e 105 | ); 106 | return Ok(true); 107 | } 108 | }; 109 | Ok( 110 | matches!(repo.graph_ahead_behind(local_head.id(), remote_head.id()), Ok(number_unique_commits) if number_unique_commits.0 > 0), 111 | ) 112 | } 113 | 114 | fn choose_remote_greedily( 115 | repository: &Repository, 116 | ) -> Result<(Option>, Option), git2::Error> { 117 | let remotes = repository.remotes()?; 118 | Ok(match remotes.get(0) { 119 | Some(remote_name) => ( 120 | Some(repository.find_remote(remote_name)?), 121 | Some(remote_name.to_string()), 122 | ), 123 | None => (None, None), 124 | }) 125 | } 126 | } 127 | --------------------------------------------------------------------------------