├── .envrc ├── .gitignore ├── screenshot.png ├── .github ├── FUNDING.yml ├── workflows │ ├── qa.yml │ ├── dependabot-auto-merge.yml │ └── rust.yml └── dependabot.yml ├── .editorconfig ├── src ├── gitlab-query.graphql ├── views.rs ├── cfg.rs ├── cli.rs ├── fetch.rs ├── gitlab_api.rs └── main.rs ├── nix └── build.nix ├── LICENSE ├── Cargo.toml ├── flake.lock ├── CHANGELOG.md ├── flake.nix ├── README.md └── Cargo.lock /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result* 3 | /.direnv 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/gitlab-timelogs/HEAD/screenshot.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: phip1611 4 | -------------------------------------------------------------------------------- /.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@v6 9 | # Executes "typos ." 10 | - uses: crate-ci/typos@v1.40.0 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | fullPath 18 | } 19 | } 20 | } 21 | pageInfo { 22 | hasPreviousPage 23 | startCursor 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | # We auto-merge all dependabot updates as soon as the CI are met. 2 | # 3 | # More info: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions 4 | 5 | name: Dependabot auto-merge 6 | on: pull_request 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | dependabot: 14 | runs-on: ubuntu-latest 15 | if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'phip1611/gitlab-timelogs' 16 | steps: 17 | - name: Approve PR 18 | run: gh pr review --approve "$PR_URL" 19 | env: 20 | PR_URL: ${{github.event.pull_request.html_url}} 21 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | - name: Enable auto-merge 23 | run: gh pr merge --auto --merge "$PR_URL" 24 | env: 25 | PR_URL: ${{github.event.pull_request.html_url}} 26 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitlab-timelogs" 3 | description = """ 4 | A lightweight CLI to fetch, summarize, and validate your GitLab issue 5 | time logs. Group entries by week, filter by date ranges, and spot anomalies 6 | like weekend work or >10h days. Read-only, fast, and cross-platform - built 7 | to make time tracking in GitLab finally usable. 8 | 9 | Made by developers for developers. gitlab-timelogs is not associated 10 | with the official GitLab project! 11 | """ 12 | version = "0.6.0" 13 | edition = "2024" 14 | rust-version = "1.85.0" 15 | keywords = ["gitlab", "productivity"] 16 | categories = ["command-line-utilities"] 17 | readme = "README.md" 18 | license = "MIT" 19 | homepage = "https://github.com/phip1611/gitlab-timelogs" 20 | repository = "https://github.com/phip1611/gitlab-timelogs" 21 | documentation = "https://docs.rs/gitlab-timelogs" 22 | authors = [ 23 | "Philipp Schuster " 24 | ] 25 | 26 | [profile.release] 27 | opt-level = "s" 28 | lto = true 29 | strip = true 30 | 31 | [dependencies] 32 | anyhow = "~1.0" 33 | chrono = { version = "~0.4", default-features = false, features = ["clock", "std", "serde"] } 34 | nu-ansi-term = "~0.50" 35 | reqwest = { version = "~0.12", features = ["blocking", "json"] } 36 | serde = { version = "~1.0", features = ["derive"] } 37 | serde_json = "~1.0" 38 | toml = "~0.9" 39 | 40 | [dependencies.clap] 41 | version = "~4.5" 42 | features = [ 43 | "color", 44 | "derive", 45 | "env", 46 | "error-context", 47 | "help", 48 | "std", 49 | "suggestions", 50 | "unicode", 51 | "usage", 52 | "wrap_help", 53 | ] 54 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "locked": { 5 | "lastModified": 1754269165, 6 | "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=", 7 | "owner": "ipetkov", 8 | "repo": "crane", 9 | "rev": "444e81206df3f7d92780680e45858e31d2f07a08", 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": 1754487366, 27 | "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", 28 | "owner": "hercules-ci", 29 | "repo": "flake-parts", 30 | "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", 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": 1755268003, 42 | "narHash": "sha256-nNaeJjo861wFR0tjHDyCnHs1rbRtrMgxAKMoig9Sj/w=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "32f313e49e42f715491e1ea7b306a87c16fe0388", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "ref": "nixpkgs-unstable", 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased (Yet) 2 | 3 | ## v0.6.0 (2025-08-16) 4 | 5 | - GraphQL Error Responses from GitLab are now handled and displayer properly 6 | ([#38](https://github.com/phip1611/gitlab-timelogs/issues/38)) 7 | - Updated dependencies 8 | - Use Rust edition 2024 9 | - MSRV is now 1.85 10 | 11 | ## v0.5.0 (2024-12-17) 12 | 13 | - Added basic error reporting. For example, the CLI might now tell you: 14 | - ``` 15 | Error: 16 | Failed to parse response body as JSON 17 | 18 | Caused by: 19 | 0: error decoding response body 20 | 1: missing field `webUrl2` at line 1 column 308 21 | ``` 22 | - ``` 23 | Error: 24 | Failed to receive proper response 25 | 26 | Caused by: 27 | HTTP status client error (401 Unauthorized) for url (https://gitlab.vpn.cyberus-technology.de/api/graphql) 28 | ``` 29 | 30 | ## v0.4.1 (2024-12-17) 31 | 32 | - Better error reporting: it is now clearer if the request failed due to a 33 | wrong or expired token, for example. 34 | 35 | ## v0.4.0 (2024-09-03) 36 | 37 | - Added `-x/--extended-summary` flag to show an extended summary at the end 38 | of the output, where the summarized time per epic and per issue is listed. 39 | - internal code improvements 40 | 41 | ## v0.3.0 (2024-08-26) 42 | 43 | - time span filtering already happens on the server-side, which accelerates 44 | requests by a notable amount. 45 | - updated dependencies 46 | 47 | ## v0.2.2 (2024-07-04) 48 | 49 | - improve handling of default xdg config dir (unix only) 50 | - fix typos 51 | 52 | ## v0.2.1 (2024-07-04) 53 | 54 | - fix documentation bugs 55 | 56 | ## v0.2.0 (2024-07-04) 57 | 58 | - tool now also builds and runs on Windows 59 | - the nix build for Darwin/macOS was fixed 60 | 61 | ## v0.1.1 (2024-07-04) 62 | 63 | - doc & README updates 64 | 65 | ## v0.1.0 (2024-06-27) 66 | 67 | - initial release 68 | -------------------------------------------------------------------------------- /.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@v6 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 --all-targets --all-features 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.85.0, stable, nightly ] 35 | runs-on: ${{ matrix.runs-on }} 36 | steps: 37 | - uses: actions/checkout@v6 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@v6 66 | - uses: cachix/install-nix-action@v31 67 | - run: nix build . 68 | - run: nix run . -- --help 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/nixpkgs-unstable"; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitlab-timelogs CLI 2 | 3 | A lightweight CLI to fetch, summarize, and validate your GitLab issue 4 | time logs. Group entries by week, filter by date ranges, and spot anomalies 5 | like weekend work or >10h days. Read-only, fast, and cross-platform - built 6 | to make time tracking in GitLab finally usable. 7 | 8 | Made by developers for developers. `gitlab-timelogs` **is not** associated 9 | with the official GitLab project! 10 | 11 | ![screenshot.png](screenshot.png) 12 | (_The screenshot is slightly outdated. The latest version shows more information._) 13 | 14 | ## Features 15 | 16 | `gitlab-timelogs` provides you with an overview of your time logs and prints 17 | warnings for typical mistakes. It does not allow you to modify entries, but just 18 | to inspect existing records, so you can fix them in GitLab (if necessary). 19 | 20 | - ✅ collect time logs from issues (timelogs associated with MRs currently not 21 | supported) 22 | - ✅ group them by week 23 | - ✅ specify time range and apply filters (such as group filter) 24 | - ✅ print warnings for common pitfalls: 25 | - accounted less than 15 minutes to an issue (typically a mistake) 26 | - accounted time to a Saturday or Sunday (not common in normal positions) 27 | (at least in Europe 😀) 28 | - accounted more than 10h a day (10h is the legal maximum in Germany) 29 | 30 | ## GitLab Server Support 31 | 32 | Development of this CLI began with GitLab 16.11. Since then, it has been 33 | regularly tested against the latest stable release (currently 18.2). Because it 34 | only relies on basic parts of the GitLab API, it should work across a wide 35 | range of GitLab versions. 36 | 37 | Note: Certain features may be unavailable on the GitLab Free tier. 38 | 39 | ## Supported Platforms 40 | _(For compilation and running.)_ 41 | 42 | `gitlab-timelogs` builds and runs at least on the following platforms: 43 | 44 | - ✅ Linux 45 | - ✅ MacOS 46 | - ✅ Windows 47 | 48 | including different versions and architectures that Rust supports (x86, ARM). 49 | 50 | ## Consume / Install 51 | 52 | **Via cargo:** 53 | 54 | - `$ cargo install https://github.com/phip1611/gitlab-timelogs` 55 | 56 | **Via Nix / on NixOS:** 57 | 58 | - Option A: [via `nixpkgs`](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=gitlab-timelogs) 59 | - A1: Add `pkgs.gitlab-timelogs` to your packages 60 | - A2: Use `nix-shell -p gitlab-timelogs` 61 | - Option B: consume this Flake/Repository 62 | - B1: Add `gitlab-timelogs.nixosModules.default` (`gitlab-timelogs` is 63 | referring to the flake input) to the modules of your NixOS configuration, 64 | which will add `gitlab-timelogs` to your system-wide packages. 65 | - B2: Open a shell: `$ nix shell github:phip1611/gitlab-timelogs` 66 | - B3: Run the tool: `$ nix run github:phip1611/gitlab-timelogs -- ` 67 | 68 | **Via home-manager:** 69 | 70 | 1. import the home-manager module: `gitlab-timelogs.nixosModules.home-manager` 71 | 2. enable and configure gitlab-timelogs: 72 | 73 | ```nix 74 | gitlab-timelogs = { 75 | enable = true; 76 | config = { 77 | gitlabHost = "gitlab.example.com"; 78 | gitlabUsername = "exampleuser"; 79 | # Either write as a string here, or read from a file that you do not push: 80 | gitlabToken = with builtins; readFile (toPath ./gitlab-token.txt); 81 | }; 82 | }; 83 | ``` 84 | 85 | ## Usage 86 | 87 | - `$ gitlab-timelogs --help` 88 | - `$ gitlab-timelogs --host gitlab.vpn.cyberus-technology.de --username pschuster --token ********** --after 2024-06-01 --before 2024-06-30` 89 | 90 | ### Configuration 91 | 92 | 1. Via CLI options. Type `--help` for guidance. 93 | 2. Via environment variables: 94 | - `GITLAB_HOST` 95 | - `GITLAB_USERNAME` 96 | - `GITLAB_TOKEN` 97 | 3. Via a configuration file either in 98 | `~/.config/gitlab-timelogs/config.toml` (UNIX) or \ 99 | `%LOCALAPPDATA%/gitlab-timelogs/config.toml` (Windows) 100 | with the following content: \ 101 | ```toml 102 | gitlab_host = "gitlab.example.com" 103 | gitlab_username = "" 104 | gitlab_token = "" 105 | ``` 106 | 107 | ## MSRV 108 | 109 | The MSRV is Rust stable `1.85.0`. 110 | 111 | ## Trivia 112 | 113 | The main motivation to create this was the unbelievable poor UX of the GitLab 114 | web UI for time logs at that given time. For example, the input mask transformed a `1h 30` to 115 | `3d 7h` instead of `1h 30m`. This common pitfall was unbelievably annoying and 116 | hard to spot - badly influencing a lot of our time records. 117 | 118 | Hence, I created this as part of my work time at [Cyberus Technology GmbH](https://cyberus-technology.de) 119 | to boost our internal productivity. We love open source! Interested in a 120 | cool employer? Contact us! 121 | -------------------------------------------------------------------------------- /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 | /// When specified, restricts results to groups whose full path (e.g., 99 | /// `team-x/project-y`) contains the given value or matches it exactly. 100 | /// 101 | /// The filter is case-sensitive. 102 | #[arg(long)] 103 | filter_group: Option, 104 | } 105 | 106 | impl CliArgs { 107 | pub fn host(&self) -> &str { 108 | &self.gitlab_host 109 | } 110 | 111 | pub fn username(&self) -> &str { 112 | &self.gitlab_username 113 | } 114 | 115 | pub fn token(&self) -> &str { 116 | &self.gitlab_token 117 | } 118 | 119 | pub fn before(&self) -> NaiveDate { 120 | // This is a bit of a hack, because Clap's default_value_t doesn't seem 121 | // to work with clap_serde_derive. *sigh* 122 | self.gitlab_before.unwrap_or_else(current_date) 123 | } 124 | 125 | pub const fn after(&self) -> NaiveDate { 126 | self.gitlab_after 127 | } 128 | 129 | pub const fn print_extended_summary(&self) -> bool { 130 | self.print_extended_summary 131 | } 132 | 133 | pub fn filter_group(&self) -> Option<&str> { 134 | self.filter_group.as_deref() 135 | } 136 | } 137 | 138 | fn current_date() -> NaiveDate { 139 | Local::now().naive_local().date() 140 | } 141 | 142 | /// Returns the previous Monday or today, if today is a Monday. 143 | /// This makes sense as one typically wants to see what one has done in the 144 | /// current week. 145 | fn get_default_after_date() -> NaiveDate { 146 | let now = Local::now(); 147 | let mut day = now; 148 | while day.weekday() != Weekday::Mon { 149 | day = day.sub(TimeDelta::days(1)); 150 | } 151 | day.naive_local().date() 152 | } 153 | -------------------------------------------------------------------------------- /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::{ResponseData, ResponseSerialized}; 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. 47 | /// 48 | /// The data is filtered for the date span to make the request smaller and 49 | /// thus much quicker. Other filters are applied locally after the server 50 | /// response has been fetched. 51 | /// 52 | /// # Parameters 53 | /// - `username`: The exact GitLab username of the user. 54 | /// - `host`: Host name of the GitLab instance without `https://` 55 | /// - `token`: GitLab token to access the GitLab instance. Must have at least 56 | /// READ access. 57 | /// - `before`: Identifier from previous request to get the next page of the 58 | /// paginated result. 59 | /// - `start_date`: Inclusive begin date. 60 | /// - `end_date`: Inclusive end date. 61 | /// - `filter_group`: Optional group filter, such as `Engineering/ProjectA`. 62 | /// If provided, the filter must be contained in the full group name of the 63 | /// entry. 64 | fn fetch_response_data( 65 | username: &str, 66 | host: &str, 67 | token: &str, 68 | before: Option<&str>, 69 | start_date: NaiveDate, 70 | end_date: NaiveDate, 71 | filter_group: Option<&str>, 72 | ) -> anyhow::Result { 73 | let graphql_query = GRAPHQL_TEMPLATE 74 | .replace("%USERNAME%", username) 75 | .replace("%BEFORE%", before.unwrap_or_default()) 76 | // GitLab API ignores the time component and just looks at the 77 | // date and the timezone. 78 | .replace( 79 | "%START_DATE%", 80 | naive_date_to_local_datetime(start_date) 81 | .to_string() 82 | .as_str(), 83 | ) 84 | // GitLab API ignores the time component and just looks at the 85 | // date and the timezone. 86 | .replace( 87 | "%END_DATE%", 88 | naive_date_to_local_datetime(end_date).to_string().as_str(), 89 | ); 90 | let payload = json!({ "query": graphql_query }); 91 | 92 | let authorization = format!("Bearer {token}"); 93 | let url = format!("https://{host}/api/graphql"); 94 | let client = Client::new(); 95 | 96 | let plain_response = client 97 | .post(url) 98 | .header(AUTHORIZATION, authorization) 99 | .json(&payload) 100 | .send() 101 | .context("Failed to send request")? 102 | .error_for_status() 103 | .context("Failed to receive proper response")?; 104 | 105 | let response = plain_response 106 | .json::() 107 | .context("Failed to parse response body as JSON") 108 | .map(|x| x.into_typed()); 109 | 110 | let response_data = response?.into_result()?; 111 | let response_data_filtered = response_data_apply_filters(response_data, filter_group); 112 | Ok(response_data_filtered) 113 | } 114 | 115 | /// Applies local filters onto the response nodes and returns a filtered object. 116 | fn response_data_apply_filters( 117 | mut response: ResponseData, 118 | filter_group: Option<&str>, 119 | ) -> ResponseData /* filtered */ { 120 | // We look for each node if it passes all filters. Otherwise, we remove it. 121 | response.timelogs.nodes.retain(|node| { 122 | // For each filter: we remove not matching items 123 | if let (Some(filter), Some(group)) = (filter_group, &node.project.group) { 124 | // case-sensitive search 125 | let has_group = group.fullName.contains(filter) || group.fullPath.contains(filter); 126 | if !has_group { 127 | return false; 128 | } 129 | } 130 | 131 | // We retain everything that was not filtered out. 132 | true 133 | }); 134 | response 135 | } 136 | 137 | /// Fetches all results from the API with pagination in mind and returns 138 | /// an aggregated single result. 139 | /// 140 | /// # Parameters 141 | /// - `username`: The exact GitLab username of the user. 142 | /// - `host`: Host name of the GitLab instance without `https://` 143 | /// - `token`: GitLab token to access the GitLab instance. Must have at least 144 | /// READ access. 145 | /// - `start_date`: Inclusive begin date. 146 | /// - `end_date`: Inclusive end date. 147 | pub fn fetch_results( 148 | username: &str, 149 | host: &str, 150 | token: &str, 151 | start_date: NaiveDate, 152 | end_date: NaiveDate, 153 | filter_group: Option<&str>, 154 | ) -> anyhow::Result { 155 | let base = fetch_response_data( 156 | username, 157 | host, 158 | token, 159 | None, 160 | start_date, 161 | end_date, 162 | filter_group, 163 | )?; 164 | 165 | let mut aggregated = base; 166 | while aggregated.timelogs.pageInfo.hasPreviousPage { 167 | let cursor = &aggregated 168 | .timelogs 169 | .pageInfo 170 | .startCursor 171 | .expect("Should be valid string at this point"); 172 | 173 | let mut next = fetch_response_data( 174 | username, 175 | host, 176 | token, 177 | Some(cursor), 178 | start_date, 179 | end_date, 180 | filter_group, 181 | )?; 182 | 183 | // Ordering here is not that important, happens later anyway. 184 | next.timelogs.nodes.extend(aggregated.timelogs.nodes); 185 | aggregated = next; 186 | } 187 | Ok(aggregated) 188 | } 189 | -------------------------------------------------------------------------------- /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 fmt::{Debug, Display}; 32 | use serde::Deserialize; 33 | use std::error::Error; 34 | use std::fmt; 35 | use std::time::Duration; 36 | 37 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 38 | pub struct Epic { 39 | pub title: String, 40 | } 41 | 42 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 43 | pub struct Issue { 44 | pub title: String, 45 | /// Full http link to issue. 46 | pub webUrl: String, 47 | pub epic: Option, 48 | } 49 | 50 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 51 | pub struct Group { 52 | pub fullName: String, 53 | pub fullPath: String, 54 | } 55 | 56 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 57 | pub struct Project { 58 | pub group: Option, 59 | } 60 | 61 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 62 | pub struct ResponseNode { 63 | pub spentAt: String, 64 | /// For some totally weird reason, GitLab allows negative times. 65 | /// We recommend just deleting these records. But to support the 66 | /// deserialization, we have to do it like that. 67 | pub timeSpent: i64, 68 | pub summary: Option, 69 | pub issue: Issue, 70 | pub project: Project, 71 | } 72 | 73 | impl ResponseNode { 74 | /// Returns a duration in seconds. 75 | pub const fn timeSpent(&self) -> (bool, Duration) { 76 | let dur = Duration::from_secs(self.timeSpent.unsigned_abs()); 77 | (self.timeSpent.is_positive(), dur) 78 | } 79 | 80 | pub fn epic_name(&self) -> Option<&str> { 81 | self.issue.epic.as_ref().map(|e| e.title.as_str()) 82 | } 83 | 84 | /// Parses the UTC timestring coming from GitLab in the local timezone of 85 | /// the user. This is necessary so that entries accounted to a Monday on 86 | /// `00:00` in CEST are not displayed as Sunday. The value is returned 87 | /// as [`NaiveDate`] but adjusted to the local time. 88 | pub fn datetime(&self) -> NaiveDate { 89 | let date = DateTime::parse_from_rfc3339(&self.spentAt).unwrap(); 90 | let datetime = DateTime::::from(date); 91 | datetime.naive_local().date() 92 | } 93 | } 94 | 95 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 96 | pub struct ResponsePageInfo { 97 | pub hasPreviousPage: bool, 98 | pub startCursor: Option, 99 | } 100 | 101 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 102 | pub struct ResponseTimelogs { 103 | pub nodes: Vec, 104 | pub pageInfo: ResponsePageInfo, 105 | } 106 | 107 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 108 | pub struct ResponseData { 109 | pub timelogs: ResponseTimelogs, 110 | } 111 | 112 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 113 | pub struct GraphQLErrorLocation { 114 | line: u64, 115 | column: u64, 116 | } 117 | 118 | impl Display for GraphQLErrorLocation { 119 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 120 | write!(f, "line {}, column {}", self.line, self.column) 121 | } 122 | } 123 | 124 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 125 | pub struct GraphQLErrorResponse { 126 | pub message: String, 127 | pub locations: Vec, 128 | } 129 | 130 | impl Display for GraphQLErrorResponse { 131 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 132 | write!(f, "{}", self.message)?; 133 | if !self.locations.is_empty() { 134 | let locs: Vec = self.locations.iter().map(|loc| loc.to_string()).collect(); 135 | write!(f, " (at {})", locs.join(", "))?; 136 | } 137 | Ok(()) 138 | } 139 | } 140 | 141 | impl Error for GraphQLErrorResponse {} 142 | 143 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 144 | pub struct GraphQLErrorsResponse(pub Vec); 145 | 146 | impl Display for GraphQLErrorsResponse { 147 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 148 | if self.0.is_empty() { 149 | write!(f, "No GraphQL errors") 150 | } else if self.0.len() == 1 { 151 | write!(f, "GraphQL error: {}", self.0[0]) 152 | } else { 153 | writeln!(f, "{} GraphQL errors:", self.0.len())?; 154 | for (i, err) in self.0.iter().enumerate() { 155 | writeln!(f, " {}. {}", i + 1, err)?; 156 | } 157 | Ok(()) 158 | } 159 | } 160 | } 161 | 162 | impl Error for GraphQLErrorsResponse {} 163 | 164 | /// The serialized/typed GraphQL response from the GitLab API with all 165 | /// timelogs for the given time frame. 166 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 167 | pub enum Response { 168 | PayloadResponse(ResponseData), 169 | ErrorResponse(GraphQLErrorsResponse), 170 | } 171 | 172 | impl Response { 173 | /// Transforms the GraphQL response to a Rust [`Result`]. 174 | pub fn into_result(self) -> Result { 175 | match self { 176 | Self::PayloadResponse(payload) => Ok(payload), 177 | Self::ErrorResponse(errors) => Err(errors), 178 | } 179 | } 180 | } 181 | 182 | /// The serialized/typed GraphQL response from the GitLab API with all 183 | /// timelogs for the given time frame. 184 | #[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] 185 | pub struct ResponseSerialized { 186 | pub data: Option, 187 | pub errors: Option, 188 | } 189 | 190 | impl ResponseSerialized { 191 | /// Transforms the GraphQL response to a Rust [`Result`]. 192 | pub fn into_typed(self) -> Response { 193 | match self { 194 | Self { 195 | data: Some(data), 196 | errors: None, 197 | } => Response::PayloadResponse(data), 198 | Self { 199 | data: None, 200 | errors: Some(errors), 201 | } => Response::ErrorResponse(errors), 202 | _ => panic!( 203 | "Unexpected response: data={:#?}, errors={:#?}", 204 | self.data, self.errors 205 | ), 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /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::{Context, anyhow}; 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 | cfg.filter_group(), 76 | )?; 77 | 78 | println!("Host : {}", cfg.host()); 79 | println!("Username : {}", cfg.username()); 80 | println!("Time Span: {} - {}", cfg.after(), cfg.before()); 81 | 82 | // All nodes but as vector to references. 83 | // Simplifies the handling with other parts of the code, especially the 84 | // `views` module. 85 | let nodes = response.timelogs.nodes.iter().collect::>(); 86 | 87 | if nodes.is_empty() { 88 | print_warning( 89 | "Empty response. Is the username correct? Does the token have read permission?", 90 | 0, 91 | ); 92 | } else { 93 | print_all_weeks(nodes.as_slice(), &cfg); 94 | } 95 | 96 | Ok(()) 97 | } 98 | 99 | fn print_timelog(log: &ResponseNode) { 100 | let (duration_is_positive, duration) = log.timeSpent(); 101 | print!(" "); 102 | print_duration(duration, Color::Magenta); 103 | println!( 104 | " {issue_name}", 105 | issue_name = Style::new() 106 | .bold() 107 | .fg(Color::Green) 108 | .paint(log.issue.title.clone()), 109 | ); 110 | let min_minutes_threshold = 15; 111 | if !duration_is_positive { 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 | print_warning("^ WARN: Less than 15 minutes! Is this correct?", 6); 119 | } 120 | 121 | // Print issue metadata. 122 | let epic_name = log.epic_name().unwrap_or(""); 123 | let whitespace = " ".repeat(11); 124 | println!( 125 | "{whitespace}{link}", 126 | link = Style::new().dimmed().paint(&log.issue.webUrl) 127 | ); 128 | if let Some(group) = &log.project.group { 129 | println!( 130 | "{whitespace}[{epic_key} {epic_name}, {group_key} {group_name}]", 131 | epic_key = Style::new().dimmed().paint("Epic:"), 132 | epic_name = Style::new().bold().paint(epic_name), 133 | group_key = Style::new().dimmed().paint("Group:"), 134 | group_name = Style::new().bold().paint(&group.fullName), 135 | whitespace = " ".repeat(11), 136 | ); 137 | } 138 | 139 | if let Some(lines) = log.summary.as_ref().map(|t| t.lines()) { 140 | for line in lines { 141 | println!(" {line}"); 142 | } 143 | } 144 | } 145 | 146 | fn print_warning(msg: &str, indention: usize) { 147 | println!( 148 | "{indention}{msg}", 149 | indention = " ".repeat(indention), 150 | msg = Style::new().bold().fg(Color::Yellow).paint(msg), 151 | ); 152 | } 153 | 154 | fn print_date(day: &NaiveDate, nodes_of_day: &[&ResponseNode]) { 155 | let total = views::to_time_spent_sum(nodes_of_day); 156 | 157 | let day_print = format!("{day}, {}", day.weekday()); 158 | 159 | print!("{} (", Style::new().bold().paint(day_print)); 160 | print_duration(total, Color::Blue); 161 | println!(")"); 162 | 163 | // Sanity checks and print warnings 164 | { 165 | let max_hours_threshold = 10; 166 | if total.as_secs() > max_hours_threshold * 60 * 60 { 167 | print_warning("^ WARN: More than 10 hours! Is this correct?", 18); 168 | } 169 | 170 | match day.weekday() { 171 | Weekday::Sat | Weekday::Sun => { 172 | print_warning("^ WARN: You shouldn't work on the weekend, right?", 12); 173 | } 174 | _ => {} 175 | } 176 | } 177 | 178 | for log in nodes_of_day { 179 | print_timelog(log); 180 | } 181 | } 182 | 183 | fn print_week(week: (i32 /* year */, u32 /* iso week */), nodes_of_week: &[&ResponseNode]) { 184 | let week_style = Style::new().bold(); 185 | let week_print = format!("WEEK {}-W{:02}", week.0, week.1); 186 | println!( 187 | "{delim} {week_print} {delim}", 188 | delim = week_style.paint("======================"), 189 | week_print = week_style.paint(week_print) 190 | ); 191 | let total_week_time = views::to_time_spent_sum(nodes_of_week); 192 | print!( 193 | "{total_time_key} ", 194 | total_time_key = Style::new().bold().paint("Total time:") 195 | ); 196 | print_duration(total_week_time, Color::Blue); 197 | println!(); 198 | println!(); 199 | 200 | let nodes_by_day = views::to_nodes_by_day(nodes_of_week); 201 | 202 | for (i, (day, nodes)) in nodes_by_day.iter().enumerate() { 203 | print_date(day, nodes); 204 | 205 | let is_last = i == nodes_by_day.len() - 1; 206 | if !is_last { 207 | println!(); 208 | } 209 | } 210 | } 211 | 212 | fn print_extended_summary(nodes: &[&ResponseNode]) { 213 | { 214 | let nodes_by_epic = views::to_nodes_by_epic(nodes); 215 | for (epic, nodes_of_epic) in nodes_by_epic { 216 | let duration = views::to_time_spent_sum(&nodes_of_epic); 217 | print!(" "); 218 | print_duration(duration, Color::Magenta); 219 | print!( 220 | " - {epic_key} {epic_name}", 221 | epic_key = Style::new().dimmed().paint("Epic:"), 222 | epic_name = Style::new().bold().paint( 223 | epic.as_ref() 224 | .map(|e| e.title.as_str()) 225 | .unwrap_or("") 226 | ) 227 | ); 228 | println!(); 229 | } 230 | } 231 | { 232 | let nodes_by_issue = views::to_nodes_by_issue(nodes); 233 | for (issue, nodes_of_issue) in nodes_by_issue { 234 | let duration = views::to_time_spent_sum(&nodes_of_issue); 235 | print!(" "); 236 | print_duration(duration, Color::Magenta); 237 | print!( 238 | " - Issue: {issue_name}", 239 | issue_name = Style::new().bold().fg(Color::Green).paint(issue.title) 240 | ); 241 | println!(); 242 | } 243 | } 244 | } 245 | 246 | fn print_final_summary(nodes: &[&ResponseNode], cfg: &CliArgs) { 247 | // Print separator. 248 | { 249 | println!(); 250 | // same length as the week separator 251 | println!("{}", "-".repeat(59)); 252 | println!(); 253 | } 254 | 255 | let total_time = views::to_time_spent_sum(nodes); 256 | let all_days = views::to_nodes_by_day(nodes); 257 | 258 | print!( 259 | "{total_time_key} ({days_amount:>2} days with records): ", 260 | total_time_key = Style::new().bold().paint("Total time"), 261 | days_amount = all_days.len(), 262 | ); 263 | print_duration(total_time, Color::Blue); 264 | println!(); 265 | 266 | if cfg.print_extended_summary() { 267 | println!(); 268 | print_extended_summary(nodes); 269 | } 270 | } 271 | 272 | fn print_all_weeks(nodes: &[&ResponseNode], cfg: &CliArgs) { 273 | let view = views::to_nodes_by_week(nodes); 274 | for (i, (week, nodes_of_week)) in view.iter().enumerate() { 275 | print_week((week.year(), week.week()), nodes_of_week); 276 | 277 | let is_last = i == view.len() - 1; 278 | if !is_last { 279 | println!(); 280 | } 281 | } 282 | 283 | print_final_summary(nodes, cfg); 284 | } 285 | 286 | const fn duration_to_hhmm(dur: Duration) -> (u64, u64) { 287 | let hours = dur.as_secs() / 60 / 60; 288 | let remaining_secs = dur.as_secs() - (hours * 60 * 60); 289 | let minutes = remaining_secs / 60; 290 | (hours, minutes) 291 | } 292 | 293 | fn print_duration(duration: Duration, color: Color) { 294 | let (hours, minutes) = duration_to_hhmm(duration); 295 | let print_str = format!("{hours:>2}h {minutes:02}m"); 296 | print!("{}", Style::new().bold().fg(color).paint(print_str)); 297 | } 298 | 299 | #[cfg(test)] 300 | mod tests { 301 | use super::*; 302 | 303 | #[test] 304 | fn test_duration_to_hhmm() { 305 | assert_eq!(duration_to_hhmm(Duration::from_secs(0)), (0, 0)); 306 | assert_eq!(duration_to_hhmm(Duration::from_secs(59)), (0, 0)); 307 | assert_eq!(duration_to_hhmm(Duration::from_secs(60)), (0, 1)); 308 | assert_eq!(duration_to_hhmm(Duration::from_secs(61)), (0, 1)); 309 | assert_eq!(duration_to_hhmm(Duration::from_secs(119)), (0, 1)); 310 | assert_eq!(duration_to_hhmm(Duration::from_secs(120)), (0, 2)); 311 | let h = 3; 312 | let m = 7; 313 | assert_eq!( 314 | duration_to_hhmm(Duration::from_secs(h * 60 * 60 + m * 60)), 315 | (h, m) 316 | ); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 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.20" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 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.11" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.7" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 70 | dependencies = [ 71 | "windows-sys 0.60.2", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.10" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 79 | dependencies = [ 80 | "anstyle", 81 | "once_cell_polyfill", 82 | "windows-sys 0.60.2", 83 | ] 84 | 85 | [[package]] 86 | name = "anyhow" 87 | version = "1.0.99" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 90 | 91 | [[package]] 92 | name = "atomic-waker" 93 | version = "1.1.2" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 96 | 97 | [[package]] 98 | name = "autocfg" 99 | version = "1.5.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 102 | 103 | [[package]] 104 | name = "backtrace" 105 | version = "0.3.75" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 108 | dependencies = [ 109 | "addr2line", 110 | "cfg-if", 111 | "libc", 112 | "miniz_oxide", 113 | "object", 114 | "rustc-demangle", 115 | "windows-targets 0.52.6", 116 | ] 117 | 118 | [[package]] 119 | name = "base64" 120 | version = "0.22.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 123 | 124 | [[package]] 125 | name = "bitflags" 126 | version = "2.9.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 129 | 130 | [[package]] 131 | name = "bumpalo" 132 | version = "3.19.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 135 | 136 | [[package]] 137 | name = "bytes" 138 | version = "1.10.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 141 | 142 | [[package]] 143 | name = "cc" 144 | version = "1.2.33" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" 147 | dependencies = [ 148 | "shlex", 149 | ] 150 | 151 | [[package]] 152 | name = "cfg-if" 153 | version = "1.0.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 156 | 157 | [[package]] 158 | name = "chrono" 159 | version = "0.4.41" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 162 | dependencies = [ 163 | "android-tzdata", 164 | "iana-time-zone", 165 | "num-traits", 166 | "serde", 167 | "windows-link", 168 | ] 169 | 170 | [[package]] 171 | name = "clap" 172 | version = "4.5.45" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" 175 | dependencies = [ 176 | "clap_builder", 177 | "clap_derive", 178 | ] 179 | 180 | [[package]] 181 | name = "clap_builder" 182 | version = "4.5.44" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" 185 | dependencies = [ 186 | "anstream", 187 | "anstyle", 188 | "clap_lex", 189 | "strsim", 190 | "terminal_size", 191 | "unicase", 192 | "unicode-width", 193 | ] 194 | 195 | [[package]] 196 | name = "clap_derive" 197 | version = "4.5.45" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" 200 | dependencies = [ 201 | "heck", 202 | "proc-macro2", 203 | "quote", 204 | "syn", 205 | ] 206 | 207 | [[package]] 208 | name = "clap_lex" 209 | version = "0.7.5" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 212 | 213 | [[package]] 214 | name = "colorchoice" 215 | version = "1.0.4" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 218 | 219 | [[package]] 220 | name = "core-foundation" 221 | version = "0.9.4" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 224 | dependencies = [ 225 | "core-foundation-sys", 226 | "libc", 227 | ] 228 | 229 | [[package]] 230 | name = "core-foundation-sys" 231 | version = "0.8.7" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 234 | 235 | [[package]] 236 | name = "displaydoc" 237 | version = "0.2.5" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 240 | dependencies = [ 241 | "proc-macro2", 242 | "quote", 243 | "syn", 244 | ] 245 | 246 | [[package]] 247 | name = "encoding_rs" 248 | version = "0.8.35" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 251 | dependencies = [ 252 | "cfg-if", 253 | ] 254 | 255 | [[package]] 256 | name = "equivalent" 257 | version = "1.0.2" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 260 | 261 | [[package]] 262 | name = "errno" 263 | version = "0.3.13" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 266 | dependencies = [ 267 | "libc", 268 | "windows-sys 0.60.2", 269 | ] 270 | 271 | [[package]] 272 | name = "fastrand" 273 | version = "2.3.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 276 | 277 | [[package]] 278 | name = "fnv" 279 | version = "1.0.7" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 282 | 283 | [[package]] 284 | name = "foreign-types" 285 | version = "0.3.2" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 288 | dependencies = [ 289 | "foreign-types-shared", 290 | ] 291 | 292 | [[package]] 293 | name = "foreign-types-shared" 294 | version = "0.1.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 297 | 298 | [[package]] 299 | name = "form_urlencoded" 300 | version = "1.2.1" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 303 | dependencies = [ 304 | "percent-encoding", 305 | ] 306 | 307 | [[package]] 308 | name = "futures-channel" 309 | version = "0.3.31" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 312 | dependencies = [ 313 | "futures-core", 314 | "futures-sink", 315 | ] 316 | 317 | [[package]] 318 | name = "futures-core" 319 | version = "0.3.31" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 322 | 323 | [[package]] 324 | name = "futures-io" 325 | version = "0.3.31" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 328 | 329 | [[package]] 330 | name = "futures-sink" 331 | version = "0.3.31" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 334 | 335 | [[package]] 336 | name = "futures-task" 337 | version = "0.3.31" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 340 | 341 | [[package]] 342 | name = "futures-util" 343 | version = "0.3.31" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 346 | dependencies = [ 347 | "futures-core", 348 | "futures-io", 349 | "futures-sink", 350 | "futures-task", 351 | "memchr", 352 | "pin-project-lite", 353 | "pin-utils", 354 | "slab", 355 | ] 356 | 357 | [[package]] 358 | name = "getrandom" 359 | version = "0.2.16" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 362 | dependencies = [ 363 | "cfg-if", 364 | "libc", 365 | "wasi 0.11.1+wasi-snapshot-preview1", 366 | ] 367 | 368 | [[package]] 369 | name = "getrandom" 370 | version = "0.3.3" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 373 | dependencies = [ 374 | "cfg-if", 375 | "libc", 376 | "r-efi", 377 | "wasi 0.14.2+wasi-0.2.4", 378 | ] 379 | 380 | [[package]] 381 | name = "gimli" 382 | version = "0.31.1" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 385 | 386 | [[package]] 387 | name = "gitlab-timelogs" 388 | version = "0.6.0" 389 | dependencies = [ 390 | "anyhow", 391 | "chrono", 392 | "clap", 393 | "nu-ansi-term", 394 | "reqwest", 395 | "serde", 396 | "serde_json", 397 | "toml", 398 | ] 399 | 400 | [[package]] 401 | name = "h2" 402 | version = "0.4.12" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 405 | dependencies = [ 406 | "atomic-waker", 407 | "bytes", 408 | "fnv", 409 | "futures-core", 410 | "futures-sink", 411 | "http", 412 | "indexmap", 413 | "slab", 414 | "tokio", 415 | "tokio-util", 416 | "tracing", 417 | ] 418 | 419 | [[package]] 420 | name = "hashbrown" 421 | version = "0.15.5" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 424 | 425 | [[package]] 426 | name = "heck" 427 | version = "0.5.0" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 430 | 431 | [[package]] 432 | name = "http" 433 | version = "1.3.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 436 | dependencies = [ 437 | "bytes", 438 | "fnv", 439 | "itoa", 440 | ] 441 | 442 | [[package]] 443 | name = "http-body" 444 | version = "1.0.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 447 | dependencies = [ 448 | "bytes", 449 | "http", 450 | ] 451 | 452 | [[package]] 453 | name = "http-body-util" 454 | version = "0.1.3" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 457 | dependencies = [ 458 | "bytes", 459 | "futures-core", 460 | "http", 461 | "http-body", 462 | "pin-project-lite", 463 | ] 464 | 465 | [[package]] 466 | name = "httparse" 467 | version = "1.10.1" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 470 | 471 | [[package]] 472 | name = "hyper" 473 | version = "1.6.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 476 | dependencies = [ 477 | "bytes", 478 | "futures-channel", 479 | "futures-util", 480 | "h2", 481 | "http", 482 | "http-body", 483 | "httparse", 484 | "itoa", 485 | "pin-project-lite", 486 | "smallvec", 487 | "tokio", 488 | "want", 489 | ] 490 | 491 | [[package]] 492 | name = "hyper-rustls" 493 | version = "0.27.7" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 496 | dependencies = [ 497 | "http", 498 | "hyper", 499 | "hyper-util", 500 | "rustls", 501 | "rustls-pki-types", 502 | "tokio", 503 | "tokio-rustls", 504 | "tower-service", 505 | ] 506 | 507 | [[package]] 508 | name = "hyper-tls" 509 | version = "0.6.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 512 | dependencies = [ 513 | "bytes", 514 | "http-body-util", 515 | "hyper", 516 | "hyper-util", 517 | "native-tls", 518 | "tokio", 519 | "tokio-native-tls", 520 | "tower-service", 521 | ] 522 | 523 | [[package]] 524 | name = "hyper-util" 525 | version = "0.1.16" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 528 | dependencies = [ 529 | "base64", 530 | "bytes", 531 | "futures-channel", 532 | "futures-core", 533 | "futures-util", 534 | "http", 535 | "http-body", 536 | "hyper", 537 | "ipnet", 538 | "libc", 539 | "percent-encoding", 540 | "pin-project-lite", 541 | "socket2", 542 | "system-configuration", 543 | "tokio", 544 | "tower-service", 545 | "tracing", 546 | "windows-registry", 547 | ] 548 | 549 | [[package]] 550 | name = "iana-time-zone" 551 | version = "0.1.63" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 554 | dependencies = [ 555 | "android_system_properties", 556 | "core-foundation-sys", 557 | "iana-time-zone-haiku", 558 | "js-sys", 559 | "log", 560 | "wasm-bindgen", 561 | "windows-core", 562 | ] 563 | 564 | [[package]] 565 | name = "iana-time-zone-haiku" 566 | version = "0.1.2" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 569 | dependencies = [ 570 | "cc", 571 | ] 572 | 573 | [[package]] 574 | name = "icu_collections" 575 | version = "2.0.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 578 | dependencies = [ 579 | "displaydoc", 580 | "potential_utf", 581 | "yoke", 582 | "zerofrom", 583 | "zerovec", 584 | ] 585 | 586 | [[package]] 587 | name = "icu_locale_core" 588 | version = "2.0.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 591 | dependencies = [ 592 | "displaydoc", 593 | "litemap", 594 | "tinystr", 595 | "writeable", 596 | "zerovec", 597 | ] 598 | 599 | [[package]] 600 | name = "icu_normalizer" 601 | version = "2.0.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 604 | dependencies = [ 605 | "displaydoc", 606 | "icu_collections", 607 | "icu_normalizer_data", 608 | "icu_properties", 609 | "icu_provider", 610 | "smallvec", 611 | "zerovec", 612 | ] 613 | 614 | [[package]] 615 | name = "icu_normalizer_data" 616 | version = "2.0.0" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 619 | 620 | [[package]] 621 | name = "icu_properties" 622 | version = "2.0.1" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 625 | dependencies = [ 626 | "displaydoc", 627 | "icu_collections", 628 | "icu_locale_core", 629 | "icu_properties_data", 630 | "icu_provider", 631 | "potential_utf", 632 | "zerotrie", 633 | "zerovec", 634 | ] 635 | 636 | [[package]] 637 | name = "icu_properties_data" 638 | version = "2.0.1" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 641 | 642 | [[package]] 643 | name = "icu_provider" 644 | version = "2.0.0" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 647 | dependencies = [ 648 | "displaydoc", 649 | "icu_locale_core", 650 | "stable_deref_trait", 651 | "tinystr", 652 | "writeable", 653 | "yoke", 654 | "zerofrom", 655 | "zerotrie", 656 | "zerovec", 657 | ] 658 | 659 | [[package]] 660 | name = "idna" 661 | version = "1.0.3" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 664 | dependencies = [ 665 | "idna_adapter", 666 | "smallvec", 667 | "utf8_iter", 668 | ] 669 | 670 | [[package]] 671 | name = "idna_adapter" 672 | version = "1.2.1" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 675 | dependencies = [ 676 | "icu_normalizer", 677 | "icu_properties", 678 | ] 679 | 680 | [[package]] 681 | name = "indexmap" 682 | version = "2.10.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 685 | dependencies = [ 686 | "equivalent", 687 | "hashbrown", 688 | ] 689 | 690 | [[package]] 691 | name = "io-uring" 692 | version = "0.7.9" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" 695 | dependencies = [ 696 | "bitflags", 697 | "cfg-if", 698 | "libc", 699 | ] 700 | 701 | [[package]] 702 | name = "ipnet" 703 | version = "2.11.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 706 | 707 | [[package]] 708 | name = "iri-string" 709 | version = "0.7.8" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 712 | dependencies = [ 713 | "memchr", 714 | "serde", 715 | ] 716 | 717 | [[package]] 718 | name = "is_terminal_polyfill" 719 | version = "1.70.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 722 | 723 | [[package]] 724 | name = "itoa" 725 | version = "1.0.15" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 728 | 729 | [[package]] 730 | name = "js-sys" 731 | version = "0.3.77" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 734 | dependencies = [ 735 | "once_cell", 736 | "wasm-bindgen", 737 | ] 738 | 739 | [[package]] 740 | name = "libc" 741 | version = "0.2.175" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 744 | 745 | [[package]] 746 | name = "linux-raw-sys" 747 | version = "0.9.4" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 750 | 751 | [[package]] 752 | name = "litemap" 753 | version = "0.8.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 756 | 757 | [[package]] 758 | name = "log" 759 | version = "0.4.27" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 762 | 763 | [[package]] 764 | name = "memchr" 765 | version = "2.7.5" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 768 | 769 | [[package]] 770 | name = "mime" 771 | version = "0.3.17" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 774 | 775 | [[package]] 776 | name = "miniz_oxide" 777 | version = "0.8.9" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 780 | dependencies = [ 781 | "adler2", 782 | ] 783 | 784 | [[package]] 785 | name = "mio" 786 | version = "1.0.4" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 789 | dependencies = [ 790 | "libc", 791 | "wasi 0.11.1+wasi-snapshot-preview1", 792 | "windows-sys 0.59.0", 793 | ] 794 | 795 | [[package]] 796 | name = "native-tls" 797 | version = "0.2.14" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 800 | dependencies = [ 801 | "libc", 802 | "log", 803 | "openssl", 804 | "openssl-probe", 805 | "openssl-sys", 806 | "schannel", 807 | "security-framework", 808 | "security-framework-sys", 809 | "tempfile", 810 | ] 811 | 812 | [[package]] 813 | name = "nu-ansi-term" 814 | version = "0.50.1" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" 817 | dependencies = [ 818 | "windows-sys 0.52.0", 819 | ] 820 | 821 | [[package]] 822 | name = "num-traits" 823 | version = "0.2.19" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 826 | dependencies = [ 827 | "autocfg", 828 | ] 829 | 830 | [[package]] 831 | name = "object" 832 | version = "0.36.7" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 835 | dependencies = [ 836 | "memchr", 837 | ] 838 | 839 | [[package]] 840 | name = "once_cell" 841 | version = "1.21.3" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 844 | 845 | [[package]] 846 | name = "once_cell_polyfill" 847 | version = "1.70.1" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 850 | 851 | [[package]] 852 | name = "openssl" 853 | version = "0.10.73" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 856 | dependencies = [ 857 | "bitflags", 858 | "cfg-if", 859 | "foreign-types", 860 | "libc", 861 | "once_cell", 862 | "openssl-macros", 863 | "openssl-sys", 864 | ] 865 | 866 | [[package]] 867 | name = "openssl-macros" 868 | version = "0.1.1" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 871 | dependencies = [ 872 | "proc-macro2", 873 | "quote", 874 | "syn", 875 | ] 876 | 877 | [[package]] 878 | name = "openssl-probe" 879 | version = "0.1.6" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 882 | 883 | [[package]] 884 | name = "openssl-sys" 885 | version = "0.9.109" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 888 | dependencies = [ 889 | "cc", 890 | "libc", 891 | "pkg-config", 892 | "vcpkg", 893 | ] 894 | 895 | [[package]] 896 | name = "percent-encoding" 897 | version = "2.3.1" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 900 | 901 | [[package]] 902 | name = "pin-project-lite" 903 | version = "0.2.16" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 906 | 907 | [[package]] 908 | name = "pin-utils" 909 | version = "0.1.0" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 912 | 913 | [[package]] 914 | name = "pkg-config" 915 | version = "0.3.32" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 918 | 919 | [[package]] 920 | name = "potential_utf" 921 | version = "0.1.2" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 924 | dependencies = [ 925 | "zerovec", 926 | ] 927 | 928 | [[package]] 929 | name = "proc-macro2" 930 | version = "1.0.97" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" 933 | dependencies = [ 934 | "unicode-ident", 935 | ] 936 | 937 | [[package]] 938 | name = "quote" 939 | version = "1.0.40" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 942 | dependencies = [ 943 | "proc-macro2", 944 | ] 945 | 946 | [[package]] 947 | name = "r-efi" 948 | version = "5.3.0" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 951 | 952 | [[package]] 953 | name = "reqwest" 954 | version = "0.12.23" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" 957 | dependencies = [ 958 | "base64", 959 | "bytes", 960 | "encoding_rs", 961 | "futures-channel", 962 | "futures-core", 963 | "futures-util", 964 | "h2", 965 | "http", 966 | "http-body", 967 | "http-body-util", 968 | "hyper", 969 | "hyper-rustls", 970 | "hyper-tls", 971 | "hyper-util", 972 | "js-sys", 973 | "log", 974 | "mime", 975 | "native-tls", 976 | "percent-encoding", 977 | "pin-project-lite", 978 | "rustls-pki-types", 979 | "serde", 980 | "serde_json", 981 | "serde_urlencoded", 982 | "sync_wrapper", 983 | "tokio", 984 | "tokio-native-tls", 985 | "tower", 986 | "tower-http", 987 | "tower-service", 988 | "url", 989 | "wasm-bindgen", 990 | "wasm-bindgen-futures", 991 | "web-sys", 992 | ] 993 | 994 | [[package]] 995 | name = "ring" 996 | version = "0.17.14" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 999 | dependencies = [ 1000 | "cc", 1001 | "cfg-if", 1002 | "getrandom 0.2.16", 1003 | "libc", 1004 | "untrusted", 1005 | "windows-sys 0.52.0", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "rustc-demangle" 1010 | version = "0.1.26" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 1013 | 1014 | [[package]] 1015 | name = "rustix" 1016 | version = "1.0.8" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 1019 | dependencies = [ 1020 | "bitflags", 1021 | "errno", 1022 | "libc", 1023 | "linux-raw-sys", 1024 | "windows-sys 0.60.2", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "rustls" 1029 | version = "0.23.31" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 1032 | dependencies = [ 1033 | "once_cell", 1034 | "rustls-pki-types", 1035 | "rustls-webpki", 1036 | "subtle", 1037 | "zeroize", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "rustls-pki-types" 1042 | version = "1.12.0" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1045 | dependencies = [ 1046 | "zeroize", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "rustls-webpki" 1051 | version = "0.103.4" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 1054 | dependencies = [ 1055 | "ring", 1056 | "rustls-pki-types", 1057 | "untrusted", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "rustversion" 1062 | version = "1.0.22" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1065 | 1066 | [[package]] 1067 | name = "ryu" 1068 | version = "1.0.20" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1071 | 1072 | [[package]] 1073 | name = "schannel" 1074 | version = "0.1.27" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1077 | dependencies = [ 1078 | "windows-sys 0.59.0", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "security-framework" 1083 | version = "2.11.1" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1086 | dependencies = [ 1087 | "bitflags", 1088 | "core-foundation", 1089 | "core-foundation-sys", 1090 | "libc", 1091 | "security-framework-sys", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "security-framework-sys" 1096 | version = "2.14.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1099 | dependencies = [ 1100 | "core-foundation-sys", 1101 | "libc", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "serde" 1106 | version = "1.0.219" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1109 | dependencies = [ 1110 | "serde_derive", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "serde_derive" 1115 | version = "1.0.219" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1118 | dependencies = [ 1119 | "proc-macro2", 1120 | "quote", 1121 | "syn", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "serde_json" 1126 | version = "1.0.142" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" 1129 | dependencies = [ 1130 | "itoa", 1131 | "memchr", 1132 | "ryu", 1133 | "serde", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "serde_spanned" 1138 | version = "1.0.0" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" 1141 | dependencies = [ 1142 | "serde", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "serde_urlencoded" 1147 | version = "0.7.1" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1150 | dependencies = [ 1151 | "form_urlencoded", 1152 | "itoa", 1153 | "ryu", 1154 | "serde", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "shlex" 1159 | version = "1.3.0" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1162 | 1163 | [[package]] 1164 | name = "slab" 1165 | version = "0.4.11" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1168 | 1169 | [[package]] 1170 | name = "smallvec" 1171 | version = "1.15.1" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1174 | 1175 | [[package]] 1176 | name = "socket2" 1177 | version = "0.6.0" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 1180 | dependencies = [ 1181 | "libc", 1182 | "windows-sys 0.59.0", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "stable_deref_trait" 1187 | version = "1.2.0" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1190 | 1191 | [[package]] 1192 | name = "strsim" 1193 | version = "0.11.1" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1196 | 1197 | [[package]] 1198 | name = "subtle" 1199 | version = "2.6.1" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1202 | 1203 | [[package]] 1204 | name = "syn" 1205 | version = "2.0.106" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 1208 | dependencies = [ 1209 | "proc-macro2", 1210 | "quote", 1211 | "unicode-ident", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "sync_wrapper" 1216 | version = "1.0.2" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1219 | dependencies = [ 1220 | "futures-core", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "synstructure" 1225 | version = "0.13.2" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1228 | dependencies = [ 1229 | "proc-macro2", 1230 | "quote", 1231 | "syn", 1232 | ] 1233 | 1234 | [[package]] 1235 | name = "system-configuration" 1236 | version = "0.6.1" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1239 | dependencies = [ 1240 | "bitflags", 1241 | "core-foundation", 1242 | "system-configuration-sys", 1243 | ] 1244 | 1245 | [[package]] 1246 | name = "system-configuration-sys" 1247 | version = "0.6.0" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1250 | dependencies = [ 1251 | "core-foundation-sys", 1252 | "libc", 1253 | ] 1254 | 1255 | [[package]] 1256 | name = "tempfile" 1257 | version = "3.20.0" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 1260 | dependencies = [ 1261 | "fastrand", 1262 | "getrandom 0.3.3", 1263 | "once_cell", 1264 | "rustix", 1265 | "windows-sys 0.59.0", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "terminal_size" 1270 | version = "0.4.3" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 1273 | dependencies = [ 1274 | "rustix", 1275 | "windows-sys 0.60.2", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "tinystr" 1280 | version = "0.8.1" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1283 | dependencies = [ 1284 | "displaydoc", 1285 | "zerovec", 1286 | ] 1287 | 1288 | [[package]] 1289 | name = "tokio" 1290 | version = "1.47.1" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 1293 | dependencies = [ 1294 | "backtrace", 1295 | "bytes", 1296 | "io-uring", 1297 | "libc", 1298 | "mio", 1299 | "pin-project-lite", 1300 | "slab", 1301 | "socket2", 1302 | "windows-sys 0.59.0", 1303 | ] 1304 | 1305 | [[package]] 1306 | name = "tokio-native-tls" 1307 | version = "0.3.1" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1310 | dependencies = [ 1311 | "native-tls", 1312 | "tokio", 1313 | ] 1314 | 1315 | [[package]] 1316 | name = "tokio-rustls" 1317 | version = "0.26.2" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1320 | dependencies = [ 1321 | "rustls", 1322 | "tokio", 1323 | ] 1324 | 1325 | [[package]] 1326 | name = "tokio-util" 1327 | version = "0.7.16" 1328 | source = "registry+https://github.com/rust-lang/crates.io-index" 1329 | checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 1330 | dependencies = [ 1331 | "bytes", 1332 | "futures-core", 1333 | "futures-sink", 1334 | "pin-project-lite", 1335 | "tokio", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "toml" 1340 | version = "0.9.5" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" 1343 | dependencies = [ 1344 | "indexmap", 1345 | "serde", 1346 | "serde_spanned", 1347 | "toml_datetime", 1348 | "toml_parser", 1349 | "toml_writer", 1350 | "winnow", 1351 | ] 1352 | 1353 | [[package]] 1354 | name = "toml_datetime" 1355 | version = "0.7.0" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" 1358 | dependencies = [ 1359 | "serde", 1360 | ] 1361 | 1362 | [[package]] 1363 | name = "toml_parser" 1364 | version = "1.0.2" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" 1367 | dependencies = [ 1368 | "winnow", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "toml_writer" 1373 | version = "1.0.2" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" 1376 | 1377 | [[package]] 1378 | name = "tower" 1379 | version = "0.5.2" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1382 | dependencies = [ 1383 | "futures-core", 1384 | "futures-util", 1385 | "pin-project-lite", 1386 | "sync_wrapper", 1387 | "tokio", 1388 | "tower-layer", 1389 | "tower-service", 1390 | ] 1391 | 1392 | [[package]] 1393 | name = "tower-http" 1394 | version = "0.6.6" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 1397 | dependencies = [ 1398 | "bitflags", 1399 | "bytes", 1400 | "futures-util", 1401 | "http", 1402 | "http-body", 1403 | "iri-string", 1404 | "pin-project-lite", 1405 | "tower", 1406 | "tower-layer", 1407 | "tower-service", 1408 | ] 1409 | 1410 | [[package]] 1411 | name = "tower-layer" 1412 | version = "0.3.3" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1415 | 1416 | [[package]] 1417 | name = "tower-service" 1418 | version = "0.3.3" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1421 | 1422 | [[package]] 1423 | name = "tracing" 1424 | version = "0.1.41" 1425 | source = "registry+https://github.com/rust-lang/crates.io-index" 1426 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1427 | dependencies = [ 1428 | "pin-project-lite", 1429 | "tracing-core", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "tracing-core" 1434 | version = "0.1.34" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1437 | dependencies = [ 1438 | "once_cell", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "try-lock" 1443 | version = "0.2.5" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1446 | 1447 | [[package]] 1448 | name = "unicase" 1449 | version = "2.8.1" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1452 | 1453 | [[package]] 1454 | name = "unicode-ident" 1455 | version = "1.0.18" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1458 | 1459 | [[package]] 1460 | name = "unicode-width" 1461 | version = "0.2.1" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 1464 | 1465 | [[package]] 1466 | name = "untrusted" 1467 | version = "0.9.0" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1470 | 1471 | [[package]] 1472 | name = "url" 1473 | version = "2.5.4" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1476 | dependencies = [ 1477 | "form_urlencoded", 1478 | "idna", 1479 | "percent-encoding", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "utf8_iter" 1484 | version = "1.0.4" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1487 | 1488 | [[package]] 1489 | name = "utf8parse" 1490 | version = "0.2.2" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1493 | 1494 | [[package]] 1495 | name = "vcpkg" 1496 | version = "0.2.15" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1499 | 1500 | [[package]] 1501 | name = "want" 1502 | version = "0.3.1" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1505 | dependencies = [ 1506 | "try-lock", 1507 | ] 1508 | 1509 | [[package]] 1510 | name = "wasi" 1511 | version = "0.11.1+wasi-snapshot-preview1" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1514 | 1515 | [[package]] 1516 | name = "wasi" 1517 | version = "0.14.2+wasi-0.2.4" 1518 | source = "registry+https://github.com/rust-lang/crates.io-index" 1519 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1520 | dependencies = [ 1521 | "wit-bindgen-rt", 1522 | ] 1523 | 1524 | [[package]] 1525 | name = "wasm-bindgen" 1526 | version = "0.2.100" 1527 | source = "registry+https://github.com/rust-lang/crates.io-index" 1528 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1529 | dependencies = [ 1530 | "cfg-if", 1531 | "once_cell", 1532 | "rustversion", 1533 | "wasm-bindgen-macro", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "wasm-bindgen-backend" 1538 | version = "0.2.100" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1541 | dependencies = [ 1542 | "bumpalo", 1543 | "log", 1544 | "proc-macro2", 1545 | "quote", 1546 | "syn", 1547 | "wasm-bindgen-shared", 1548 | ] 1549 | 1550 | [[package]] 1551 | name = "wasm-bindgen-futures" 1552 | version = "0.4.50" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1555 | dependencies = [ 1556 | "cfg-if", 1557 | "js-sys", 1558 | "once_cell", 1559 | "wasm-bindgen", 1560 | "web-sys", 1561 | ] 1562 | 1563 | [[package]] 1564 | name = "wasm-bindgen-macro" 1565 | version = "0.2.100" 1566 | source = "registry+https://github.com/rust-lang/crates.io-index" 1567 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1568 | dependencies = [ 1569 | "quote", 1570 | "wasm-bindgen-macro-support", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "wasm-bindgen-macro-support" 1575 | version = "0.2.100" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1578 | dependencies = [ 1579 | "proc-macro2", 1580 | "quote", 1581 | "syn", 1582 | "wasm-bindgen-backend", 1583 | "wasm-bindgen-shared", 1584 | ] 1585 | 1586 | [[package]] 1587 | name = "wasm-bindgen-shared" 1588 | version = "0.2.100" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1591 | dependencies = [ 1592 | "unicode-ident", 1593 | ] 1594 | 1595 | [[package]] 1596 | name = "web-sys" 1597 | version = "0.3.77" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1600 | dependencies = [ 1601 | "js-sys", 1602 | "wasm-bindgen", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "windows-core" 1607 | version = "0.61.2" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 1610 | dependencies = [ 1611 | "windows-implement", 1612 | "windows-interface", 1613 | "windows-link", 1614 | "windows-result", 1615 | "windows-strings", 1616 | ] 1617 | 1618 | [[package]] 1619 | name = "windows-implement" 1620 | version = "0.60.0" 1621 | source = "registry+https://github.com/rust-lang/crates.io-index" 1622 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 1623 | dependencies = [ 1624 | "proc-macro2", 1625 | "quote", 1626 | "syn", 1627 | ] 1628 | 1629 | [[package]] 1630 | name = "windows-interface" 1631 | version = "0.59.1" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1634 | dependencies = [ 1635 | "proc-macro2", 1636 | "quote", 1637 | "syn", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "windows-link" 1642 | version = "0.1.3" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1645 | 1646 | [[package]] 1647 | name = "windows-registry" 1648 | version = "0.5.3" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 1651 | dependencies = [ 1652 | "windows-link", 1653 | "windows-result", 1654 | "windows-strings", 1655 | ] 1656 | 1657 | [[package]] 1658 | name = "windows-result" 1659 | version = "0.3.4" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 1662 | dependencies = [ 1663 | "windows-link", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "windows-strings" 1668 | version = "0.4.2" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 1671 | dependencies = [ 1672 | "windows-link", 1673 | ] 1674 | 1675 | [[package]] 1676 | name = "windows-sys" 1677 | version = "0.52.0" 1678 | source = "registry+https://github.com/rust-lang/crates.io-index" 1679 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1680 | dependencies = [ 1681 | "windows-targets 0.52.6", 1682 | ] 1683 | 1684 | [[package]] 1685 | name = "windows-sys" 1686 | version = "0.59.0" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1689 | dependencies = [ 1690 | "windows-targets 0.52.6", 1691 | ] 1692 | 1693 | [[package]] 1694 | name = "windows-sys" 1695 | version = "0.60.2" 1696 | source = "registry+https://github.com/rust-lang/crates.io-index" 1697 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1698 | dependencies = [ 1699 | "windows-targets 0.53.3", 1700 | ] 1701 | 1702 | [[package]] 1703 | name = "windows-targets" 1704 | version = "0.52.6" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1707 | dependencies = [ 1708 | "windows_aarch64_gnullvm 0.52.6", 1709 | "windows_aarch64_msvc 0.52.6", 1710 | "windows_i686_gnu 0.52.6", 1711 | "windows_i686_gnullvm 0.52.6", 1712 | "windows_i686_msvc 0.52.6", 1713 | "windows_x86_64_gnu 0.52.6", 1714 | "windows_x86_64_gnullvm 0.52.6", 1715 | "windows_x86_64_msvc 0.52.6", 1716 | ] 1717 | 1718 | [[package]] 1719 | name = "windows-targets" 1720 | version = "0.53.3" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 1723 | dependencies = [ 1724 | "windows-link", 1725 | "windows_aarch64_gnullvm 0.53.0", 1726 | "windows_aarch64_msvc 0.53.0", 1727 | "windows_i686_gnu 0.53.0", 1728 | "windows_i686_gnullvm 0.53.0", 1729 | "windows_i686_msvc 0.53.0", 1730 | "windows_x86_64_gnu 0.53.0", 1731 | "windows_x86_64_gnullvm 0.53.0", 1732 | "windows_x86_64_msvc 0.53.0", 1733 | ] 1734 | 1735 | [[package]] 1736 | name = "windows_aarch64_gnullvm" 1737 | version = "0.52.6" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1740 | 1741 | [[package]] 1742 | name = "windows_aarch64_gnullvm" 1743 | version = "0.53.0" 1744 | source = "registry+https://github.com/rust-lang/crates.io-index" 1745 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1746 | 1747 | [[package]] 1748 | name = "windows_aarch64_msvc" 1749 | version = "0.52.6" 1750 | source = "registry+https://github.com/rust-lang/crates.io-index" 1751 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1752 | 1753 | [[package]] 1754 | name = "windows_aarch64_msvc" 1755 | version = "0.53.0" 1756 | source = "registry+https://github.com/rust-lang/crates.io-index" 1757 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1758 | 1759 | [[package]] 1760 | name = "windows_i686_gnu" 1761 | version = "0.52.6" 1762 | source = "registry+https://github.com/rust-lang/crates.io-index" 1763 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1764 | 1765 | [[package]] 1766 | name = "windows_i686_gnu" 1767 | version = "0.53.0" 1768 | source = "registry+https://github.com/rust-lang/crates.io-index" 1769 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1770 | 1771 | [[package]] 1772 | name = "windows_i686_gnullvm" 1773 | version = "0.52.6" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1776 | 1777 | [[package]] 1778 | name = "windows_i686_gnullvm" 1779 | version = "0.53.0" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1782 | 1783 | [[package]] 1784 | name = "windows_i686_msvc" 1785 | version = "0.52.6" 1786 | source = "registry+https://github.com/rust-lang/crates.io-index" 1787 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1788 | 1789 | [[package]] 1790 | name = "windows_i686_msvc" 1791 | version = "0.53.0" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1794 | 1795 | [[package]] 1796 | name = "windows_x86_64_gnu" 1797 | version = "0.52.6" 1798 | source = "registry+https://github.com/rust-lang/crates.io-index" 1799 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1800 | 1801 | [[package]] 1802 | name = "windows_x86_64_gnu" 1803 | version = "0.53.0" 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" 1805 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1806 | 1807 | [[package]] 1808 | name = "windows_x86_64_gnullvm" 1809 | version = "0.52.6" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1812 | 1813 | [[package]] 1814 | name = "windows_x86_64_gnullvm" 1815 | version = "0.53.0" 1816 | source = "registry+https://github.com/rust-lang/crates.io-index" 1817 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1818 | 1819 | [[package]] 1820 | name = "windows_x86_64_msvc" 1821 | version = "0.52.6" 1822 | source = "registry+https://github.com/rust-lang/crates.io-index" 1823 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1824 | 1825 | [[package]] 1826 | name = "windows_x86_64_msvc" 1827 | version = "0.53.0" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1830 | 1831 | [[package]] 1832 | name = "winnow" 1833 | version = "0.7.12" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" 1836 | 1837 | [[package]] 1838 | name = "wit-bindgen-rt" 1839 | version = "0.39.0" 1840 | source = "registry+https://github.com/rust-lang/crates.io-index" 1841 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1842 | dependencies = [ 1843 | "bitflags", 1844 | ] 1845 | 1846 | [[package]] 1847 | name = "writeable" 1848 | version = "0.6.1" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1851 | 1852 | [[package]] 1853 | name = "yoke" 1854 | version = "0.8.0" 1855 | source = "registry+https://github.com/rust-lang/crates.io-index" 1856 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1857 | dependencies = [ 1858 | "serde", 1859 | "stable_deref_trait", 1860 | "yoke-derive", 1861 | "zerofrom", 1862 | ] 1863 | 1864 | [[package]] 1865 | name = "yoke-derive" 1866 | version = "0.8.0" 1867 | source = "registry+https://github.com/rust-lang/crates.io-index" 1868 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1869 | dependencies = [ 1870 | "proc-macro2", 1871 | "quote", 1872 | "syn", 1873 | "synstructure", 1874 | ] 1875 | 1876 | [[package]] 1877 | name = "zerofrom" 1878 | version = "0.1.6" 1879 | source = "registry+https://github.com/rust-lang/crates.io-index" 1880 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1881 | dependencies = [ 1882 | "zerofrom-derive", 1883 | ] 1884 | 1885 | [[package]] 1886 | name = "zerofrom-derive" 1887 | version = "0.1.6" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1890 | dependencies = [ 1891 | "proc-macro2", 1892 | "quote", 1893 | "syn", 1894 | "synstructure", 1895 | ] 1896 | 1897 | [[package]] 1898 | name = "zeroize" 1899 | version = "1.8.1" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1902 | 1903 | [[package]] 1904 | name = "zerotrie" 1905 | version = "0.2.2" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1908 | dependencies = [ 1909 | "displaydoc", 1910 | "yoke", 1911 | "zerofrom", 1912 | ] 1913 | 1914 | [[package]] 1915 | name = "zerovec" 1916 | version = "0.11.4" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 1919 | dependencies = [ 1920 | "yoke", 1921 | "zerofrom", 1922 | "zerovec-derive", 1923 | ] 1924 | 1925 | [[package]] 1926 | name = "zerovec-derive" 1927 | version = "0.11.1" 1928 | source = "registry+https://github.com/rust-lang/crates.io-index" 1929 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1930 | dependencies = [ 1931 | "proc-macro2", 1932 | "quote", 1933 | "syn", 1934 | ] 1935 | --------------------------------------------------------------------------------