├── .editorconfig ├── .envrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── qa.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── nix └── build.nix ├── screenshot.png └── src ├── cfg.rs ├── cli.rs ├── fetch.rs ├── gitlab-query.graphql ├── gitlab_api.rs ├── main.rs └── views.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | max_line_length = 80 13 | 14 | [{*.graphql,*.nix,*.toml,*.yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: phip1611 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "*" 10 | update-types: [ "version-update:semver-patch" ] 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: monthly 15 | open-pull-requests-limit: 10 16 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | on: [ merge_group, push, pull_request ] 3 | jobs: 4 | spellcheck: 5 | name: Spellcheck 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | # Executes "typos ." 10 | - uses: crate-ci/typos@v1.32.0 11 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | push: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | style: 13 | name: Code style 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup Rust toolchain 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | toolchain: stable 21 | components: rustfmt, clippy 22 | - uses: Swatinem/rust-cache@v2 23 | - name: cargo fmt 24 | run: cargo fmt -- --check 25 | - name: cargo clippy 26 | run: cargo clippy 27 | 28 | build_regular: 29 | name: Build (regular) 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | runs-on: [ macos-latest, ubuntu-latest, windows-latest ] 34 | rust: [ 1.74.0, stable, nightly ] 35 | runs-on: ${{ matrix.runs-on }} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Setup Rust toolchain 39 | uses: dtolnay/rust-toolchain@stable 40 | with: 41 | toolchain: ${{ matrix.rust }} 42 | - uses: Swatinem/rust-cache@v2 43 | with: 44 | key: "${{ matrix.runs-on }}-${{ matrix.rust }}" 45 | - name: cargo check (debug) 46 | run: cargo check --verbose 47 | - name: cargo check (release) 48 | run: cargo check --verbose --release 49 | - name: cargo build (debug) 50 | run: cargo build --verbose 51 | - name: cargo build (release) 52 | run: cargo build --verbose --release 53 | - run: cargo test --verbose 54 | - name: "CLI: --help" 55 | run: cargo run --release -- --help 56 | 57 | build_nix: 58 | name: Build (nix) 59 | strategy: 60 | fail-fast: false 61 | matrix: 62 | runs-on: [ macos-latest, ubuntu-latest ] 63 | runs-on: ${{ matrix.runs-on }} 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: cachix/install-nix-action@v31 67 | - run: nix build . 68 | - run: nix run . -- --help 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result* 3 | /.direnv 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased (Yet) 2 | 3 | ## v0.5.0 (2024-12-17) 4 | 5 | - Added basic error reporting. For example, the CLI might now tell you: 6 | - ``` 7 | Error: 8 | Failed to parse response body as JSON 9 | 10 | Caused by: 11 | 0: error decoding response body 12 | 1: missing field `webUrl2` at line 1 column 308 13 | ``` 14 | - ``` 15 | Error: 16 | Failed to receive proper response 17 | 18 | Caused by: 19 | HTTP status client error (401 Unauthorized) for url (https://gitlab.vpn.cyberus-technology.de/api/graphql) 20 | ``` 21 | 22 | ## v0.4.1 (2024-12-17) 23 | 24 | - Better error reporting: it is now clearer if the request failed due to a 25 | wrong or expired token, for example. 26 | 27 | ## v0.4.0 (2024-09-03) 28 | 29 | - Added `-x/--extended-summary` flag to show an extended summary at the end 30 | of the output, where the summarized time per epic and per issue is listed. 31 | - internal code improvements 32 | 33 | ## v0.3.0 (2024-08-26) 34 | 35 | - time span filtering already happens on the server-side, which accelerates 36 | requests by a notable amount. 37 | - updated dependencies 38 | 39 | ## v0.2.2 (2024-07-04) 40 | 41 | - improve handling of default xdg config dir (unix only) 42 | - fix typos 43 | 44 | ## v0.2.1 (2024-07-04) 45 | 46 | - fix documentation bugs 47 | 48 | ## v0.2.0 (2024-07-04) 49 | 50 | - tool now also builds and runs on Windows 51 | - the nix build for Darwin/macOS was fixed 52 | 53 | ## v0.1.1 (2024-07-04) 54 | 55 | - doc & README updates 56 | 57 | ## v0.1.0 (2024-06-27) 58 | 59 | - initial release 60 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 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 = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys 0.59.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 79 | dependencies = [ 80 | "anstyle", 81 | "windows-sys 0.59.0", 82 | ] 83 | 84 | [[package]] 85 | name = "anyhow" 86 | version = "1.0.94" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" 89 | 90 | [[package]] 91 | name = "atomic-waker" 92 | version = "1.1.2" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 95 | 96 | [[package]] 97 | name = "autocfg" 98 | version = "1.4.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 101 | 102 | [[package]] 103 | name = "backtrace" 104 | version = "0.3.74" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 107 | dependencies = [ 108 | "addr2line", 109 | "cfg-if", 110 | "libc", 111 | "miniz_oxide", 112 | "object", 113 | "rustc-demangle", 114 | "windows-targets", 115 | ] 116 | 117 | [[package]] 118 | name = "base64" 119 | version = "0.22.1" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 122 | 123 | [[package]] 124 | name = "bitflags" 125 | version = "2.6.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 128 | 129 | [[package]] 130 | name = "bumpalo" 131 | version = "3.16.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 134 | 135 | [[package]] 136 | name = "bytes" 137 | version = "1.9.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 140 | 141 | [[package]] 142 | name = "cc" 143 | version = "1.2.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" 146 | dependencies = [ 147 | "shlex", 148 | ] 149 | 150 | [[package]] 151 | name = "cfg-if" 152 | version = "1.0.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 155 | 156 | [[package]] 157 | name = "chrono" 158 | version = "0.4.39" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 161 | dependencies = [ 162 | "android-tzdata", 163 | "iana-time-zone", 164 | "num-traits", 165 | "serde", 166 | "windows-targets", 167 | ] 168 | 169 | [[package]] 170 | name = "clap" 171 | version = "4.5.23" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 174 | dependencies = [ 175 | "clap_builder", 176 | "clap_derive", 177 | ] 178 | 179 | [[package]] 180 | name = "clap_builder" 181 | version = "4.5.23" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 184 | dependencies = [ 185 | "anstream", 186 | "anstyle", 187 | "clap_lex", 188 | "strsim", 189 | "terminal_size", 190 | "unicase", 191 | "unicode-width", 192 | ] 193 | 194 | [[package]] 195 | name = "clap_derive" 196 | version = "4.5.18" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 199 | dependencies = [ 200 | "heck", 201 | "proc-macro2", 202 | "quote", 203 | "syn", 204 | ] 205 | 206 | [[package]] 207 | name = "clap_lex" 208 | version = "0.7.4" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 211 | 212 | [[package]] 213 | name = "colorchoice" 214 | version = "1.0.3" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 217 | 218 | [[package]] 219 | name = "core-foundation" 220 | version = "0.9.4" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 223 | dependencies = [ 224 | "core-foundation-sys", 225 | "libc", 226 | ] 227 | 228 | [[package]] 229 | name = "core-foundation-sys" 230 | version = "0.8.7" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 233 | 234 | [[package]] 235 | name = "displaydoc" 236 | version = "0.2.5" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 239 | dependencies = [ 240 | "proc-macro2", 241 | "quote", 242 | "syn", 243 | ] 244 | 245 | [[package]] 246 | name = "encoding_rs" 247 | version = "0.8.35" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 250 | dependencies = [ 251 | "cfg-if", 252 | ] 253 | 254 | [[package]] 255 | name = "equivalent" 256 | version = "1.0.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 259 | 260 | [[package]] 261 | name = "errno" 262 | version = "0.3.10" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 265 | dependencies = [ 266 | "libc", 267 | "windows-sys 0.59.0", 268 | ] 269 | 270 | [[package]] 271 | name = "fastrand" 272 | version = "2.3.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 275 | 276 | [[package]] 277 | name = "fnv" 278 | version = "1.0.7" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 281 | 282 | [[package]] 283 | name = "foreign-types" 284 | version = "0.3.2" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 287 | dependencies = [ 288 | "foreign-types-shared", 289 | ] 290 | 291 | [[package]] 292 | name = "foreign-types-shared" 293 | version = "0.1.1" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 296 | 297 | [[package]] 298 | name = "form_urlencoded" 299 | version = "1.2.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 302 | dependencies = [ 303 | "percent-encoding", 304 | ] 305 | 306 | [[package]] 307 | name = "futures-channel" 308 | version = "0.3.31" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 311 | dependencies = [ 312 | "futures-core", 313 | "futures-sink", 314 | ] 315 | 316 | [[package]] 317 | name = "futures-core" 318 | version = "0.3.31" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 321 | 322 | [[package]] 323 | name = "futures-io" 324 | version = "0.3.31" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 327 | 328 | [[package]] 329 | name = "futures-sink" 330 | version = "0.3.31" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 333 | 334 | [[package]] 335 | name = "futures-task" 336 | version = "0.3.31" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 339 | 340 | [[package]] 341 | name = "futures-util" 342 | version = "0.3.31" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 345 | dependencies = [ 346 | "futures-core", 347 | "futures-io", 348 | "futures-sink", 349 | "futures-task", 350 | "memchr", 351 | "pin-project-lite", 352 | "pin-utils", 353 | "slab", 354 | ] 355 | 356 | [[package]] 357 | name = "getrandom" 358 | version = "0.2.15" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 361 | dependencies = [ 362 | "cfg-if", 363 | "libc", 364 | "wasi", 365 | ] 366 | 367 | [[package]] 368 | name = "gimli" 369 | version = "0.31.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 372 | 373 | [[package]] 374 | name = "gitlab-timelogs" 375 | version = "0.5.0" 376 | dependencies = [ 377 | "anyhow", 378 | "chrono", 379 | "clap", 380 | "nu-ansi-term", 381 | "reqwest", 382 | "serde", 383 | "serde_json", 384 | "toml", 385 | ] 386 | 387 | [[package]] 388 | name = "h2" 389 | version = "0.4.7" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" 392 | dependencies = [ 393 | "atomic-waker", 394 | "bytes", 395 | "fnv", 396 | "futures-core", 397 | "futures-sink", 398 | "http", 399 | "indexmap", 400 | "slab", 401 | "tokio", 402 | "tokio-util", 403 | "tracing", 404 | ] 405 | 406 | [[package]] 407 | name = "hashbrown" 408 | version = "0.15.2" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 411 | 412 | [[package]] 413 | name = "heck" 414 | version = "0.5.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 417 | 418 | [[package]] 419 | name = "http" 420 | version = "1.2.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 423 | dependencies = [ 424 | "bytes", 425 | "fnv", 426 | "itoa", 427 | ] 428 | 429 | [[package]] 430 | name = "http-body" 431 | version = "1.0.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 434 | dependencies = [ 435 | "bytes", 436 | "http", 437 | ] 438 | 439 | [[package]] 440 | name = "http-body-util" 441 | version = "0.1.2" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 444 | dependencies = [ 445 | "bytes", 446 | "futures-util", 447 | "http", 448 | "http-body", 449 | "pin-project-lite", 450 | ] 451 | 452 | [[package]] 453 | name = "httparse" 454 | version = "1.9.5" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 457 | 458 | [[package]] 459 | name = "hyper" 460 | version = "1.5.2" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" 463 | dependencies = [ 464 | "bytes", 465 | "futures-channel", 466 | "futures-util", 467 | "h2", 468 | "http", 469 | "http-body", 470 | "httparse", 471 | "itoa", 472 | "pin-project-lite", 473 | "smallvec", 474 | "tokio", 475 | "want", 476 | ] 477 | 478 | [[package]] 479 | name = "hyper-rustls" 480 | version = "0.27.3" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" 483 | dependencies = [ 484 | "futures-util", 485 | "http", 486 | "hyper", 487 | "hyper-util", 488 | "rustls", 489 | "rustls-pki-types", 490 | "tokio", 491 | "tokio-rustls", 492 | "tower-service", 493 | ] 494 | 495 | [[package]] 496 | name = "hyper-tls" 497 | version = "0.6.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 500 | dependencies = [ 501 | "bytes", 502 | "http-body-util", 503 | "hyper", 504 | "hyper-util", 505 | "native-tls", 506 | "tokio", 507 | "tokio-native-tls", 508 | "tower-service", 509 | ] 510 | 511 | [[package]] 512 | name = "hyper-util" 513 | version = "0.1.10" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 516 | dependencies = [ 517 | "bytes", 518 | "futures-channel", 519 | "futures-util", 520 | "http", 521 | "http-body", 522 | "hyper", 523 | "pin-project-lite", 524 | "socket2", 525 | "tokio", 526 | "tower-service", 527 | "tracing", 528 | ] 529 | 530 | [[package]] 531 | name = "iana-time-zone" 532 | version = "0.1.61" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 535 | dependencies = [ 536 | "android_system_properties", 537 | "core-foundation-sys", 538 | "iana-time-zone-haiku", 539 | "js-sys", 540 | "wasm-bindgen", 541 | "windows-core", 542 | ] 543 | 544 | [[package]] 545 | name = "iana-time-zone-haiku" 546 | version = "0.1.2" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 549 | dependencies = [ 550 | "cc", 551 | ] 552 | 553 | [[package]] 554 | name = "icu_collections" 555 | version = "1.5.0" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 558 | dependencies = [ 559 | "displaydoc", 560 | "yoke", 561 | "zerofrom", 562 | "zerovec", 563 | ] 564 | 565 | [[package]] 566 | name = "icu_locid" 567 | version = "1.5.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 570 | dependencies = [ 571 | "displaydoc", 572 | "litemap", 573 | "tinystr", 574 | "writeable", 575 | "zerovec", 576 | ] 577 | 578 | [[package]] 579 | name = "icu_locid_transform" 580 | version = "1.5.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 583 | dependencies = [ 584 | "displaydoc", 585 | "icu_locid", 586 | "icu_locid_transform_data", 587 | "icu_provider", 588 | "tinystr", 589 | "zerovec", 590 | ] 591 | 592 | [[package]] 593 | name = "icu_locid_transform_data" 594 | version = "1.5.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 597 | 598 | [[package]] 599 | name = "icu_normalizer" 600 | version = "1.5.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 603 | dependencies = [ 604 | "displaydoc", 605 | "icu_collections", 606 | "icu_normalizer_data", 607 | "icu_properties", 608 | "icu_provider", 609 | "smallvec", 610 | "utf16_iter", 611 | "utf8_iter", 612 | "write16", 613 | "zerovec", 614 | ] 615 | 616 | [[package]] 617 | name = "icu_normalizer_data" 618 | version = "1.5.0" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 621 | 622 | [[package]] 623 | name = "icu_properties" 624 | version = "1.5.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 627 | dependencies = [ 628 | "displaydoc", 629 | "icu_collections", 630 | "icu_locid_transform", 631 | "icu_properties_data", 632 | "icu_provider", 633 | "tinystr", 634 | "zerovec", 635 | ] 636 | 637 | [[package]] 638 | name = "icu_properties_data" 639 | version = "1.5.0" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 642 | 643 | [[package]] 644 | name = "icu_provider" 645 | version = "1.5.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 648 | dependencies = [ 649 | "displaydoc", 650 | "icu_locid", 651 | "icu_provider_macros", 652 | "stable_deref_trait", 653 | "tinystr", 654 | "writeable", 655 | "yoke", 656 | "zerofrom", 657 | "zerovec", 658 | ] 659 | 660 | [[package]] 661 | name = "icu_provider_macros" 662 | version = "1.5.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 665 | dependencies = [ 666 | "proc-macro2", 667 | "quote", 668 | "syn", 669 | ] 670 | 671 | [[package]] 672 | name = "idna" 673 | version = "1.0.3" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 676 | dependencies = [ 677 | "idna_adapter", 678 | "smallvec", 679 | "utf8_iter", 680 | ] 681 | 682 | [[package]] 683 | name = "idna_adapter" 684 | version = "1.2.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 687 | dependencies = [ 688 | "icu_normalizer", 689 | "icu_properties", 690 | ] 691 | 692 | [[package]] 693 | name = "indexmap" 694 | version = "2.7.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 697 | dependencies = [ 698 | "equivalent", 699 | "hashbrown", 700 | ] 701 | 702 | [[package]] 703 | name = "ipnet" 704 | version = "2.10.1" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" 707 | 708 | [[package]] 709 | name = "is_terminal_polyfill" 710 | version = "1.70.1" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 713 | 714 | [[package]] 715 | name = "itoa" 716 | version = "1.0.14" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 719 | 720 | [[package]] 721 | name = "js-sys" 722 | version = "0.3.76" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 725 | dependencies = [ 726 | "once_cell", 727 | "wasm-bindgen", 728 | ] 729 | 730 | [[package]] 731 | name = "libc" 732 | version = "0.2.168" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" 735 | 736 | [[package]] 737 | name = "linux-raw-sys" 738 | version = "0.4.14" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 741 | 742 | [[package]] 743 | name = "litemap" 744 | version = "0.7.4" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 747 | 748 | [[package]] 749 | name = "log" 750 | version = "0.4.22" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 753 | 754 | [[package]] 755 | name = "memchr" 756 | version = "2.7.4" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 759 | 760 | [[package]] 761 | name = "mime" 762 | version = "0.3.17" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 765 | 766 | [[package]] 767 | name = "miniz_oxide" 768 | version = "0.8.1" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "a2ef2593ffb6958c941575cee70c8e257438749971869c4ae5acf6f91a168a61" 771 | dependencies = [ 772 | "adler2", 773 | ] 774 | 775 | [[package]] 776 | name = "mio" 777 | version = "1.0.3" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 780 | dependencies = [ 781 | "libc", 782 | "wasi", 783 | "windows-sys 0.52.0", 784 | ] 785 | 786 | [[package]] 787 | name = "native-tls" 788 | version = "0.2.12" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 791 | dependencies = [ 792 | "libc", 793 | "log", 794 | "openssl", 795 | "openssl-probe", 796 | "openssl-sys", 797 | "schannel", 798 | "security-framework", 799 | "security-framework-sys", 800 | "tempfile", 801 | ] 802 | 803 | [[package]] 804 | name = "nu-ansi-term" 805 | version = "0.50.1" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" 808 | dependencies = [ 809 | "windows-sys 0.52.0", 810 | ] 811 | 812 | [[package]] 813 | name = "num-traits" 814 | version = "0.2.19" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 817 | dependencies = [ 818 | "autocfg", 819 | ] 820 | 821 | [[package]] 822 | name = "object" 823 | version = "0.36.5" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 826 | dependencies = [ 827 | "memchr", 828 | ] 829 | 830 | [[package]] 831 | name = "once_cell" 832 | version = "1.20.2" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 835 | 836 | [[package]] 837 | name = "openssl" 838 | version = "0.10.68" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" 841 | dependencies = [ 842 | "bitflags", 843 | "cfg-if", 844 | "foreign-types", 845 | "libc", 846 | "once_cell", 847 | "openssl-macros", 848 | "openssl-sys", 849 | ] 850 | 851 | [[package]] 852 | name = "openssl-macros" 853 | version = "0.1.1" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 856 | dependencies = [ 857 | "proc-macro2", 858 | "quote", 859 | "syn", 860 | ] 861 | 862 | [[package]] 863 | name = "openssl-probe" 864 | version = "0.1.5" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 867 | 868 | [[package]] 869 | name = "openssl-sys" 870 | version = "0.9.104" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" 873 | dependencies = [ 874 | "cc", 875 | "libc", 876 | "pkg-config", 877 | "vcpkg", 878 | ] 879 | 880 | [[package]] 881 | name = "percent-encoding" 882 | version = "2.3.1" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 885 | 886 | [[package]] 887 | name = "pin-project-lite" 888 | version = "0.2.15" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 891 | 892 | [[package]] 893 | name = "pin-utils" 894 | version = "0.1.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 897 | 898 | [[package]] 899 | name = "pkg-config" 900 | version = "0.3.31" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 903 | 904 | [[package]] 905 | name = "proc-macro2" 906 | version = "1.0.92" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 909 | dependencies = [ 910 | "unicode-ident", 911 | ] 912 | 913 | [[package]] 914 | name = "quote" 915 | version = "1.0.37" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 918 | dependencies = [ 919 | "proc-macro2", 920 | ] 921 | 922 | [[package]] 923 | name = "reqwest" 924 | version = "0.12.9" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" 927 | dependencies = [ 928 | "base64", 929 | "bytes", 930 | "encoding_rs", 931 | "futures-channel", 932 | "futures-core", 933 | "futures-util", 934 | "h2", 935 | "http", 936 | "http-body", 937 | "http-body-util", 938 | "hyper", 939 | "hyper-rustls", 940 | "hyper-tls", 941 | "hyper-util", 942 | "ipnet", 943 | "js-sys", 944 | "log", 945 | "mime", 946 | "native-tls", 947 | "once_cell", 948 | "percent-encoding", 949 | "pin-project-lite", 950 | "rustls-pemfile", 951 | "serde", 952 | "serde_json", 953 | "serde_urlencoded", 954 | "sync_wrapper", 955 | "system-configuration", 956 | "tokio", 957 | "tokio-native-tls", 958 | "tower-service", 959 | "url", 960 | "wasm-bindgen", 961 | "wasm-bindgen-futures", 962 | "web-sys", 963 | "windows-registry", 964 | ] 965 | 966 | [[package]] 967 | name = "ring" 968 | version = "0.17.8" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 971 | dependencies = [ 972 | "cc", 973 | "cfg-if", 974 | "getrandom", 975 | "libc", 976 | "spin", 977 | "untrusted", 978 | "windows-sys 0.52.0", 979 | ] 980 | 981 | [[package]] 982 | name = "rustc-demangle" 983 | version = "0.1.24" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 986 | 987 | [[package]] 988 | name = "rustix" 989 | version = "0.38.42" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 992 | dependencies = [ 993 | "bitflags", 994 | "errno", 995 | "libc", 996 | "linux-raw-sys", 997 | "windows-sys 0.59.0", 998 | ] 999 | 1000 | [[package]] 1001 | name = "rustls" 1002 | version = "0.23.20" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" 1005 | dependencies = [ 1006 | "once_cell", 1007 | "rustls-pki-types", 1008 | "rustls-webpki", 1009 | "subtle", 1010 | "zeroize", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "rustls-pemfile" 1015 | version = "2.2.0" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1018 | dependencies = [ 1019 | "rustls-pki-types", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "rustls-pki-types" 1024 | version = "1.10.1" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" 1027 | 1028 | [[package]] 1029 | name = "rustls-webpki" 1030 | version = "0.102.8" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1033 | dependencies = [ 1034 | "ring", 1035 | "rustls-pki-types", 1036 | "untrusted", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "ryu" 1041 | version = "1.0.18" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1044 | 1045 | [[package]] 1046 | name = "schannel" 1047 | version = "0.1.27" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1050 | dependencies = [ 1051 | "windows-sys 0.59.0", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "security-framework" 1056 | version = "2.11.1" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1059 | dependencies = [ 1060 | "bitflags", 1061 | "core-foundation", 1062 | "core-foundation-sys", 1063 | "libc", 1064 | "security-framework-sys", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "security-framework-sys" 1069 | version = "2.12.1" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" 1072 | dependencies = [ 1073 | "core-foundation-sys", 1074 | "libc", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "serde" 1079 | version = "1.0.216" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 1082 | dependencies = [ 1083 | "serde_derive", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "serde_derive" 1088 | version = "1.0.216" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 1091 | dependencies = [ 1092 | "proc-macro2", 1093 | "quote", 1094 | "syn", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "serde_json" 1099 | version = "1.0.133" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 1102 | dependencies = [ 1103 | "itoa", 1104 | "memchr", 1105 | "ryu", 1106 | "serde", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "serde_spanned" 1111 | version = "0.6.8" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1114 | dependencies = [ 1115 | "serde", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "serde_urlencoded" 1120 | version = "0.7.1" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1123 | dependencies = [ 1124 | "form_urlencoded", 1125 | "itoa", 1126 | "ryu", 1127 | "serde", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "shlex" 1132 | version = "1.3.0" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1135 | 1136 | [[package]] 1137 | name = "slab" 1138 | version = "0.4.9" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1141 | dependencies = [ 1142 | "autocfg", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "smallvec" 1147 | version = "1.13.2" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1150 | 1151 | [[package]] 1152 | name = "socket2" 1153 | version = "0.5.8" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1156 | dependencies = [ 1157 | "libc", 1158 | "windows-sys 0.52.0", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "spin" 1163 | version = "0.9.8" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1166 | 1167 | [[package]] 1168 | name = "stable_deref_trait" 1169 | version = "1.2.0" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1172 | 1173 | [[package]] 1174 | name = "strsim" 1175 | version = "0.11.1" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1178 | 1179 | [[package]] 1180 | name = "subtle" 1181 | version = "2.6.1" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1184 | 1185 | [[package]] 1186 | name = "syn" 1187 | version = "2.0.90" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 1190 | dependencies = [ 1191 | "proc-macro2", 1192 | "quote", 1193 | "unicode-ident", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "sync_wrapper" 1198 | version = "1.0.2" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1201 | dependencies = [ 1202 | "futures-core", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "synstructure" 1207 | version = "0.13.1" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1210 | dependencies = [ 1211 | "proc-macro2", 1212 | "quote", 1213 | "syn", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "system-configuration" 1218 | version = "0.6.1" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1221 | dependencies = [ 1222 | "bitflags", 1223 | "core-foundation", 1224 | "system-configuration-sys", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "system-configuration-sys" 1229 | version = "0.6.0" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1232 | dependencies = [ 1233 | "core-foundation-sys", 1234 | "libc", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "tempfile" 1239 | version = "3.14.0" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 1242 | dependencies = [ 1243 | "cfg-if", 1244 | "fastrand", 1245 | "once_cell", 1246 | "rustix", 1247 | "windows-sys 0.59.0", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "terminal_size" 1252 | version = "0.4.1" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" 1255 | dependencies = [ 1256 | "rustix", 1257 | "windows-sys 0.59.0", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "tinystr" 1262 | version = "0.7.6" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1265 | dependencies = [ 1266 | "displaydoc", 1267 | "zerovec", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "tokio" 1272 | version = "1.42.0" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" 1275 | dependencies = [ 1276 | "backtrace", 1277 | "bytes", 1278 | "libc", 1279 | "mio", 1280 | "pin-project-lite", 1281 | "socket2", 1282 | "windows-sys 0.52.0", 1283 | ] 1284 | 1285 | [[package]] 1286 | name = "tokio-native-tls" 1287 | version = "0.3.1" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1290 | dependencies = [ 1291 | "native-tls", 1292 | "tokio", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "tokio-rustls" 1297 | version = "0.26.1" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" 1300 | dependencies = [ 1301 | "rustls", 1302 | "tokio", 1303 | ] 1304 | 1305 | [[package]] 1306 | name = "tokio-util" 1307 | version = "0.7.13" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 1310 | dependencies = [ 1311 | "bytes", 1312 | "futures-core", 1313 | "futures-sink", 1314 | "pin-project-lite", 1315 | "tokio", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "toml" 1320 | version = "0.8.19" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1323 | dependencies = [ 1324 | "serde", 1325 | "serde_spanned", 1326 | "toml_datetime", 1327 | "toml_edit", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "toml_datetime" 1332 | version = "0.6.8" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1335 | dependencies = [ 1336 | "serde", 1337 | ] 1338 | 1339 | [[package]] 1340 | name = "toml_edit" 1341 | version = "0.22.22" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 1344 | dependencies = [ 1345 | "indexmap", 1346 | "serde", 1347 | "serde_spanned", 1348 | "toml_datetime", 1349 | "winnow", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "tower-service" 1354 | version = "0.3.3" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1357 | 1358 | [[package]] 1359 | name = "tracing" 1360 | version = "0.1.41" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1363 | dependencies = [ 1364 | "pin-project-lite", 1365 | "tracing-core", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "tracing-core" 1370 | version = "0.1.33" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1373 | dependencies = [ 1374 | "once_cell", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "try-lock" 1379 | version = "0.2.5" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1382 | 1383 | [[package]] 1384 | name = "unicase" 1385 | version = "2.8.0" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" 1388 | 1389 | [[package]] 1390 | name = "unicode-ident" 1391 | version = "1.0.14" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1394 | 1395 | [[package]] 1396 | name = "unicode-width" 1397 | version = "0.2.0" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1400 | 1401 | [[package]] 1402 | name = "untrusted" 1403 | version = "0.9.0" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1406 | 1407 | [[package]] 1408 | name = "url" 1409 | version = "2.5.4" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1412 | dependencies = [ 1413 | "form_urlencoded", 1414 | "idna", 1415 | "percent-encoding", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "utf16_iter" 1420 | version = "1.0.5" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1423 | 1424 | [[package]] 1425 | name = "utf8_iter" 1426 | version = "1.0.4" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1429 | 1430 | [[package]] 1431 | name = "utf8parse" 1432 | version = "0.2.2" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1435 | 1436 | [[package]] 1437 | name = "vcpkg" 1438 | version = "0.2.15" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1441 | 1442 | [[package]] 1443 | name = "want" 1444 | version = "0.3.1" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1447 | dependencies = [ 1448 | "try-lock", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "wasi" 1453 | version = "0.11.0+wasi-snapshot-preview1" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1456 | 1457 | [[package]] 1458 | name = "wasm-bindgen" 1459 | version = "0.2.99" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 1462 | dependencies = [ 1463 | "cfg-if", 1464 | "once_cell", 1465 | "wasm-bindgen-macro", 1466 | ] 1467 | 1468 | [[package]] 1469 | name = "wasm-bindgen-backend" 1470 | version = "0.2.99" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 1473 | dependencies = [ 1474 | "bumpalo", 1475 | "log", 1476 | "proc-macro2", 1477 | "quote", 1478 | "syn", 1479 | "wasm-bindgen-shared", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "wasm-bindgen-futures" 1484 | version = "0.4.49" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" 1487 | dependencies = [ 1488 | "cfg-if", 1489 | "js-sys", 1490 | "once_cell", 1491 | "wasm-bindgen", 1492 | "web-sys", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "wasm-bindgen-macro" 1497 | version = "0.2.99" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 1500 | dependencies = [ 1501 | "quote", 1502 | "wasm-bindgen-macro-support", 1503 | ] 1504 | 1505 | [[package]] 1506 | name = "wasm-bindgen-macro-support" 1507 | version = "0.2.99" 1508 | source = "registry+https://github.com/rust-lang/crates.io-index" 1509 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 1510 | dependencies = [ 1511 | "proc-macro2", 1512 | "quote", 1513 | "syn", 1514 | "wasm-bindgen-backend", 1515 | "wasm-bindgen-shared", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "wasm-bindgen-shared" 1520 | version = "0.2.99" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 1523 | 1524 | [[package]] 1525 | name = "web-sys" 1526 | version = "0.3.76" 1527 | source = "registry+https://github.com/rust-lang/crates.io-index" 1528 | checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" 1529 | dependencies = [ 1530 | "js-sys", 1531 | "wasm-bindgen", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "windows-core" 1536 | version = "0.52.0" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1539 | dependencies = [ 1540 | "windows-targets", 1541 | ] 1542 | 1543 | [[package]] 1544 | name = "windows-registry" 1545 | version = "0.2.0" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 1548 | dependencies = [ 1549 | "windows-result", 1550 | "windows-strings", 1551 | "windows-targets", 1552 | ] 1553 | 1554 | [[package]] 1555 | name = "windows-result" 1556 | version = "0.2.0" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 1559 | dependencies = [ 1560 | "windows-targets", 1561 | ] 1562 | 1563 | [[package]] 1564 | name = "windows-strings" 1565 | version = "0.1.0" 1566 | source = "registry+https://github.com/rust-lang/crates.io-index" 1567 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 1568 | dependencies = [ 1569 | "windows-result", 1570 | "windows-targets", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "windows-sys" 1575 | version = "0.52.0" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1578 | dependencies = [ 1579 | "windows-targets", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "windows-sys" 1584 | version = "0.59.0" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1587 | dependencies = [ 1588 | "windows-targets", 1589 | ] 1590 | 1591 | [[package]] 1592 | name = "windows-targets" 1593 | version = "0.52.6" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1596 | dependencies = [ 1597 | "windows_aarch64_gnullvm", 1598 | "windows_aarch64_msvc", 1599 | "windows_i686_gnu", 1600 | "windows_i686_gnullvm", 1601 | "windows_i686_msvc", 1602 | "windows_x86_64_gnu", 1603 | "windows_x86_64_gnullvm", 1604 | "windows_x86_64_msvc", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "windows_aarch64_gnullvm" 1609 | version = "0.52.6" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1612 | 1613 | [[package]] 1614 | name = "windows_aarch64_msvc" 1615 | version = "0.52.6" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1618 | 1619 | [[package]] 1620 | name = "windows_i686_gnu" 1621 | version = "0.52.6" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1624 | 1625 | [[package]] 1626 | name = "windows_i686_gnullvm" 1627 | version = "0.52.6" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1630 | 1631 | [[package]] 1632 | name = "windows_i686_msvc" 1633 | version = "0.52.6" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1636 | 1637 | [[package]] 1638 | name = "windows_x86_64_gnu" 1639 | version = "0.52.6" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1642 | 1643 | [[package]] 1644 | name = "windows_x86_64_gnullvm" 1645 | version = "0.52.6" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1648 | 1649 | [[package]] 1650 | name = "windows_x86_64_msvc" 1651 | version = "0.52.6" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1654 | 1655 | [[package]] 1656 | name = "winnow" 1657 | version = "0.6.20" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 1660 | dependencies = [ 1661 | "memchr", 1662 | ] 1663 | 1664 | [[package]] 1665 | name = "write16" 1666 | version = "1.0.0" 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" 1668 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1669 | 1670 | [[package]] 1671 | name = "writeable" 1672 | version = "0.5.5" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1675 | 1676 | [[package]] 1677 | name = "yoke" 1678 | version = "0.7.5" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1681 | dependencies = [ 1682 | "serde", 1683 | "stable_deref_trait", 1684 | "yoke-derive", 1685 | "zerofrom", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "yoke-derive" 1690 | version = "0.7.5" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1693 | dependencies = [ 1694 | "proc-macro2", 1695 | "quote", 1696 | "syn", 1697 | "synstructure", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "zerofrom" 1702 | version = "0.1.5" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1705 | dependencies = [ 1706 | "zerofrom-derive", 1707 | ] 1708 | 1709 | [[package]] 1710 | name = "zerofrom-derive" 1711 | version = "0.1.5" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1714 | dependencies = [ 1715 | "proc-macro2", 1716 | "quote", 1717 | "syn", 1718 | "synstructure", 1719 | ] 1720 | 1721 | [[package]] 1722 | name = "zeroize" 1723 | version = "1.8.1" 1724 | source = "registry+https://github.com/rust-lang/crates.io-index" 1725 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1726 | 1727 | [[package]] 1728 | name = "zerovec" 1729 | version = "0.10.4" 1730 | source = "registry+https://github.com/rust-lang/crates.io-index" 1731 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1732 | dependencies = [ 1733 | "yoke", 1734 | "zerofrom", 1735 | "zerovec-derive", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "zerovec-derive" 1740 | version = "0.10.3" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1743 | dependencies = [ 1744 | "proc-macro2", 1745 | "quote", 1746 | "syn", 1747 | ] 1748 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitlab-timelogs" 3 | description = """ 4 | CLI utility to assist you with your time logs in GitLab. 5 | 6 | gitlab-timelogs is not associated with the official GitLab project! 7 | """ 8 | version = "0.5.0" 9 | edition = "2021" 10 | keywords = ["gitlab", "productivity"] 11 | categories = ["command-line-utilities"] 12 | readme = "README.md" 13 | license = "MIT" 14 | homepage = "https://github.com/phip1611/gitlab-timelogs" 15 | repository = "https://github.com/phip1611/gitlab-timelogs" 16 | documentation = "https://docs.rs/gitlab-timelogs" 17 | authors = [ 18 | "Philipp Schuster " 19 | ] 20 | 21 | [dependencies] 22 | anyhow = "1.0.94" 23 | chrono = { version = "0.4.38", default-features = false, features = ["clock", "std", "serde"] } 24 | nu-ansi-term = "0.50.0" 25 | reqwest = { version = "0.12.4", features = ["blocking", "json"] } 26 | serde = { version = "1.0.203", features = ["derive"] } 27 | serde_json = "1.0.117" 28 | toml = "0.8.14" 29 | 30 | [dependencies.clap] 31 | version = "~4.5.4" 32 | features = [ 33 | "color", 34 | "derive", 35 | "env", 36 | "error-context", 37 | "help", 38 | "std", 39 | "suggestions", 40 | "unicode", 41 | "usage", 42 | "wrap_help", 43 | ] 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Philipp Schuster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitlab-timelogs CLI 2 | 3 | CLI utility to assist you with your time logs in GitLab. The GitLab web UI for 4 | time logs is very rudimentary and the UX is poor (June 2024, GitLab 16.11), 5 | especially a summary view is missing. This is where `gitlab-timelogs` help you 6 | by leveraging the GitLab API. 7 | 8 | This CLI is made by developers for developers who want to check their timelogs 9 | at the of the work day or week. `gitlab-timelogs` **is not** associated with the 10 | official GitLab project! 11 | 12 | ![screenshot.png](screenshot.png) 13 | (_The screenshot is slightly outdated. The latest version shows more information._) 14 | 15 | ## Features 16 | 17 | `gitlab-timelogs` provides you with an overview of your time logs and prints 18 | warnings for typical mistakes. It does not allow you to modify entries, but just 19 | to inspect existing records, so you can fix them in GitLab (if necessary). 20 | 21 | - ✅ collect time logs from issues (timelogs associated with MRs currently not 22 | supported) 23 | - ✅ group them by week 24 | - ✅ specify time range 25 | - ✅ print warnings for common pitfalls: 26 | - accounted less than 15 minutes to an issue (typically a mistake) 27 | - accounted time to a Saturday or Sunday (not common in normal positions) 28 | - accounted more than 10h a day (10h is the legal maximum in Germany) 29 | - ✅ Created for GitLab 16.11. Older and newer versions should work as well, 30 | but haven't been tested. Note that the free tier may not support time 31 | logs, but only the enterprise edition. 32 | 33 | ## Supported Platforms 34 | _(For compilation and running.)_ 35 | 36 | `gitlab-timelogs` builds and runs at least on the following platforms: 37 | 38 | - Linux (all architectures, I guess?) 39 | - MacOS (all architectures, I guess?) 40 | - Windows (all architectures, I guess?) 41 | 42 | Note that I only tested recent versions of these OSs in Mid-2024. Older versions 43 | of these systems should work as well. 44 | 45 | ## Consume / Install 46 | 47 | **Via cargo:** 48 | 49 | - `$ cargo install https://github.com/phip1611/gitlab-timelogs` 50 | 51 | **Via Nix / on NixOS:** 52 | 53 | - Option A: [via `nixpkgs`](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=gitlab-timelogs) 54 | - A1: Add `pkgs.gitlab-timelogs` to your packages 55 | - A2: Use `nix-shell -p gitlab-timelogs` 56 | - Option B: consume this Flake/Repository 57 | - B1: Add `gitlab-timelogs.nixosModules.default` (`gitlab-timelogs` is 58 | referring to the flake input) to the modules of your NixOS configuration, 59 | which will add `gitlab-timelogs` to your system-wide packages. 60 | - B2: Open a shell: `$ nix shell github:phip1611/gitlab-timelogs` 61 | - B3: Run the tool: `$ nix run github:phip1611/gitlab-timelogs -- ` 62 | 63 | **Via home-manager:** 64 | 65 | 1. import the home-manager module: `gitlab-timelogs.nixosModules.home-manager` 66 | 2. enable and configure gitlab-timelogs: 67 | 68 | ```nix 69 | gitlab-timelogs = { 70 | enable = true; 71 | config = { 72 | gitlabHost = "gitlab.example.com"; 73 | gitlabUsername = "exampleuser"; 74 | # Either write as a string here, or read from a file that you do not push: 75 | gitlabToken = with builtins; readFile (toPath ./gitlab-token.txt); 76 | }; 77 | }; 78 | ``` 79 | 80 | ## Usage 81 | 82 | - `$ gitlab-timelogs --help` 83 | - `$ gitlab-timelogs --host gitlab.vpn.cyberus-technology.de --username pschuster --token ********** --after 2024-06-01 --before 2024-06-30` 84 | 85 | ### Configuration 86 | 87 | 1. Via CLI options. Type `--help` for guidance. 88 | 2. Via environment variables: 89 | - `GITLAB_HOST` 90 | - `GITLAB_USERNAME` 91 | - `GITLAB_TOKEN` 92 | 3. Via a configuration file either in 93 | `~/.config/gitlab-timelogs/config.toml` (UNIX) or \ 94 | `%LOCALAPPDATA%/gitlab-timelogs/config.toml` (Windows) 95 | with the following content: \ 96 | ```toml 97 | gitlab_host = "gitlab.example.com" 98 | gitlab_username = "" 99 | gitlab_token = "" 100 | ``` 101 | 102 | ## MSRV 103 | 104 | The MSRV is Rust stable `1.74.0`. 105 | 106 | ## Trivia 107 | 108 | The main motivation to create this was the unbelievable poor UX of the GitLab 109 | web UI for time logs. For example, the input mask transformed a `1h 30` to 110 | `3d 7h` instead of `1h 30m`. This common pitfall was unbelievable annoying and 111 | hard to spot - badly influencing a lot of our time records. 112 | 113 | Hence, I created this as part of my work time at [Cyberus Technology GmbH](https://cyberus-technology.de) 114 | to boost our internal productivity. We love open source! Interested in a 115 | cool employer? Contact us! 116 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "locked": { 5 | "lastModified": 1745454774, 6 | "narHash": "sha256-oLvmxOnsEKGtwczxp/CwhrfmQUG2ym24OMWowcoRhH8=", 7 | "owner": "ipetkov", 8 | "repo": "crane", 9 | "rev": "efd36682371678e2b6da3f108fdb5c613b3ec598", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "ipetkov", 14 | "ref": "master", 15 | "repo": "crane", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-parts": { 20 | "inputs": { 21 | "nixpkgs-lib": [ 22 | "nixpkgs" 23 | ] 24 | }, 25 | "locked": { 26 | "lastModified": 1743550720, 27 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", 28 | "owner": "hercules-ci", 29 | "repo": "flake-parts", 30 | "rev": "c621e8422220273271f52058f618c94e405bb0f5", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "hercules-ci", 35 | "repo": "flake-parts", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1746055187, 42 | "narHash": "sha256-3dqArYSMP9hM7Qpy5YWhnSjiqniSaT2uc5h2Po7tmg0=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "3e362ce63e16b9572d8c2297c04f7c19ab6725a5", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "ref": "nixos-24.11", 51 | "repo": "nixpkgs", 52 | "type": "github" 53 | } 54 | }, 55 | "root": { 56 | "inputs": { 57 | "crane": "crane", 58 | "flake-parts": "flake-parts", 59 | "nixpkgs": "nixpkgs" 60 | } 61 | } 62 | }, 63 | "root": "root", 64 | "version": 7 65 | } 66 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "gitlab-timelogs"; 3 | 4 | inputs = { 5 | crane.url = "github:ipetkov/crane/master"; 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 8 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; 9 | }; 10 | 11 | outputs = { self, flake-parts, ... }@inputs: 12 | flake-parts.lib.mkFlake { inherit inputs; } 13 | { 14 | flake = { 15 | nixosModules = { 16 | default = ({ pkgs, ... }: 17 | { 18 | environment.systemPackages = [ 19 | self.packages.${pkgs.system}.gitlab-timelogs 20 | ]; 21 | }); 22 | home-manager = ({ config, pkgs, lib, ... }: 23 | let 24 | cfg = config.gitlab-timelogs; 25 | in 26 | { 27 | options.gitlab-timelogs = { 28 | enable = lib.mkEnableOption "gitlab-timelogs"; 29 | package = lib.mkOption { 30 | type = lib.types.package; 31 | default = self.packages.${pkgs.system}.gitlab-timelogs; 32 | }; 33 | config = lib.mkOption { 34 | description = "The values in your config file."; 35 | type = lib.types.submodule { 36 | options = { 37 | gitlabHost = lib.mkOption { 38 | type = lib.types.str; 39 | description = "Gitlab host you want to query."; 40 | example = "gitlab.example.com"; 41 | }; 42 | gitlabUsername = lib.mkOption { 43 | type = lib.types.str; 44 | description = "Your gitlab username"; 45 | example = "exampleuser"; 46 | }; 47 | gitlabToken = lib.mkOption { 48 | type = lib.types.str; 49 | description = "A gitlab token with read access to the given host."; 50 | example = "glpat-XXXXXXXXXXXXXXXXXXXX"; 51 | }; 52 | }; 53 | }; 54 | }; 55 | }; 56 | config = lib.mkIf cfg.enable { 57 | home.packages = [ 58 | cfg.package 59 | ]; 60 | 61 | home.file.".config/gitlab-timelogs/config.toml".text = '' 62 | gitlab_host = "${cfg.config.gitlabHost}" 63 | gitlab_username = "${cfg.config.gitlabUsername}" 64 | gitlab_token = "${cfg.config.gitlabToken}" 65 | ''; 66 | }; 67 | }); 68 | }; 69 | }; 70 | # Don't artificially limit users at this point. If the build fails, 71 | # they will see soon enough. 72 | systems = inputs.nixpkgs.lib.systems.flakeExposed; 73 | perSystem = { system, self', pkgs, ... }: 74 | { 75 | devShells = { 76 | default = pkgs.mkShell { 77 | inputsFrom = [ self'.packages.default ]; 78 | nativeBuildInputs = [ pkgs.pkg-config ]; 79 | buildInputs = [ 80 | pkgs.openssl 81 | ]; 82 | }; 83 | }; 84 | formatter = pkgs.nixpkgs-fmt; 85 | packages = rec { 86 | default = gitlab-timelogs; 87 | gitlab-timelogs = pkgs.callPackage ./nix/build.nix { 88 | craneLib = inputs.crane.mkLib pkgs; 89 | }; 90 | }; 91 | }; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /nix/build.nix: -------------------------------------------------------------------------------- 1 | { craneLib 2 | , darwin 3 | , lib 4 | , nix-gitignore 5 | , openssl 6 | , iconv 7 | , pkg-config 8 | , stdenv 9 | }: 10 | 11 | let 12 | commonArgs = { 13 | src = nix-gitignore.gitignoreSource [ ] ../.; 14 | # Not using this, as this removes the ".graphql" file. 15 | # src = craneLib.cleanCargoSource ./..; 16 | nativeBuildInputs = [ 17 | pkg-config 18 | ]; 19 | buildInputs = [ 20 | openssl 21 | ] ++ lib.optionals stdenv.isDarwin [ 22 | iconv 23 | darwin.apple_sdk.frameworks.SystemConfiguration 24 | ]; 25 | # Fix build. Reference: 26 | # - https://github.com/sfackler/rust-openssl/issues/1430 27 | # - https://docs.rs/openssl/latest/openssl/ 28 | OPENSSL_NO_VENDOR = true; 29 | }; 30 | 31 | # Downloaded and compiled dependencies. 32 | cargoArtifacts = craneLib.buildDepsOnly commonArgs; 33 | 34 | cargoPackage = craneLib.buildPackage (commonArgs // { 35 | inherit cargoArtifacts; 36 | }); 37 | in 38 | cargoPackage 39 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/gitlab-timelogs/2edf6984b6bdde9e1121d8a3e21108f39654dc7b/screenshot.png -------------------------------------------------------------------------------- /src/cfg.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //! Module for obtaining the effective configuration, based on the configuration 26 | //! file and CLI parameters. 27 | //! 28 | //! [`get_cfg`] is the entry point. 29 | 30 | use crate::cli::{CfgFile, CliArgs}; 31 | use crate::{cli, print_warning}; 32 | use clap::Parser; 33 | use serde::de::DeserializeOwned; 34 | use std::error::Error; 35 | use std::io::ErrorKind; 36 | use std::path::PathBuf; 37 | 38 | /// Returns the path of the config file with respect to the current OS. 39 | fn config_file_path() -> Result> { 40 | #[cfg(target_family = "unix")] 41 | let config_os_dir = { 42 | // First look for XDG_CONFIG_HOME, then fall back to HOME 43 | // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 44 | let home = std::env::var("XDG_CONFIG_HOME").unwrap_or(std::env::var("HOME")?); 45 | PathBuf::from(home).join(".config") 46 | }; 47 | #[cfg(target_family = "windows")] 48 | let config_os_dir = PathBuf::from(std::env::var("LOCALAPPDATA")?); 49 | 50 | let config_dir = config_os_dir.join("gitlab-timelogs"); 51 | Ok(config_dir.join("config.toml")) 52 | } 53 | 54 | /// Reads the config file and parses it from TOML. 55 | /// On UNIX, it uses ` 56 | fn read_config_file() -> Result> { 57 | let config_file = config_file_path()?; 58 | let content = match std::fs::read_to_string(&config_file) { 59 | Ok(c) => c, 60 | Err(e) => { 61 | match e.kind() { 62 | ErrorKind::NotFound => {} 63 | _ => print_warning( 64 | &format!( 65 | "Failed to read config file at {}: {e}", 66 | config_file.display() 67 | ), 68 | 0, 69 | ), 70 | } 71 | 72 | // Treat failure to read a config file as the empty config file. 73 | String::new() 74 | } 75 | }; 76 | 77 | Ok(toml::from_str(&content)?) 78 | } 79 | 80 | /// Parses the command line options but first, reads the config file. If certain 81 | /// command line options are not present, they are taken from the config file. 82 | /// 83 | /// This is a workaround that clap has no built-in support for a config file 84 | /// that serves as source for command line options by itself. The focus is 85 | /// also on the natural error reporting by clap. 86 | pub fn get_cfg() -> Result> { 87 | let config_content = read_config_file::()?; 88 | let config_args: Vec<(String, String)> = config_content.to_cli_args(); 89 | let mut all_args = std::env::args().collect::>(); 90 | 91 | // Push config options as arguments, before parsing them in clap. 92 | for (opt_name, opt_value) in config_args { 93 | if !all_args.contains(&opt_name) { 94 | all_args.push(opt_name); 95 | all_args.push(opt_value); 96 | } 97 | } 98 | 99 | Ok(cli::CliArgs::parse_from(all_args)) 100 | } 101 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use chrono::{Datelike, Local, NaiveDate, TimeDelta, Weekday}; 25 | use clap::Parser; 26 | use std::ops::Sub; 27 | 28 | #[derive(serde::Deserialize)] 29 | pub struct CfgFile { 30 | gitlab_host: Option, 31 | gitlab_username: Option, 32 | gitlab_token: Option, 33 | } 34 | 35 | impl CfgFile { 36 | #[allow(clippy::wrong_self_convention)] 37 | pub fn to_cli_args(self) -> Vec<(String, String)> { 38 | let mut args = Vec::new(); 39 | if let Some(host) = self.gitlab_host { 40 | args.push(("--host".to_string(), host)); 41 | } 42 | if let Some(username) = self.gitlab_username { 43 | args.push(("--username".to_string(), username)); 44 | } 45 | if let Some(token) = self.gitlab_token { 46 | args.push(("--token".to_string(), token)); 47 | } 48 | args 49 | } 50 | } 51 | 52 | /// CLI Arguments for `clap`. If not present, the values are taken from 53 | /// environment variables. 54 | #[derive(Parser, Debug)] 55 | #[command( 56 | version, 57 | about = "\ 58 | Tool to fetch the timelogs from the GitLab API and display them in a helpful 59 | way. Can either be configured via CLI options, environment variables, or by 60 | a configuration file. The configuration file must be placed under 61 | `~/.config/gitlab-timelogs/config.toml` (UNIX) or 62 | `%LOCALAPPDATA%/gitlab-timelogs/config.toml` (Windows), and must follow the 63 | following structure: 64 | 65 | gitlab_host = \"gitlab.example.com\" 66 | gitlab_username = \"\" 67 | gitlab_token = \"\" 68 | 69 | 70 | gitlab-timelogs IS NOT associated with the official GitLab project!" 71 | )] 72 | pub struct CliArgs { 73 | /// The GitLab host without `https://`. For example `gitlab.example.com`. 74 | #[arg(long = "host", env)] 75 | gitlab_host: String, 76 | /// Your GitLab username. 77 | #[arg(long = "username", env)] 78 | gitlab_username: String, 79 | /// Token with read access (scope `read_api`) to GitLab API. You can get one 80 | /// on `https:///-/user_settings/personal_access_tokens`. 81 | #[arg(long = "token", env)] 82 | gitlab_token: String, 83 | /// Filter for newest date (inclusive). For example `2024-06-30`. 84 | /// By default, this defaults to today (local time). 85 | /// 86 | /// Must be no less than `--after`. 87 | #[arg(long = "before")] 88 | gitlab_before: Option, 89 | /// Filter for oldest date (inclusive). For example `2024-06-01`. 90 | /// 91 | /// Must be no more than `--before`. 92 | #[arg(long = "after", default_value_t = get_default_after_date())] 93 | gitlab_after: NaiveDate, 94 | /// Show an extended summary at the end with the time per issue and per 95 | /// epic. 96 | #[arg(short = 'x', long = "extended-summary")] 97 | print_extended_summary: bool, 98 | } 99 | 100 | impl CliArgs { 101 | #[allow(clippy::missing_const_for_fn)] // to keep MSRV 102 | pub fn host(&self) -> &str { 103 | &self.gitlab_host 104 | } 105 | #[allow(clippy::missing_const_for_fn)] // to keep MSRV 106 | pub fn username(&self) -> &str { 107 | &self.gitlab_username 108 | } 109 | #[allow(clippy::missing_const_for_fn)] // to keep MSRV 110 | pub fn token(&self) -> &str { 111 | &self.gitlab_token 112 | } 113 | pub fn before(&self) -> NaiveDate { 114 | // This is a bit of a hack, because Clap's default_value_t doesn't seem 115 | // to work with clap_serde_derive. *sigh* 116 | self.gitlab_before.unwrap_or_else(current_date) 117 | } 118 | pub const fn after(&self) -> NaiveDate { 119 | self.gitlab_after 120 | } 121 | 122 | pub const fn print_extended_summary(&self) -> bool { 123 | self.print_extended_summary 124 | } 125 | } 126 | 127 | fn current_date() -> NaiveDate { 128 | Local::now().naive_local().date() 129 | } 130 | 131 | /// Returns the previous Monday or today, if today is a Monday. 132 | /// This makes sense as one typically wants to see what one has done in the 133 | /// current week. 134 | fn get_default_after_date() -> NaiveDate { 135 | let now = Local::now(); 136 | let mut day = now; 137 | while day.weekday() != Weekday::Mon { 138 | day = day.sub(TimeDelta::days(1)); 139 | } 140 | day.naive_local().date() 141 | } 142 | -------------------------------------------------------------------------------- /src/fetch.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //! Functionality to fetch data from the GitLab API. 26 | //! 27 | //! [`fetch_results`] is the entry point. 28 | 29 | use crate::gitlab_api::types::Response; 30 | use anyhow::Context; 31 | use chrono::{DateTime, Local, NaiveDate, NaiveTime}; 32 | use reqwest::blocking::Client; 33 | use reqwest::header::AUTHORIZATION; 34 | use serde_json::json; 35 | 36 | const GRAPHQL_TEMPLATE: &str = include_str!("./gitlab-query.graphql"); 37 | 38 | /// Transforms a [`NaiveDate`] to a `DateTime`. 39 | fn naive_date_to_local_datetime(date: NaiveDate) -> DateTime { 40 | date.and_time(NaiveTime::MIN) 41 | .and_local_timezone(Local) 42 | .unwrap() 43 | } 44 | 45 | /// Performs a single request against the GitLab API, getting exactly one page 46 | /// of the paged data source. The data is filtered for the date span to make the 47 | /// request smaller/quicker. 48 | /// 49 | /// # Parameters 50 | /// - `username`: The exact GitLab username of the user. 51 | /// - `host`: Host name of the GitLab instance without `https://` 52 | /// - `token`: GitLab token to access the GitLab instance. Must have at least 53 | /// READ access. 54 | /// - `before`: Identifier from previous request to get the next page of the 55 | /// paginated result. 56 | /// - `start_date`: Inclusive begin date. 57 | /// - `end_date`: Inclusive end date. 58 | fn fetch_result( 59 | username: &str, 60 | host: &str, 61 | token: &str, 62 | before: Option<&str>, 63 | start_date: NaiveDate, 64 | end_date: NaiveDate, 65 | ) -> anyhow::Result { 66 | let graphql_query = GRAPHQL_TEMPLATE 67 | .replace("%USERNAME%", username) 68 | .replace("%BEFORE%", before.unwrap_or_default()) 69 | // GitLab API ignores the time component and just looks at the 70 | // date and the timezone. 71 | .replace( 72 | "%START_DATE%", 73 | naive_date_to_local_datetime(start_date) 74 | .to_string() 75 | .as_str(), 76 | ) 77 | // GitLab API ignores the time component and just looks at the 78 | // date and the timezone. 79 | .replace( 80 | "%END_DATE%", 81 | naive_date_to_local_datetime(end_date).to_string().as_str(), 82 | ); 83 | let payload = json!({ "query": graphql_query }); 84 | 85 | let authorization = format!("Bearer {token}"); 86 | let url = format!("https://{host}/api/graphql"); 87 | let client = Client::new(); 88 | 89 | let plain_response = client 90 | .post(url) 91 | .header(AUTHORIZATION, authorization) 92 | .json(&payload) 93 | .send() 94 | .context("Failed to send request")? 95 | .error_for_status() 96 | .context("Failed to receive proper response")?; 97 | 98 | plain_response 99 | .json::() 100 | .context("Failed to parse response body as JSON") 101 | } 102 | 103 | /// Fetches all results from the API with pagination in mind. 104 | /// 105 | /// # Parameters 106 | /// - `username`: The exact GitLab username of the user. 107 | /// - `host`: Host name of the GitLab instance without `https://` 108 | /// - `token`: GitLab token to access the GitLab instance. Must have at least 109 | /// READ access. 110 | /// - `start_date`: Inclusive begin date. 111 | /// - `end_date`: Inclusive end date. 112 | pub fn fetch_results( 113 | username: &str, 114 | host: &str, 115 | token: &str, 116 | start_date: NaiveDate, 117 | end_date: NaiveDate, 118 | ) -> anyhow::Result { 119 | let base = fetch_result(username, host, token, None, start_date, end_date)?; 120 | 121 | let mut aggregated = base; 122 | while aggregated.data.timelogs.pageInfo.hasPreviousPage { 123 | let mut next = fetch_result( 124 | username, 125 | host, 126 | token, 127 | Some( 128 | &aggregated 129 | .data 130 | .timelogs 131 | .pageInfo 132 | .startCursor 133 | .expect("Should be valid string at this point"), 134 | ), 135 | start_date, 136 | end_date, 137 | )?; 138 | 139 | // Ordering here is not that important, happens later anyway. 140 | next.data 141 | .timelogs 142 | .nodes 143 | .extend(aggregated.data.timelogs.nodes); 144 | aggregated = next; 145 | } 146 | Ok(aggregated) 147 | } 148 | -------------------------------------------------------------------------------- /src/gitlab-query.graphql: -------------------------------------------------------------------------------- 1 | { 2 | timelogs(username: "%USERNAME%", last: 500, before: "%BEFORE%", startDate: "%START_DATE%", endDate: "%END_DATE%") { 3 | nodes { 4 | spentAt 5 | timeSpent 6 | summary 7 | issue { 8 | title 9 | webUrl 10 | epic { 11 | title 12 | } 13 | } 14 | project { 15 | group { 16 | fullName 17 | } 18 | } 19 | } 20 | pageInfo { 21 | hasPreviousPage 22 | startCursor 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/gitlab_api.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //! Typings for the GitLab API. These types are specific to the graphql query 26 | //! used by this tool. 27 | 28 | #[allow(non_snake_case)] 29 | pub mod types { 30 | use chrono::{DateTime, Local, NaiveDate}; 31 | use serde::Deserialize; 32 | use std::time::Duration; 33 | 34 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 35 | pub struct Epic { 36 | pub title: String, 37 | } 38 | 39 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 40 | pub struct Issue { 41 | pub title: String, 42 | /// Full http link to issue. 43 | pub webUrl: String, 44 | pub epic: Option, 45 | } 46 | 47 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 48 | pub struct Group { 49 | pub fullName: String, 50 | } 51 | 52 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 53 | pub struct Project { 54 | pub group: Option, 55 | } 56 | 57 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 58 | pub struct ResponseNode { 59 | pub spentAt: String, 60 | /// For some totally weird reason, GitLab allows negative times. 61 | /// We recommend just deleting these records. But to support the 62 | /// deserialization, we have to do it like that. 63 | pub timeSpent: i64, 64 | pub summary: Option, 65 | pub issue: Issue, 66 | pub project: Project, 67 | } 68 | 69 | impl ResponseNode { 70 | /// Returns a duration in seconds. 71 | pub const fn timeSpent(&self) -> (bool, Duration) { 72 | let dur = Duration::from_secs(self.timeSpent.unsigned_abs()); 73 | (self.timeSpent.is_positive(), dur) 74 | } 75 | 76 | pub fn epic_name(&self) -> Option<&str> { 77 | self.issue.epic.as_ref().map(|e| e.title.as_str()) 78 | } 79 | 80 | /// Parses the UTC timestring coming from GitLab in the local timezone of 81 | /// the user. This is necessary so that entries accounted to a Monday on 82 | /// `00:00` in CEST are not displayed as Sunday. The value is returned 83 | /// as [`NaiveDate`] but adjusted to the local time. 84 | pub fn datetime(&self) -> NaiveDate { 85 | let date = DateTime::parse_from_rfc3339(&self.spentAt).unwrap(); 86 | let datetime = DateTime::::from(date); 87 | datetime.naive_local().date() 88 | } 89 | } 90 | 91 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 92 | pub struct ResponsePageInfo { 93 | pub hasPreviousPage: bool, 94 | pub startCursor: Option, 95 | } 96 | 97 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 98 | pub struct ResponseTimelogs { 99 | pub nodes: Vec, 100 | pub pageInfo: ResponsePageInfo, 101 | } 102 | 103 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 104 | pub struct ResponseData { 105 | pub timelogs: ResponseTimelogs, 106 | } 107 | 108 | /// The response from the GitLab API with all timelogs for the given 109 | /// time frame. 110 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 111 | pub struct Response { 112 | pub data: ResponseData, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | #![deny( 25 | clippy::all, 26 | clippy::cargo, 27 | clippy::nursery, 28 | clippy::must_use_candidate, 29 | // clippy::restriction, 30 | // clippy::pedantic 31 | )] 32 | // now allow a few rules which are denied by the above statement 33 | // --> they are ridiculous and not necessary 34 | #![allow( 35 | clippy::suboptimal_flops, 36 | clippy::redundant_pub_crate, 37 | clippy::fallible_impl_from 38 | )] 39 | // I can't do anything about this; fault of the dependencies 40 | #![allow(clippy::multiple_crate_versions)] 41 | #![deny(missing_debug_implementations)] 42 | #![deny(rustdoc::all)] 43 | 44 | use crate::cfg::get_cfg; 45 | use crate::cli::CliArgs; 46 | use crate::fetch::fetch_results; 47 | use crate::gitlab_api::types::ResponseNode; 48 | use anyhow::{anyhow, Context}; 49 | use chrono::{Datelike, NaiveDate, Weekday}; 50 | use nu_ansi_term::{Color, Style}; 51 | use std::error::Error; 52 | use std::time::Duration; 53 | 54 | mod cfg; 55 | mod cli; 56 | mod fetch; 57 | mod gitlab_api; 58 | mod views; 59 | 60 | fn main() -> Result<(), Box> { 61 | let cfg = get_cfg()?; 62 | if cfg.before() < cfg.after() { 63 | Err(anyhow!( 64 | "The `--before` date must come after the `--after` date" 65 | )) 66 | .context("Failed to validate config")?; 67 | } 68 | 69 | let response = fetch_results( 70 | cfg.username(), 71 | cfg.host(), 72 | cfg.token(), 73 | cfg.after(), 74 | cfg.before(), 75 | )?; 76 | 77 | println!("Host : {}", cfg.host()); 78 | println!("Username : {}", cfg.username()); 79 | println!("Time Span: {} - {}", cfg.after(), cfg.before()); 80 | 81 | // All nodes but as vector to references. 82 | // Simplifies the handling with other parts of the code, especially the 83 | // `views` module. 84 | let nodes = response.data.timelogs.nodes.iter().collect::>(); 85 | 86 | if nodes.is_empty() { 87 | print_warning( 88 | "Empty response. Is the username correct? Does the token has read permission?", 89 | 0, 90 | ); 91 | } else { 92 | print_all_weeks(nodes.as_slice(), &cfg); 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | fn print_timelog(log: &ResponseNode) { 99 | let (duration_is_positive, duration) = log.timeSpent(); 100 | print!(" "); 101 | print_duration(duration, Color::Magenta); 102 | println!( 103 | " {issue_name}", 104 | issue_name = Style::new() 105 | .bold() 106 | .fg(Color::Green) 107 | .paint(log.issue.title.clone()), 108 | ); 109 | let min_minutes_threshold = 15; 110 | if !duration_is_positive { 111 | // msg is aligned with the suspicious data output 112 | print_warning( 113 | "^ ERROR: You have logged this time as NEGATIVE: Update the ticket!", 114 | 3, 115 | ); 116 | } 117 | if duration.as_secs() / 60 < min_minutes_threshold { 118 | // msg is aligned with the suspicious data output 119 | print_warning("^ WARN: Less than 15 minutes! Is this correct?", 6); 120 | } 121 | 122 | // Print issue metadata. 123 | let epic_name = log.epic_name().unwrap_or(""); 124 | let whitespace = " ".repeat(11); 125 | println!( 126 | "{whitespace}{link}", 127 | link = Style::new().dimmed().paint(&log.issue.webUrl) 128 | ); 129 | if let Some(group) = &log.project.group { 130 | println!( 131 | "{whitespace}[{epic_key} {epic_name}, {group_key} {group_name}]", 132 | epic_key = Style::new().dimmed().paint("Epic:"), 133 | epic_name = Style::new().bold().paint(epic_name), 134 | group_key = Style::new().dimmed().paint("Group:"), 135 | group_name = Style::new().bold().paint(&group.fullName), 136 | whitespace = " ".repeat(11), 137 | ); 138 | } 139 | 140 | if let Some(lines) = log.summary.as_ref().map(|t| t.lines()) { 141 | for line in lines { 142 | println!(" {line}"); 143 | } 144 | } 145 | } 146 | 147 | fn print_warning(msg: &str, indention: usize) { 148 | println!( 149 | "{indention}{msg}", 150 | indention = " ".repeat(indention), 151 | msg = Style::new().bold().fg(Color::Yellow).paint(msg), 152 | ); 153 | } 154 | 155 | fn print_date(day: &NaiveDate, nodes_of_day: &[&ResponseNode]) { 156 | let total = views::to_time_spent_sum(nodes_of_day); 157 | 158 | let day_print = format!("{day}, {}", day.weekday()); 159 | 160 | print!("{} (", Style::new().bold().paint(day_print)); 161 | print_duration(total, Color::Blue); 162 | println!(")"); 163 | 164 | // Sanity checks and print warnings 165 | { 166 | let max_hours_threshold = 10; 167 | if total.as_secs() > max_hours_threshold * 60 * 60 { 168 | // msg is aligned with the suspicious data output 169 | print_warning("^ WARN: More than 10 hours! Is this correct?", 18); 170 | } 171 | 172 | match day.weekday() { 173 | Weekday::Sat | Weekday::Sun => { 174 | // msg is aligned with the suspicious data output 175 | print_warning("^ WARN: You shouldn't work on the weekend, right?", 12); 176 | } 177 | _ => {} 178 | } 179 | } 180 | 181 | for log in nodes_of_day { 182 | print_timelog(log); 183 | } 184 | } 185 | 186 | fn print_week(week: (i32 /* year */, u32 /* iso week */), nodes_of_week: &[&ResponseNode]) { 187 | let week_style = Style::new().bold(); 188 | let week_print = format!("WEEK {}-W{:02}", week.0, week.1); 189 | println!( 190 | "{delim} {week_print} {delim}", 191 | delim = week_style.paint("======================"), 192 | week_print = week_style.paint(week_print) 193 | ); 194 | let total_week_time = views::to_time_spent_sum(nodes_of_week); 195 | print!( 196 | "{total_time_key} ", 197 | total_time_key = Style::new().bold().paint("Total time:") 198 | ); 199 | print_duration(total_week_time, Color::Blue); 200 | println!(); 201 | println!(); 202 | 203 | let nodes_by_day = views::to_nodes_by_day(nodes_of_week); 204 | 205 | for (i, (day, nodes)) in nodes_by_day.iter().enumerate() { 206 | print_date(day, nodes); 207 | 208 | let is_last = i == nodes_by_day.len() - 1; 209 | if !is_last { 210 | println!(); 211 | } 212 | } 213 | } 214 | 215 | fn print_extended_summary(nodes: &[&ResponseNode]) { 216 | { 217 | let nodes_by_epic = views::to_nodes_by_epic(nodes); 218 | for (epic, nodes_of_epic) in nodes_by_epic { 219 | let duration = views::to_time_spent_sum(&nodes_of_epic); 220 | print!(" "); 221 | print_duration(duration, Color::Magenta); 222 | print!( 223 | " - {epic_key} {epic_name}", 224 | epic_key = Style::new().dimmed().paint("Epic:"), 225 | epic_name = Style::new().bold().paint( 226 | epic.as_ref() 227 | .map(|e| e.title.as_str()) 228 | .unwrap_or("") 229 | ) 230 | ); 231 | println!(); 232 | } 233 | } 234 | { 235 | let nodes_by_issue = views::to_nodes_by_issue(nodes); 236 | for (issue, nodes_of_issue) in nodes_by_issue { 237 | let duration = views::to_time_spent_sum(&nodes_of_issue); 238 | print!(" "); 239 | print_duration(duration, Color::Magenta); 240 | print!( 241 | " - Issue: {issue_name}", 242 | issue_name = Style::new().bold().fg(Color::Green).paint(issue.title) 243 | ); 244 | println!(); 245 | } 246 | } 247 | } 248 | 249 | fn print_final_summary(nodes: &[&ResponseNode], cfg: &CliArgs) { 250 | // Print separator. 251 | { 252 | println!(); 253 | // same length as the week separator 254 | println!("{}", "-".repeat(59)); 255 | println!(); 256 | } 257 | 258 | let total_time = views::to_time_spent_sum(nodes); 259 | let all_days = views::to_nodes_by_day(nodes); 260 | 261 | print!( 262 | "{total_time_key} ({days_amount:>2} days with records): ", 263 | total_time_key = Style::new().bold().paint("Total time"), 264 | days_amount = all_days.len(), 265 | ); 266 | print_duration(total_time, Color::Blue); 267 | println!(); 268 | 269 | if cfg.print_extended_summary() { 270 | println!(); 271 | print_extended_summary(nodes); 272 | } 273 | } 274 | 275 | fn print_all_weeks(nodes: &[&ResponseNode], cfg: &CliArgs) { 276 | let view = views::to_nodes_by_week(nodes); 277 | for (i, (week, nodes_of_week)) in view.iter().enumerate() { 278 | print_week((week.year(), week.week()), nodes_of_week); 279 | 280 | let is_last = i == view.len() - 1; 281 | if !is_last { 282 | println!(); 283 | } 284 | } 285 | 286 | print_final_summary(nodes, cfg); 287 | } 288 | 289 | const fn duration_to_hhmm(dur: Duration) -> (u64, u64) { 290 | let hours = dur.as_secs() / 60 / 60; 291 | let remaining_secs = dur.as_secs() - (hours * 60 * 60); 292 | let minutes = remaining_secs / 60; 293 | (hours, minutes) 294 | } 295 | 296 | fn print_duration(duration: Duration, color: Color) { 297 | let (hours, minutes) = duration_to_hhmm(duration); 298 | let print_str = format!("{hours:>2}h {minutes:02}m"); 299 | print!("{}", Style::new().bold().fg(color).paint(print_str)); 300 | } 301 | 302 | #[cfg(test)] 303 | mod tests { 304 | use super::*; 305 | 306 | #[test] 307 | fn test_duration_to_hhmm() { 308 | assert_eq!(duration_to_hhmm(Duration::from_secs(0)), (0, 0)); 309 | assert_eq!(duration_to_hhmm(Duration::from_secs(59)), (0, 0)); 310 | assert_eq!(duration_to_hhmm(Duration::from_secs(60)), (0, 1)); 311 | assert_eq!(duration_to_hhmm(Duration::from_secs(61)), (0, 1)); 312 | assert_eq!(duration_to_hhmm(Duration::from_secs(119)), (0, 1)); 313 | assert_eq!(duration_to_hhmm(Duration::from_secs(120)), (0, 2)); 314 | let h = 3; 315 | let m = 7; 316 | assert_eq!( 317 | duration_to_hhmm(Duration::from_secs(h * 60 * 60 + m * 60)), 318 | (h, m) 319 | ); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/views.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //! Provides transform functions for different views into the data. 26 | 27 | use crate::gitlab_api::types::{Epic, Issue, ResponseNode}; 28 | use chrono::{Datelike, IsoWeek, NaiveDate}; 29 | use std::collections::BTreeMap; 30 | use std::time::Duration; 31 | 32 | fn group_notes_by_filter<'a, T: PartialEq + Ord>( 33 | nodes: &[&'a ResponseNode], 34 | map_fn: impl Fn(&ResponseNode) -> T, 35 | ) -> BTreeMap> { 36 | let items = nodes.iter().map(|node| map_fn(node)).collect::>(); 37 | 38 | let mut map = BTreeMap::new(); 39 | for item in items { 40 | let nodes_of_week = nodes 41 | .iter() 42 | .filter(|node| map_fn(node) == item) 43 | .cloned() 44 | .collect::>(); 45 | 46 | map.entry(item).or_insert(nodes_of_week); 47 | } 48 | map 49 | } 50 | 51 | /// Returns the nodes per [`IsoWeek`]. 52 | pub fn to_nodes_by_week<'a>( 53 | nodes: &[&'a ResponseNode], 54 | ) -> BTreeMap> { 55 | group_notes_by_filter(nodes, |node| node.datetime().iso_week()) 56 | } 57 | 58 | /// Returns the nodes per [`NaiveDate`]. 59 | pub fn to_nodes_by_day<'a>( 60 | nodes: &[&'a ResponseNode], 61 | ) -> BTreeMap> { 62 | group_notes_by_filter(nodes, |node| node.datetime()) 63 | } 64 | 65 | /// Returns the nodes per [`Epic`]. 66 | pub fn to_nodes_by_epic<'a>( 67 | nodes: &[&'a ResponseNode], 68 | ) -> BTreeMap, Vec<&'a ResponseNode>> { 69 | group_notes_by_filter(nodes, |node| node.issue.epic.clone()) 70 | } 71 | 72 | /// Returns the nodes per [`Issue`]. 73 | pub fn to_nodes_by_issue<'a>(nodes: &[&'a ResponseNode]) -> BTreeMap> { 74 | group_notes_by_filter(nodes, |node| node.issue.clone()) 75 | } 76 | 77 | /// Returns the time spent per day. 78 | pub fn to_time_spent_sum(nodes: &[&ResponseNode]) -> Duration { 79 | nodes.iter().map(|node| node.timeSpent().1).sum() 80 | } 81 | --------------------------------------------------------------------------------