├── .cargo └── config.toml ├── .clippy.toml ├── .github ├── renovate.json5 ├── settings.yml └── workflows │ ├── audit.yml │ ├── ci.yml │ ├── committed.yml │ ├── post-release.yml │ ├── pre-commit.yml │ ├── release-notes.py │ ├── rust-next.yml │ └── spelling.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── acknowledgements.bin ├── screenshot.svg ├── syntaxes.bin ├── theme_preview.rs └── themes.bin ├── committed.toml ├── deny.toml ├── docs └── comparison.md ├── release.toml ├── src ├── args.rs ├── assets │ ├── lazy_theme_set.rs │ └── mod.rs ├── blame.rs ├── config.rs ├── git2_config.rs ├── git_pager.rs ├── logger.rs └── main.rs ├── tests └── testsuite │ ├── cli.rs │ ├── docs.rs │ └── main.rs └── typos.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-Ctarget-feature=+crt-static"] 3 | 4 | [target.i686-pc-windows-msvc] 5 | rustflags = ["-Ctarget-feature=+crt-static"] 6 | -------------------------------------------------------------------------------- /.clippy.toml: -------------------------------------------------------------------------------- 1 | allow-print-in-tests = true 2 | allow-expect-in-tests = true 3 | allow-unwrap-in-tests = true 4 | allow-dbg-in-tests = true 5 | disallowed-methods = [ 6 | { path = "std::option::Option::map_or", reason = "prefer `map(..).unwrap_or(..)` for legibility" }, 7 | { path = "std::option::Option::map_or_else", reason = "prefer `map(..).unwrap_or_else(..)` for legibility" }, 8 | { path = "std::result::Result::map_or", reason = "prefer `map(..).unwrap_or(..)` for legibility" }, 9 | { path = "std::result::Result::map_or_else", reason = "prefer `map(..).unwrap_or_else(..)` for legibility" }, 10 | { path = "std::iter::Iterator::for_each", reason = "prefer `for` for side-effects" }, 11 | { path = "std::iter::Iterator::try_for_each", reason = "prefer `for` for side-effects" }, 12 | ] 13 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | schedule: [ 3 | 'before 5am on the first day of the month', 4 | ], 5 | semanticCommits: 'enabled', 6 | commitMessageLowerCase: 'never', 7 | configMigration: true, 8 | dependencyDashboard: true, 9 | customManagers: [ 10 | { 11 | customType: 'regex', 12 | fileMatch: [ 13 | '^rust-toolchain\\.toml$', 14 | 'Cargo.toml$', 15 | 'clippy.toml$', 16 | '\\.clippy.toml$', 17 | '^\\.github/workflows/ci.yml$', 18 | '^\\.github/workflows/rust-next.yml$', 19 | ], 20 | matchStrings: [ 21 | 'STABLE.*?(?\\d+\\.\\d+(\\.\\d+)?)', 22 | '(?\\d+\\.\\d+(\\.\\d+)?).*?STABLE', 23 | ], 24 | depNameTemplate: 'STABLE', 25 | packageNameTemplate: 'rust-lang/rust', 26 | datasourceTemplate: 'github-releases', 27 | }, 28 | ], 29 | packageRules: [ 30 | { 31 | commitMessageTopic: 'Rust Stable', 32 | matchManagers: [ 33 | 'custom.regex', 34 | ], 35 | matchDepNames: [ 36 | 'STABLE', 37 | ], 38 | extractVersion: '^(?\\d+\\.\\d+)', // Drop the patch version 39 | schedule: [ 40 | '* * * * *', 41 | ], 42 | automerge: true, 43 | }, 44 | // Goals: 45 | // - Rollup safe upgrades to reduce CI runner load 46 | // - Have lockfile and manifest in-sync 47 | { 48 | matchManagers: [ 49 | 'cargo', 50 | ], 51 | matchCurrentVersion: '>=0.1.0', 52 | matchUpdateTypes: [ 53 | 'patch', 54 | ], 55 | automerge: true, 56 | groupName: 'compatible', 57 | }, 58 | { 59 | matchManagers: [ 60 | 'cargo', 61 | ], 62 | matchCurrentVersion: '>=1.0.0', 63 | matchUpdateTypes: [ 64 | 'minor', 65 | 'patch', 66 | ], 67 | automerge: true, 68 | groupName: 'compatible', 69 | }, 70 | ], 71 | } 72 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # These settings are synced to GitHub by https://probot.github.io/apps/settings/ 2 | 3 | repository: 4 | description: "Dive into a file's history to find root cause" 5 | topics: "git rust cli" 6 | has_issues: true 7 | has_projects: false 8 | has_wiki: false 9 | has_downloads: true 10 | default_branch: main 11 | 12 | # Preference: people do clean commits 13 | allow_merge_commit: true 14 | # Backup in case we need to clean up commits 15 | allow_squash_merge: true 16 | # Not really needed 17 | allow_rebase_merge: false 18 | 19 | allow_auto_merge: true 20 | delete_branch_on_merge: true 21 | 22 | squash_merge_commit_title: "PR_TITLE" 23 | squash_merge_commit_message: "PR_BODY" 24 | merge_commit_message: "PR_BODY" 25 | 26 | labels: 27 | # Type 28 | - name: bug 29 | color: '#b60205' 30 | description: "Not as expected" 31 | - name: enhancement 32 | color: '#1d76db' 33 | description: "Improve the expected" 34 | # Flavor 35 | - name: question 36 | color: "#cc317c" 37 | description: "Uncertainty is involved" 38 | - name: breaking-change 39 | color: "#e99695" 40 | - name: good first issue 41 | color: '#c2e0c6' 42 | description: "Help wanted!" 43 | 44 | # This serves more as documentation. 45 | # Branch protection API was replaced by rulesets but settings isn't updated. 46 | # See https://github.com/repository-settings/app/issues/825 47 | # 48 | # branches: 49 | # - name: main 50 | # protection: 51 | # required_pull_request_reviews: null 52 | # required_conversation_resolution: true 53 | # required_status_checks: 54 | # # Required. Require branches to be up to date before merging. 55 | # strict: false 56 | # contexts: ["CI", "Spell Check with Typos"] 57 | # enforce_admins: false 58 | # restrictions: null 59 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - '**/Cargo.toml' 10 | - '**/Cargo.lock' 11 | push: 12 | branches: 13 | - main 14 | 15 | env: 16 | RUST_BACKTRACE: 1 17 | CARGO_TERM_COLOR: always 18 | CLICOLOR: 1 19 | 20 | concurrency: 21 | group: "${{ github.workflow }}-${{ github.ref }}" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | security_audit: 26 | permissions: 27 | issues: write # to create issues (actions-rs/audit-check) 28 | checks: write # to create check (actions-rs/audit-check) 29 | runs-on: ubuntu-latest 30 | # Prevent sudden announcement of a new advisory from failing ci: 31 | continue-on-error: true 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | - uses: actions-rs/audit-check@v1 36 | with: 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | cargo_deny: 40 | permissions: 41 | issues: write # to create issues (actions-rs/audit-check) 42 | checks: write # to create check (actions-rs/audit-check) 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | checks: 47 | - bans licenses sources 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: EmbarkStudios/cargo-deny-action@v2 51 | with: 52 | command: check ${{ matrix.checks }} 53 | rust-version: stable 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | 12 | env: 13 | RUST_BACKTRACE: 1 14 | CARGO_TERM_COLOR: always 15 | CLICOLOR: 1 16 | 17 | concurrency: 18 | group: "${{ github.workflow }}-${{ github.ref }}" 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | ci: 23 | permissions: 24 | contents: none 25 | name: CI 26 | needs: [test, msrv, lockfile, docs, rustfmt, clippy] 27 | runs-on: ubuntu-latest 28 | if: "always()" 29 | steps: 30 | - name: Failed 31 | run: exit 1 32 | if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')" 33 | test: 34 | name: Test 35 | strategy: 36 | matrix: 37 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 38 | rust: ["stable"] 39 | continue-on-error: ${{ matrix.rust != 'stable' }} 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | - name: Install Rust 45 | uses: dtolnay/rust-toolchain@stable 46 | with: 47 | toolchain: ${{ matrix.rust }} 48 | - uses: Swatinem/rust-cache@v2 49 | - name: Configure git 50 | run: | 51 | git config --global user.name "Test User" 52 | git config --global user.email "test_user@example.com" 53 | - uses: taiki-e/install-action@cargo-hack 54 | - name: Build 55 | run: cargo test --workspace --no-run 56 | - name: Test 57 | run: cargo hack test --feature-powerset --workspace 58 | msrv: 59 | name: "Check MSRV" 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout repository 63 | uses: actions/checkout@v4 64 | - name: Install Rust 65 | uses: dtolnay/rust-toolchain@stable 66 | with: 67 | toolchain: stable 68 | - uses: Swatinem/rust-cache@v2 69 | - uses: taiki-e/install-action@cargo-hack 70 | - name: Default features 71 | run: cargo hack check --feature-powerset --locked --rust-version --ignore-private --workspace --all-targets 72 | lockfile: 73 | runs-on: ubuntu-latest 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@v4 77 | - name: Install Rust 78 | uses: dtolnay/rust-toolchain@stable 79 | with: 80 | toolchain: stable 81 | - uses: Swatinem/rust-cache@v2 82 | - name: "Is lockfile updated?" 83 | run: cargo update --workspace --locked 84 | docs: 85 | name: Docs 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Checkout repository 89 | uses: actions/checkout@v4 90 | - name: Install Rust 91 | uses: dtolnay/rust-toolchain@stable 92 | with: 93 | toolchain: "1.87" # STABLE 94 | - uses: Swatinem/rust-cache@v2 95 | - name: Check documentation 96 | env: 97 | RUSTDOCFLAGS: -D warnings 98 | run: cargo doc --workspace --all-features --no-deps --document-private-items 99 | rustfmt: 100 | name: rustfmt 101 | runs-on: ubuntu-latest 102 | steps: 103 | - name: Checkout repository 104 | uses: actions/checkout@v4 105 | - name: Install Rust 106 | uses: dtolnay/rust-toolchain@stable 107 | with: 108 | toolchain: "1.87" # STABLE 109 | components: rustfmt 110 | - uses: Swatinem/rust-cache@v2 111 | - name: Check formatting 112 | run: cargo fmt --all -- --check 113 | clippy: 114 | name: clippy 115 | runs-on: ubuntu-latest 116 | permissions: 117 | security-events: write # to upload sarif results 118 | steps: 119 | - name: Checkout repository 120 | uses: actions/checkout@v4 121 | - name: Install Rust 122 | uses: dtolnay/rust-toolchain@stable 123 | with: 124 | toolchain: "1.87" # STABLE 125 | components: clippy 126 | - uses: Swatinem/rust-cache@v2 127 | - name: Install SARIF tools 128 | run: cargo install clippy-sarif --locked 129 | - name: Install SARIF tools 130 | run: cargo install sarif-fmt --locked 131 | - name: Check 132 | run: > 133 | cargo clippy --workspace --all-features --all-targets --message-format=json 134 | | clippy-sarif 135 | | tee clippy-results.sarif 136 | | sarif-fmt 137 | continue-on-error: true 138 | - name: Upload 139 | uses: github/codeql-action/upload-sarif@v3 140 | with: 141 | sarif_file: clippy-results.sarif 142 | wait-for-processing: true 143 | - name: Report status 144 | run: cargo clippy --workspace --all-features --all-targets -- -D warnings --allow deprecated 145 | coverage: 146 | name: Coverage 147 | runs-on: ubuntu-latest 148 | steps: 149 | - name: Checkout repository 150 | uses: actions/checkout@v4 151 | - name: Install Rust 152 | uses: dtolnay/rust-toolchain@stable 153 | with: 154 | toolchain: stable 155 | - uses: Swatinem/rust-cache@v2 156 | - name: Install cargo-tarpaulin 157 | run: cargo install cargo-tarpaulin 158 | - name: Configure git 159 | run: | 160 | git config --global user.name "Test User" 161 | git config --global user.email "test_user@example.com" 162 | - name: Gather coverage 163 | run: cargo tarpaulin --output-dir coverage --out lcov 164 | - name: Publish to Coveralls 165 | uses: coverallsapp/github-action@master 166 | with: 167 | github-token: ${{ secrets.GITHUB_TOKEN }} 168 | -------------------------------------------------------------------------------- /.github/workflows/committed.yml: -------------------------------------------------------------------------------- 1 | # Not run as part of pre-commit checks because they don't handle sending the correct commit 2 | # range to `committed` 3 | name: Lint Commits 4 | on: [pull_request] 5 | 6 | permissions: 7 | contents: read 8 | 9 | env: 10 | RUST_BACKTRACE: 1 11 | CARGO_TERM_COLOR: always 12 | CLICOLOR: 1 13 | 14 | concurrency: 15 | group: "${{ github.workflow }}-${{ github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | committed: 20 | name: Lint Commits 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout Actions Repository 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Lint Commits 28 | uses: crate-ci/committed@master 29 | -------------------------------------------------------------------------------- /.github/workflows/post-release.yml: -------------------------------------------------------------------------------- 1 | # The way this works is the following: 2 | # 3 | # The create-release job runs purely to initialize the GitHub release itself 4 | # and to output upload_url for the following job. 5 | # 6 | # The build-release job runs only once create-release is finished. It gets the 7 | # release upload URL from create-release job outputs, then builds the release 8 | # executables for each supported platform and attaches them as release assets 9 | # to the previously created release. 10 | # 11 | # The key here is that we create the release only once. 12 | # 13 | # Reference: 14 | # https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ 15 | 16 | name: post-release 17 | on: 18 | push: 19 | tags: 20 | - "v*" 21 | 22 | env: 23 | CRATE_NAME: git-dive 24 | 25 | # We need this to be able to create releases. 26 | permissions: 27 | contents: write 28 | 29 | jobs: 30 | create-release: 31 | name: create-release 32 | runs-on: ubuntu-latest 33 | outputs: 34 | tag: ${{ env.TAG }} 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 1 40 | - name: Get the release version from the tag 41 | if: env.TAG == '' 42 | run: echo "TAG=${{ github.ref_name }}" >> $GITHUB_ENV 43 | - name: Show the tag 44 | run: | 45 | echo "tag is: $TAG" 46 | - name: Generate Release Notes 47 | run: | 48 | ./.github/workflows/release-notes.py --tag ${{ env.TAG }} --output notes-${{ env.TAG }}.md 49 | cat notes-${{ env.TAG }}.md 50 | - name: Create GitHub release 51 | env: 52 | GH_TOKEN: ${{ github.token }} 53 | run: gh release create $TAG --verify-tag --draft --title $TAG --notes-file notes-${{ env.TAG }}.md 54 | build-release: 55 | name: build-release 56 | needs: create-release 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | build: [linux, macos, win-msvc] 61 | include: 62 | - build: linux 63 | os: ubuntu-22.04 64 | rust: stable 65 | target: x86_64-unknown-linux-musl 66 | - build: macos 67 | os: macos-latest 68 | rust: stable 69 | target: x86_64-apple-darwin 70 | - build: macos-aarch64 71 | os: macos-latest 72 | rust: stable 73 | target: aarch64-apple-darwin 74 | - build: win-msvc 75 | os: windows-2019 76 | rust: stable 77 | target: x86_64-pc-windows-msvc 78 | runs-on: ${{ matrix.os }} 79 | steps: 80 | - name: Checkout repository 81 | uses: actions/checkout@v4 82 | with: 83 | fetch-depth: 1 84 | - name: Install packages (Ubuntu) 85 | if: matrix.os == 'ubuntu-22.04' 86 | run: | 87 | sudo apt-get update 88 | sudo apt-get install -y --no-install-recommends xz-utils liblz4-tool musl-tools 89 | - name: Install Rust 90 | uses: dtolnay/rust-toolchain@stable 91 | with: 92 | toolchain: ${{ matrix.rust }} 93 | targets: ${{ matrix.target }} 94 | - name: Build release binary 95 | run: cargo build --target ${{ matrix.target }} --verbose --release 96 | - name: Build archive 97 | shell: bash 98 | run: | 99 | outdir="./target/${{ matrix.target }}/release" 100 | staging="${{ env.BIN_NAME }}-${{ needs.create-release.outputs.tag }}-${{ matrix.target }}" 101 | mkdir -p "$staging"/doc 102 | cp {README.md,LICENSE-*} "$staging/" 103 | cp {CHANGELOG.md,docs/*} "$staging/doc/" 104 | if [ "${{ matrix.os }}" = "windows-2019" ]; then 105 | cp "target/${{ matrix.target }}/release/${{ env.BIN_NAME }}.exe" "$staging/" 106 | ls -l "$staging" 107 | cd "$staging" 108 | 7z a "../$staging.zip" . 109 | echo "ASSET=$staging.zip" >> $GITHUB_ENV 110 | else 111 | cp "target/${{ matrix.target }}/release/${{ env.BIN_NAME }}" "$staging/" 112 | ls -l "$staging" 113 | tar czf "$staging.tar.gz" -C "$staging" . 114 | echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV 115 | fi 116 | - name: Upload release archive 117 | env: 118 | GH_TOKEN: ${{ github.token }} 119 | shell: bash 120 | run: | 121 | tag="${{ needs.create-release.outputs.tag }}" 122 | gh release upload "$tag" ${{ env.ASSET }} 123 | publish-release: 124 | name: Publish Release 125 | needs: [create-release, build-release] 126 | runs-on: ubuntu-latest 127 | steps: 128 | - name: Checkout repository 129 | uses: actions/checkout@v4 130 | with: 131 | fetch-depth: 1 132 | - name: Publish Release 133 | env: 134 | GH_TOKEN: ${{ github.token }} 135 | run: gh release edit "${{ needs.create-release.outputs.tag }}" --draft=false 136 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | permissions: {} # none 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: [main] 9 | 10 | env: 11 | RUST_BACKTRACE: 1 12 | CARGO_TERM_COLOR: always 13 | CLICOLOR: 1 14 | 15 | concurrency: 16 | group: "${{ github.workflow }}-${{ github.ref }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | pre-commit: 21 | permissions: 22 | contents: read 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-python@v5 27 | with: 28 | python-version: '3.x' 29 | - uses: pre-commit/action@v3.0.1 30 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import re 5 | import pathlib 6 | import sys 7 | 8 | 9 | _STDIO = pathlib.Path("-") 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("-i", "--input", type=pathlib.Path, default="CHANGELOG.md") 15 | parser.add_argument("--tag", required=True) 16 | parser.add_argument("-o", "--output", type=pathlib.Path, required=True) 17 | args = parser.parse_args() 18 | 19 | if args.input == _STDIO: 20 | lines = sys.stdin.readlines() 21 | else: 22 | with args.input.open() as fh: 23 | lines = fh.readlines() 24 | version = args.tag.lstrip("v") 25 | 26 | note_lines = [] 27 | for line in lines: 28 | if line.startswith("## ") and version in line: 29 | note_lines.append(line) 30 | elif note_lines and line.startswith("## "): 31 | break 32 | elif note_lines: 33 | note_lines.append(line) 34 | 35 | notes = "".join(note_lines).strip() 36 | if args.output == _STDIO: 37 | print(notes) 38 | else: 39 | args.output.write_text(notes) 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /.github/workflows/rust-next.yml: -------------------------------------------------------------------------------- 1 | name: rust-next 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | schedule: 8 | - cron: '7 7 7 * *' 9 | 10 | env: 11 | RUST_BACKTRACE: 1 12 | CARGO_TERM_COLOR: always 13 | CLICOLOR: 1 14 | 15 | concurrency: 16 | group: "${{ github.workflow }}-${{ github.ref }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | name: Test 22 | strategy: 23 | matrix: 24 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 25 | rust: ["stable", "beta"] 26 | include: 27 | - os: ubuntu-latest 28 | rust: "nightly" 29 | continue-on-error: ${{ matrix.rust != 'stable' }} 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | - name: Install Rust 35 | uses: dtolnay/rust-toolchain@stable 36 | with: 37 | toolchain: ${{ matrix.rust }} 38 | - uses: Swatinem/rust-cache@v2 39 | - name: Configure git 40 | run: | 41 | git config --global user.name "Test User" 42 | git config --global user.email "test_user@example.com" 43 | - uses: taiki-e/install-action@cargo-hack 44 | - name: Build 45 | run: cargo test --workspace --no-run 46 | - name: Test 47 | run: cargo hack test --feature-powerset --workspace 48 | latest: 49 | name: "Check latest dependencies" 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v4 54 | - name: Install Rust 55 | uses: dtolnay/rust-toolchain@stable 56 | with: 57 | toolchain: stable 58 | - uses: Swatinem/rust-cache@v2 59 | - name: Configure git 60 | run: | 61 | git config --global user.name "Test User" 62 | git config --global user.email "test_user@example.com" 63 | - uses: taiki-e/install-action@cargo-hack 64 | - name: Update dependencies 65 | run: cargo update 66 | - name: Build 67 | run: cargo test --workspace --no-run 68 | - name: Test 69 | run: cargo hack test --feature-powerset --workspace 70 | -------------------------------------------------------------------------------- /.github/workflows/spelling.yml: -------------------------------------------------------------------------------- 1 | name: Spelling 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [pull_request] 7 | 8 | env: 9 | RUST_BACKTRACE: 1 10 | CARGO_TERM_COLOR: always 11 | CLICOLOR: 1 12 | 13 | concurrency: 14 | group: "${{ github.workflow }}-${{ github.ref }}" 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | spelling: 19 | name: Spell Check with Typos 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout Actions Repository 23 | uses: actions/checkout@v4 24 | - name: Spell Check Repo 25 | uses: crate-ci/typos@master 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | stages: [commit] 7 | - id: check-json 8 | stages: [commit] 9 | - id: check-toml 10 | stages: [commit] 11 | - id: check-merge-conflict 12 | stages: [commit] 13 | - id: check-case-conflict 14 | stages: [commit] 15 | - id: detect-private-key 16 | stages: [commit] 17 | - repo: https://github.com/crate-ci/typos 18 | rev: v1.16.20 19 | hooks: 20 | - id: typos 21 | stages: [commit] 22 | - repo: https://github.com/crate-ci/committed 23 | rev: v1.0.20 24 | hooks: 25 | - id: committed 26 | stages: [commit-msg] 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | 8 | ## [Unreleased] - ReleaseDate 9 | 10 | ## [0.1.6] - 2023-04-13 11 | 12 | ### Internal 13 | 14 | - Update dependency 15 | 16 | ## [0.1.5] - 2023-03-18 17 | 18 | ### Fixes 19 | 20 | - Don't crash when highlighting javascript 21 | 22 | ## [0.1.4] - 2023-03-16 23 | 24 | ### Fixes 25 | 26 | - Correctly find repo-relative path on Windows 27 | 28 | ## [0.1.3] - 2023-03-16 29 | 30 | ### Fixes 31 | 32 | - Allow using system git2 33 | 34 | ## [0.1.2] - 2023-03-16 35 | 36 | ### Internal 37 | 38 | - Update dependency 39 | 40 | ## [0.1.1] - 2023-03-16 41 | 42 | ### Documentation 43 | 44 | - Slight tweak 45 | 46 | ## [0.1.0] - 2023-03-16 47 | 48 | 49 | [Unreleased]: https://github.com/gitext-rs/git-dive/compare/v0.1.6...HEAD 50 | [0.1.6]: https://github.com/gitext-rs/git-dive/compare/v0.1.5...v0.1.6 51 | [0.1.5]: https://github.com/gitext-rs/git-dive/compare/v0.1.4...v0.1.5 52 | [0.1.4]: https://github.com/gitext-rs/git-dive/compare/v0.1.3...v0.1.4 53 | [0.1.3]: https://github.com/gitext-rs/git-dive/compare/v0.1.2...v0.1.3 54 | [0.1.2]: https://github.com/gitext-rs/git-dive/compare/v0.1.1...v0.1.2 55 | [0.1.1]: https://github.com/gitext-rs/git-dive/compare/v0.1.0...v0.1.1 56 | [0.1.0]: https://github.com/gitext-rs/git-dive/compare/4c4c6c6ab7dd425a8c0fe3f29476e63b57e28d44...v0.1.0 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `git-dive` 2 | 3 | Thanks for wanting to contribute! There are many ways to contribute and we 4 | appreciate any level you're willing to do. 5 | 6 | ## Feature Requests 7 | 8 | Need some new functionality to help? You can let us know by opening an 9 | [issue][new issue]. It's helpful to look through [all issues][all issues] in 10 | case it's already being talked about. 11 | 12 | ## Bug Reports 13 | 14 | Please let us know about what problems you run into, whether in behavior or 15 | ergonomics of API. You can do this by opening an [issue][new issue]. It's 16 | helpful to look through [all issues][all issues] in case it's already being 17 | talked about. 18 | 19 | ### Reproducing Bugs 20 | 21 | To make reproduction easier, we've created a YAML format for describing git 22 | trees. You can verify your yaml file by the `git-fixture` command. 23 | 24 | - [Schema](crates/git-fixture/docs/schema.json) 25 | - [Examples](tests/fixtures/) 26 | 27 | ## Pull Requests 28 | 29 | Looking for an idea? Check our [issues][issues]. If the issue looks open ended, 30 | it is probably best to post on the issue how you are thinking of resolving the 31 | issue so you can get feedback early in the process. We want you to be 32 | successful and it can be discouraging to find out a lot of re-work is needed. 33 | 34 | Already have an idea? It might be good to first [create an issue][new issue] 35 | to propose it so we can make sure we are aligned and lower the risk of having 36 | to re-work some of it and the discouragement that goes along with that. 37 | 38 | ### Process 39 | 40 | As a heads up, we'll be running your PR through the following gauntlet: 41 | - warnings turned to compile errors 42 | - `cargo test` 43 | - `rustfmt` 44 | - `clippy` 45 | - `rustdoc` 46 | - [`committed`](https://github.com/crate-ci/committed) as we use [Conventional](https://www.conventionalcommits.org) commit style 47 | - [`typos`](https://github.com/crate-ci/typos) to check spelling 48 | 49 | Not everything can be checked automatically though. 50 | 51 | We request that the commit history gets cleaned up. 52 | 53 | We ask that commits are atomic, meaning they are complete and have a single responsibility. 54 | A complete commit should build, pass tests, update documentation and tests, and not have dead code. 55 | 56 | PRs should tell a cohesive story, with refactor and test commits that keep the 57 | fix or feature commits simple and clear. 58 | 59 | Specifically, we would encourage 60 | - File renames be isolated into their own commit 61 | - Add tests in a commit before their feature or fix, showing the current behavior (i.e. they should pass). 62 | The diff for the feature/fix commit will then show how the behavior changed, 63 | making the commit's intent clearer to reviewers and the community, and showing people that the 64 | test is verifying the expected state. 65 | - e.g. [clap#5520](https://github.com/clap-rs/clap/pull/5520) 66 | 67 | Note that we are talking about ideals. 68 | We understand having a clean history requires more advanced git skills; 69 | feel free to ask us for help! 70 | We might even suggest where it would work to be lax. 71 | We also understand that editing some early commits may cause a lot of churn 72 | with merge conflicts which can make it not worth editing all of the history. 73 | 74 | For code organization, we recommend 75 | - Grouping `impl` blocks next to their type (or trait) 76 | - Grouping private items after the `pub` item that uses them. 77 | - The intent is to help people quickly find the "relevant" details, allowing them to "dig deeper" as needed. Or put another way, the `pub` items serve as a table-of-contents. 78 | - The exact order is fuzzy; do what makes sense 79 | 80 | ## Releasing 81 | 82 | Pre-requisites 83 | - Running `cargo login` 84 | - A member of `ORG:Maintainers` 85 | - Push permission to the repo 86 | - [`cargo-release`](https://github.com/crate-ci/cargo-release/) 87 | 88 | When we're ready to release, a project owner should do the following 89 | 1. Update the changelog (see `cargo release changes` for ideas) 90 | 2. Determine what the next version is, according to semver 91 | 3. Run [`cargo release -x `](https://github.com/crate-ci/cargo-release) 92 | 93 | [issues]: https://github.com/gitext-rs/git-dive/issues 94 | [new issue]: https://github.com/gitext-rs/git-dive/issues/new 95 | [all issues]: https://github.com/gitext-rs/git-dive/issues?utf8=%E2%9C%93&q=is%3Aissue 96 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 55 | dependencies = [ 56 | "windows-sys 0.48.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-syntect" 61 | version = "1.0.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "2ae9340ad778909f64f80cea87a80052b6a740c639dcd8e88dd6bd4e83286a5d" 64 | dependencies = [ 65 | "anstyle", 66 | "syntect", 67 | "thiserror", 68 | ] 69 | 70 | [[package]] 71 | name = "anstyle-wincon" 72 | version = "3.0.6" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 75 | dependencies = [ 76 | "anstyle", 77 | "windows-sys 0.59.0", 78 | ] 79 | 80 | [[package]] 81 | name = "anyhow" 82 | version = "1.0.98" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 85 | 86 | [[package]] 87 | name = "atty" 88 | version = "0.2.14" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 91 | dependencies = [ 92 | "hermit-abi 0.1.19", 93 | "libc", 94 | "winapi", 95 | ] 96 | 97 | [[package]] 98 | name = "automod" 99 | version = "1.0.15" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "ebb4bd301db2e2ca1f5be131c24eb8ebf2d9559bc3744419e93baf8ddea7e670" 102 | dependencies = [ 103 | "proc-macro2", 104 | "quote", 105 | "syn 2.0.86", 106 | ] 107 | 108 | [[package]] 109 | name = "backtrace" 110 | version = "0.3.74" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 113 | dependencies = [ 114 | "addr2line", 115 | "cfg-if", 116 | "libc", 117 | "miniz_oxide", 118 | "object", 119 | "rustc-demangle", 120 | "windows-targets 0.52.6", 121 | ] 122 | 123 | [[package]] 124 | name = "bincode" 125 | version = "1.3.3" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 128 | dependencies = [ 129 | "serde", 130 | ] 131 | 132 | [[package]] 133 | name = "bincode" 134 | version = "2.0.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" 137 | dependencies = [ 138 | "serde", 139 | "unty", 140 | ] 141 | 142 | [[package]] 143 | name = "bitflags" 144 | version = "1.3.2" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 147 | 148 | [[package]] 149 | name = "bitflags" 150 | version = "2.4.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 153 | 154 | [[package]] 155 | name = "block-buffer" 156 | version = "0.10.4" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 159 | dependencies = [ 160 | "generic-array", 161 | ] 162 | 163 | [[package]] 164 | name = "bstr" 165 | version = "1.4.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" 168 | dependencies = [ 169 | "memchr", 170 | "once_cell", 171 | "regex-automata", 172 | "serde", 173 | ] 174 | 175 | [[package]] 176 | name = "bugreport" 177 | version = "0.5.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "f280f65ce85b880919349bbfcb204930291251eedcb2e5f84ce2f51df969c162" 180 | dependencies = [ 181 | "git-version", 182 | "shell-escape", 183 | "sysinfo", 184 | ] 185 | 186 | [[package]] 187 | name = "bytecount" 188 | version = "0.6.3" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" 191 | 192 | [[package]] 193 | name = "cc" 194 | version = "1.0.79" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 197 | dependencies = [ 198 | "jobserver", 199 | ] 200 | 201 | [[package]] 202 | name = "cfg-if" 203 | version = "1.0.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 206 | 207 | [[package]] 208 | name = "clap" 209 | version = "4.5.39" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 212 | dependencies = [ 213 | "clap_builder", 214 | "clap_derive", 215 | ] 216 | 217 | [[package]] 218 | name = "clap-verbosity-flag" 219 | version = "3.0.3" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1" 222 | dependencies = [ 223 | "clap", 224 | "log", 225 | ] 226 | 227 | [[package]] 228 | name = "clap_builder" 229 | version = "4.5.39" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 232 | dependencies = [ 233 | "anstream", 234 | "anstyle", 235 | "clap_lex", 236 | "strsim", 237 | "terminal_size", 238 | ] 239 | 240 | [[package]] 241 | name = "clap_derive" 242 | version = "4.5.32" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 245 | dependencies = [ 246 | "heck", 247 | "proc-macro2", 248 | "quote", 249 | "syn 2.0.86", 250 | ] 251 | 252 | [[package]] 253 | name = "clap_lex" 254 | version = "0.7.4" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 257 | 258 | [[package]] 259 | name = "colorchoice" 260 | version = "1.0.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 263 | 264 | [[package]] 265 | name = "colorchoice-clap" 266 | version = "1.0.6" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "96a804da36d925510ac4d61a0ee8edfdba6ae00c7d5c93c8bf58f25915966956" 269 | dependencies = [ 270 | "clap", 271 | "colorchoice", 272 | ] 273 | 274 | [[package]] 275 | name = "content_inspector" 276 | version = "0.2.4" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" 279 | dependencies = [ 280 | "memchr", 281 | ] 282 | 283 | [[package]] 284 | name = "convert_case" 285 | version = "0.7.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 288 | dependencies = [ 289 | "unicode-segmentation", 290 | ] 291 | 292 | [[package]] 293 | name = "core-foundation-sys" 294 | version = "0.8.7" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 297 | 298 | [[package]] 299 | name = "cpufeatures" 300 | version = "0.2.5" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" 303 | dependencies = [ 304 | "libc", 305 | ] 306 | 307 | [[package]] 308 | name = "crc32fast" 309 | version = "1.3.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 312 | dependencies = [ 313 | "cfg-if", 314 | ] 315 | 316 | [[package]] 317 | name = "crossbeam-deque" 318 | version = "0.8.6" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 321 | dependencies = [ 322 | "crossbeam-epoch", 323 | "crossbeam-utils", 324 | ] 325 | 326 | [[package]] 327 | name = "crossbeam-epoch" 328 | version = "0.9.18" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 331 | dependencies = [ 332 | "crossbeam-utils", 333 | ] 334 | 335 | [[package]] 336 | name = "crossbeam-utils" 337 | version = "0.8.21" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 340 | 341 | [[package]] 342 | name = "crypto-common" 343 | version = "0.1.6" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 346 | dependencies = [ 347 | "generic-array", 348 | "typenum", 349 | ] 350 | 351 | [[package]] 352 | name = "ctor" 353 | version = "0.1.26" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" 356 | dependencies = [ 357 | "quote", 358 | "syn 1.0.109", 359 | ] 360 | 361 | [[package]] 362 | name = "derive_more" 363 | version = "2.0.1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 366 | dependencies = [ 367 | "derive_more-impl", 368 | ] 369 | 370 | [[package]] 371 | name = "derive_more-impl" 372 | version = "2.0.1" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 375 | dependencies = [ 376 | "convert_case", 377 | "proc-macro2", 378 | "quote", 379 | "syn 2.0.86", 380 | ] 381 | 382 | [[package]] 383 | name = "diff" 384 | version = "0.1.13" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 387 | 388 | [[package]] 389 | name = "digest" 390 | version = "0.10.6" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 393 | dependencies = [ 394 | "block-buffer", 395 | "crypto-common", 396 | ] 397 | 398 | [[package]] 399 | name = "dunce" 400 | version = "1.0.5" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 403 | 404 | [[package]] 405 | name = "either" 406 | version = "1.8.1" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 409 | 410 | [[package]] 411 | name = "encoding_rs" 412 | version = "0.8.35" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 415 | dependencies = [ 416 | "cfg-if", 417 | ] 418 | 419 | [[package]] 420 | name = "env_filter" 421 | version = "0.1.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 424 | dependencies = [ 425 | "log", 426 | ] 427 | 428 | [[package]] 429 | name = "env_logger" 430 | version = "0.11.8" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 433 | dependencies = [ 434 | "anstream", 435 | "anstyle", 436 | "env_filter", 437 | "log", 438 | ] 439 | 440 | [[package]] 441 | name = "equivalent" 442 | version = "1.0.1" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 445 | 446 | [[package]] 447 | name = "errno" 448 | version = "0.2.8" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 451 | dependencies = [ 452 | "errno-dragonfly", 453 | "libc", 454 | "winapi", 455 | ] 456 | 457 | [[package]] 458 | name = "errno" 459 | version = "0.3.10" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 462 | dependencies = [ 463 | "libc", 464 | "windows-sys 0.59.0", 465 | ] 466 | 467 | [[package]] 468 | name = "errno-dragonfly" 469 | version = "0.1.2" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 472 | dependencies = [ 473 | "cc", 474 | "libc", 475 | ] 476 | 477 | [[package]] 478 | name = "eyre" 479 | version = "0.6.8" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" 482 | dependencies = [ 483 | "indenter", 484 | "once_cell", 485 | ] 486 | 487 | [[package]] 488 | name = "fastrand" 489 | version = "1.9.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 492 | dependencies = [ 493 | "instant", 494 | ] 495 | 496 | [[package]] 497 | name = "filetime" 498 | version = "0.2.20" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" 501 | dependencies = [ 502 | "cfg-if", 503 | "libc", 504 | "redox_syscall", 505 | "windows-sys 0.45.0", 506 | ] 507 | 508 | [[package]] 509 | name = "flate2" 510 | version = "1.1.1" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 513 | dependencies = [ 514 | "crc32fast", 515 | "miniz_oxide", 516 | ] 517 | 518 | [[package]] 519 | name = "fnv" 520 | version = "1.0.7" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 523 | 524 | [[package]] 525 | name = "form_urlencoded" 526 | version = "1.1.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 529 | dependencies = [ 530 | "percent-encoding", 531 | ] 532 | 533 | [[package]] 534 | name = "generic-array" 535 | version = "0.14.6" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 538 | dependencies = [ 539 | "typenum", 540 | "version_check", 541 | ] 542 | 543 | [[package]] 544 | name = "getrandom" 545 | version = "0.2.8" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 548 | dependencies = [ 549 | "cfg-if", 550 | "libc", 551 | "wasi", 552 | ] 553 | 554 | [[package]] 555 | name = "gimli" 556 | version = "0.31.1" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 559 | 560 | [[package]] 561 | name = "git-config-env" 562 | version = "0.2.3" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "20f47a62ab6eeab1a4a8e73cfb3eef632a19cc30da08ed8e84a381b519fa8cfc" 565 | dependencies = [ 566 | "itertools", 567 | "winnow", 568 | ] 569 | 570 | [[package]] 571 | name = "git-dive" 572 | version = "0.1.6" 573 | dependencies = [ 574 | "anstream", 575 | "anstyle", 576 | "anstyle-syntect", 577 | "anyhow", 578 | "automod", 579 | "bincode 2.0.1", 580 | "bugreport", 581 | "clap", 582 | "clap-verbosity-flag", 583 | "colorchoice-clap", 584 | "content_inspector", 585 | "dunce", 586 | "encoding_rs", 587 | "env_logger", 588 | "flate2", 589 | "git-config-env", 590 | "git-fixture", 591 | "git2", 592 | "human-panic", 593 | "is-terminal", 594 | "log", 595 | "once_cell", 596 | "path-clean", 597 | "proc-exit", 598 | "serde", 599 | "shlex", 600 | "snapbox", 601 | "syntect", 602 | "term-transcript", 603 | "terminal_size", 604 | "textwrap", 605 | ] 606 | 607 | [[package]] 608 | name = "git-fixture" 609 | version = "0.3.5" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "1fcb1ee4a05575949f7b51f0b007a98209265663d47ab792a2584e87d69d40db" 612 | dependencies = [ 613 | "bstr", 614 | "derive_more", 615 | "eyre", 616 | "git2", 617 | "log", 618 | "pkg-config", 619 | ] 620 | 621 | [[package]] 622 | name = "git-version" 623 | version = "0.3.5" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" 626 | dependencies = [ 627 | "git-version-macro", 628 | "proc-macro-hack", 629 | ] 630 | 631 | [[package]] 632 | name = "git-version-macro" 633 | version = "0.3.5" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" 636 | dependencies = [ 637 | "proc-macro-hack", 638 | "proc-macro2", 639 | "quote", 640 | "syn 1.0.109", 641 | ] 642 | 643 | [[package]] 644 | name = "git2" 645 | version = "0.18.3" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" 648 | dependencies = [ 649 | "bitflags 2.4.0", 650 | "libc", 651 | "libgit2-sys", 652 | "log", 653 | "url", 654 | ] 655 | 656 | [[package]] 657 | name = "handlebars" 658 | version = "4.3.6" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "035ef95d03713f2c347a72547b7cd38cbc9af7cd51e6099fb62d586d4a6dee3a" 661 | dependencies = [ 662 | "log", 663 | "pest", 664 | "pest_derive", 665 | "serde", 666 | "serde_json", 667 | "thiserror", 668 | ] 669 | 670 | [[package]] 671 | name = "hashbrown" 672 | version = "0.14.3" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 675 | 676 | [[package]] 677 | name = "heck" 678 | version = "0.5.0" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 681 | 682 | [[package]] 683 | name = "hermit-abi" 684 | version = "0.1.19" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 687 | dependencies = [ 688 | "libc", 689 | ] 690 | 691 | [[package]] 692 | name = "hermit-abi" 693 | version = "0.3.1" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 696 | 697 | [[package]] 698 | name = "hermit-abi" 699 | version = "0.5.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" 702 | 703 | [[package]] 704 | name = "human-panic" 705 | version = "2.0.2" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "80b84a66a325082740043a6c28bbea400c129eac0d3a27673a1de971e44bf1f7" 708 | dependencies = [ 709 | "anstream", 710 | "anstyle", 711 | "backtrace", 712 | "os_info", 713 | "serde", 714 | "serde_derive", 715 | "toml", 716 | "uuid", 717 | ] 718 | 719 | [[package]] 720 | name = "idna" 721 | version = "0.3.0" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 724 | dependencies = [ 725 | "unicode-bidi", 726 | "unicode-normalization", 727 | ] 728 | 729 | [[package]] 730 | name = "indenter" 731 | version = "0.3.3" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 734 | 735 | [[package]] 736 | name = "indexmap" 737 | version = "2.1.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 740 | dependencies = [ 741 | "equivalent", 742 | "hashbrown", 743 | ] 744 | 745 | [[package]] 746 | name = "instant" 747 | version = "0.1.12" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 750 | dependencies = [ 751 | "cfg-if", 752 | ] 753 | 754 | [[package]] 755 | name = "io-lifetimes" 756 | version = "1.0.9" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" 759 | dependencies = [ 760 | "hermit-abi 0.3.1", 761 | "libc", 762 | "windows-sys 0.45.0", 763 | ] 764 | 765 | [[package]] 766 | name = "is-terminal" 767 | version = "0.4.16" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 770 | dependencies = [ 771 | "hermit-abi 0.5.0", 772 | "libc", 773 | "windows-sys 0.59.0", 774 | ] 775 | 776 | [[package]] 777 | name = "is_terminal_polyfill" 778 | version = "1.70.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 781 | 782 | [[package]] 783 | name = "itertools" 784 | version = "0.14.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 787 | dependencies = [ 788 | "either", 789 | ] 790 | 791 | [[package]] 792 | name = "itoa" 793 | version = "1.0.4" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" 796 | 797 | [[package]] 798 | name = "jobserver" 799 | version = "0.1.26" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" 802 | dependencies = [ 803 | "libc", 804 | ] 805 | 806 | [[package]] 807 | name = "libc" 808 | version = "0.2.170" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 811 | 812 | [[package]] 813 | name = "libgit2-sys" 814 | version = "0.16.2+1.7.2" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" 817 | dependencies = [ 818 | "cc", 819 | "libc", 820 | "libz-sys", 821 | "pkg-config", 822 | ] 823 | 824 | [[package]] 825 | name = "libz-sys" 826 | version = "1.1.8" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" 829 | dependencies = [ 830 | "cc", 831 | "libc", 832 | "pkg-config", 833 | "vcpkg", 834 | ] 835 | 836 | [[package]] 837 | name = "linux-raw-sys" 838 | version = "0.1.3" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" 841 | 842 | [[package]] 843 | name = "linux-raw-sys" 844 | version = "0.9.3" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 847 | 848 | [[package]] 849 | name = "log" 850 | version = "0.4.27" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 853 | 854 | [[package]] 855 | name = "memchr" 856 | version = "2.5.0" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 859 | 860 | [[package]] 861 | name = "miniz_oxide" 862 | version = "0.8.5" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 865 | dependencies = [ 866 | "adler2", 867 | ] 868 | 869 | [[package]] 870 | name = "normalize-line-endings" 871 | version = "0.3.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 874 | 875 | [[package]] 876 | name = "ntapi" 877 | version = "0.4.1" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 880 | dependencies = [ 881 | "winapi", 882 | ] 883 | 884 | [[package]] 885 | name = "object" 886 | version = "0.36.5" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 889 | dependencies = [ 890 | "memchr", 891 | ] 892 | 893 | [[package]] 894 | name = "once_cell" 895 | version = "1.21.3" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 898 | 899 | [[package]] 900 | name = "onig" 901 | version = "6.4.0" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" 904 | dependencies = [ 905 | "bitflags 1.3.2", 906 | "libc", 907 | "once_cell", 908 | "onig_sys", 909 | ] 910 | 911 | [[package]] 912 | name = "onig_sys" 913 | version = "69.8.1" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" 916 | dependencies = [ 917 | "cc", 918 | "pkg-config", 919 | ] 920 | 921 | [[package]] 922 | name = "os_info" 923 | version = "3.6.0" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "5c424bc68d15e0778838ac013b5b3449544d8133633d8016319e7e05a820b8c0" 926 | dependencies = [ 927 | "log", 928 | "serde", 929 | "winapi", 930 | ] 931 | 932 | [[package]] 933 | name = "os_pipe" 934 | version = "1.1.3" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "a53dbb20faf34b16087a931834cba2d7a73cc74af2b7ef345a4c8324e2409a12" 937 | dependencies = [ 938 | "libc", 939 | "windows-sys 0.45.0", 940 | ] 941 | 942 | [[package]] 943 | name = "output_vt100" 944 | version = "0.1.3" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" 947 | dependencies = [ 948 | "winapi", 949 | ] 950 | 951 | [[package]] 952 | name = "path-clean" 953 | version = "1.0.1" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" 956 | 957 | [[package]] 958 | name = "percent-encoding" 959 | version = "2.2.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 962 | 963 | [[package]] 964 | name = "pest" 965 | version = "2.5.6" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "8cbd939b234e95d72bc393d51788aec68aeeb5d51e748ca08ff3aad58cb722f7" 968 | dependencies = [ 969 | "thiserror", 970 | "ucd-trie", 971 | ] 972 | 973 | [[package]] 974 | name = "pest_derive" 975 | version = "2.5.6" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7" 978 | dependencies = [ 979 | "pest", 980 | "pest_generator", 981 | ] 982 | 983 | [[package]] 984 | name = "pest_generator" 985 | version = "2.5.6" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b" 988 | dependencies = [ 989 | "pest", 990 | "pest_meta", 991 | "proc-macro2", 992 | "quote", 993 | "syn 1.0.109", 994 | ] 995 | 996 | [[package]] 997 | name = "pest_meta" 998 | version = "2.5.6" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80" 1001 | dependencies = [ 1002 | "once_cell", 1003 | "pest", 1004 | "sha2", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "pkg-config" 1009 | version = "0.3.26" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 1012 | 1013 | [[package]] 1014 | name = "pretty_assertions" 1015 | version = "1.3.0" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" 1018 | dependencies = [ 1019 | "ctor", 1020 | "diff", 1021 | "output_vt100", 1022 | "yansi", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "proc-exit" 1027 | version = "2.0.2" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "77a5390699eb9ac50677729fda96fb8339d4629f257cc6cfa6eaa673730f8f63" 1030 | 1031 | [[package]] 1032 | name = "proc-macro-hack" 1033 | version = "0.5.19" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 1036 | 1037 | [[package]] 1038 | name = "proc-macro2" 1039 | version = "1.0.89" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 1042 | dependencies = [ 1043 | "unicode-ident", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "quick-xml" 1048 | version = "0.28.2" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" 1051 | dependencies = [ 1052 | "memchr", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "quote" 1057 | version = "1.0.35" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 1060 | dependencies = [ 1061 | "proc-macro2", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "rayon" 1066 | version = "1.10.0" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1069 | dependencies = [ 1070 | "either", 1071 | "rayon-core", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "rayon-core" 1076 | version = "1.12.1" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1079 | dependencies = [ 1080 | "crossbeam-deque", 1081 | "crossbeam-utils", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "redox_syscall" 1086 | version = "0.2.16" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1089 | dependencies = [ 1090 | "bitflags 1.3.2", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "regex-automata" 1095 | version = "0.1.10" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1098 | 1099 | [[package]] 1100 | name = "regex-syntax" 1101 | version = "0.8.2" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 1104 | 1105 | [[package]] 1106 | name = "rustc-demangle" 1107 | version = "0.1.24" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1110 | 1111 | [[package]] 1112 | name = "rustix" 1113 | version = "0.36.4" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" 1116 | dependencies = [ 1117 | "bitflags 1.3.2", 1118 | "errno 0.2.8", 1119 | "io-lifetimes", 1120 | "libc", 1121 | "linux-raw-sys 0.1.3", 1122 | "windows-sys 0.42.0", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "rustix" 1127 | version = "1.0.3" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" 1130 | dependencies = [ 1131 | "bitflags 2.4.0", 1132 | "errno 0.3.10", 1133 | "libc", 1134 | "linux-raw-sys 0.9.3", 1135 | "windows-sys 0.59.0", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "ryu" 1140 | version = "1.0.11" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 1143 | 1144 | [[package]] 1145 | name = "same-file" 1146 | version = "1.0.6" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1149 | dependencies = [ 1150 | "winapi-util", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "serde" 1155 | version = "1.0.219" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1158 | dependencies = [ 1159 | "serde_derive", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "serde_derive" 1164 | version = "1.0.219" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1167 | dependencies = [ 1168 | "proc-macro2", 1169 | "quote", 1170 | "syn 2.0.86", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "serde_json" 1175 | version = "1.0.85" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" 1178 | dependencies = [ 1179 | "itoa", 1180 | "ryu", 1181 | "serde", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "serde_spanned" 1186 | version = "0.6.4" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" 1189 | dependencies = [ 1190 | "serde", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "sha2" 1195 | version = "0.10.6" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" 1198 | dependencies = [ 1199 | "cfg-if", 1200 | "cpufeatures", 1201 | "digest", 1202 | ] 1203 | 1204 | [[package]] 1205 | name = "shell-escape" 1206 | version = "0.1.5" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" 1209 | 1210 | [[package]] 1211 | name = "shlex" 1212 | version = "1.3.0" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1215 | 1216 | [[package]] 1217 | name = "similar" 1218 | version = "2.2.1" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" 1221 | 1222 | [[package]] 1223 | name = "smawk" 1224 | version = "0.3.2" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 1227 | 1228 | [[package]] 1229 | name = "snapbox" 1230 | version = "0.6.21" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b" 1233 | dependencies = [ 1234 | "anstream", 1235 | "anstyle", 1236 | "content_inspector", 1237 | "dunce", 1238 | "filetime", 1239 | "normalize-line-endings", 1240 | "similar", 1241 | "snapbox-macros", 1242 | "tempfile", 1243 | "walkdir", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "snapbox-macros" 1248 | version = "0.3.10" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af" 1251 | dependencies = [ 1252 | "anstream", 1253 | ] 1254 | 1255 | [[package]] 1256 | name = "strsim" 1257 | version = "0.11.0" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 1260 | 1261 | [[package]] 1262 | name = "syn" 1263 | version = "1.0.109" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1266 | dependencies = [ 1267 | "proc-macro2", 1268 | "quote", 1269 | "unicode-ident", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "syn" 1274 | version = "2.0.86" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" 1277 | dependencies = [ 1278 | "proc-macro2", 1279 | "quote", 1280 | "unicode-ident", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "syntect" 1285 | version = "5.2.0" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" 1288 | dependencies = [ 1289 | "bincode 1.3.3", 1290 | "bitflags 1.3.2", 1291 | "flate2", 1292 | "fnv", 1293 | "once_cell", 1294 | "onig", 1295 | "regex-syntax", 1296 | "serde", 1297 | "serde_derive", 1298 | "serde_json", 1299 | "thiserror", 1300 | "walkdir", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "sysinfo" 1305 | version = "0.33.1" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" 1308 | dependencies = [ 1309 | "core-foundation-sys", 1310 | "libc", 1311 | "memchr", 1312 | "ntapi", 1313 | "rayon", 1314 | "windows", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "tempfile" 1319 | version = "3.4.0" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 1322 | dependencies = [ 1323 | "cfg-if", 1324 | "fastrand", 1325 | "redox_syscall", 1326 | "rustix 0.36.4", 1327 | "windows-sys 0.42.0", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "term-transcript" 1332 | version = "0.3.0" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "8ab2925c31fe1c0cdd428dc846bccd040fbfbc628851c8949b2148e9f0ead90d" 1335 | dependencies = [ 1336 | "atty", 1337 | "bytecount", 1338 | "handlebars", 1339 | "os_pipe", 1340 | "pretty_assertions", 1341 | "quick-xml", 1342 | "serde", 1343 | "serde_json", 1344 | "termcolor", 1345 | "unicode-width 0.1.10", 1346 | ] 1347 | 1348 | [[package]] 1349 | name = "termcolor" 1350 | version = "1.1.3" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 1353 | dependencies = [ 1354 | "winapi-util", 1355 | ] 1356 | 1357 | [[package]] 1358 | name = "terminal_size" 1359 | version = "0.4.2" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" 1362 | dependencies = [ 1363 | "rustix 1.0.3", 1364 | "windows-sys 0.59.0", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "textwrap" 1369 | version = "0.16.2" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 1372 | dependencies = [ 1373 | "smawk", 1374 | "unicode-linebreak", 1375 | "unicode-width 0.2.0", 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "thiserror" 1380 | version = "1.0.37" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 1383 | dependencies = [ 1384 | "thiserror-impl", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "thiserror-impl" 1389 | version = "1.0.37" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 1392 | dependencies = [ 1393 | "proc-macro2", 1394 | "quote", 1395 | "syn 1.0.109", 1396 | ] 1397 | 1398 | [[package]] 1399 | name = "tinyvec" 1400 | version = "1.6.0" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1403 | dependencies = [ 1404 | "tinyvec_macros", 1405 | ] 1406 | 1407 | [[package]] 1408 | name = "tinyvec_macros" 1409 | version = "0.1.1" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1412 | 1413 | [[package]] 1414 | name = "toml" 1415 | version = "0.8.8" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" 1418 | dependencies = [ 1419 | "serde", 1420 | "serde_spanned", 1421 | "toml_datetime", 1422 | "toml_edit", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "toml_datetime" 1427 | version = "0.6.5" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 1430 | dependencies = [ 1431 | "serde", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "toml_edit" 1436 | version = "0.21.0" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" 1439 | dependencies = [ 1440 | "indexmap", 1441 | "serde", 1442 | "serde_spanned", 1443 | "toml_datetime", 1444 | ] 1445 | 1446 | [[package]] 1447 | name = "typenum" 1448 | version = "1.16.0" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1451 | 1452 | [[package]] 1453 | name = "ucd-trie" 1454 | version = "0.1.5" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" 1457 | 1458 | [[package]] 1459 | name = "unicode-bidi" 1460 | version = "0.3.13" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1463 | 1464 | [[package]] 1465 | name = "unicode-ident" 1466 | version = "1.0.8" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 1469 | 1470 | [[package]] 1471 | name = "unicode-linebreak" 1472 | version = "0.1.5" 1473 | source = "registry+https://github.com/rust-lang/crates.io-index" 1474 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 1475 | 1476 | [[package]] 1477 | name = "unicode-normalization" 1478 | version = "0.1.22" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1481 | dependencies = [ 1482 | "tinyvec", 1483 | ] 1484 | 1485 | [[package]] 1486 | name = "unicode-segmentation" 1487 | version = "1.12.0" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1490 | 1491 | [[package]] 1492 | name = "unicode-width" 1493 | version = "0.1.10" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1496 | 1497 | [[package]] 1498 | name = "unicode-width" 1499 | version = "0.2.0" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1502 | 1503 | [[package]] 1504 | name = "unty" 1505 | version = "0.0.4" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" 1508 | 1509 | [[package]] 1510 | name = "url" 1511 | version = "2.3.1" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1514 | dependencies = [ 1515 | "form_urlencoded", 1516 | "idna", 1517 | "percent-encoding", 1518 | ] 1519 | 1520 | [[package]] 1521 | name = "utf8parse" 1522 | version = "0.2.1" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1525 | 1526 | [[package]] 1527 | name = "uuid" 1528 | version = "1.3.0" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" 1531 | dependencies = [ 1532 | "getrandom", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "vcpkg" 1537 | version = "0.2.15" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1540 | 1541 | [[package]] 1542 | name = "version_check" 1543 | version = "0.9.4" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1546 | 1547 | [[package]] 1548 | name = "walkdir" 1549 | version = "2.3.2" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1552 | dependencies = [ 1553 | "same-file", 1554 | "winapi", 1555 | "winapi-util", 1556 | ] 1557 | 1558 | [[package]] 1559 | name = "wasi" 1560 | version = "0.11.0+wasi-snapshot-preview1" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1563 | 1564 | [[package]] 1565 | name = "winapi" 1566 | version = "0.3.9" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1569 | dependencies = [ 1570 | "winapi-i686-pc-windows-gnu", 1571 | "winapi-x86_64-pc-windows-gnu", 1572 | ] 1573 | 1574 | [[package]] 1575 | name = "winapi-i686-pc-windows-gnu" 1576 | version = "0.4.0" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1579 | 1580 | [[package]] 1581 | name = "winapi-util" 1582 | version = "0.1.5" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1585 | dependencies = [ 1586 | "winapi", 1587 | ] 1588 | 1589 | [[package]] 1590 | name = "winapi-x86_64-pc-windows-gnu" 1591 | version = "0.4.0" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1594 | 1595 | [[package]] 1596 | name = "windows" 1597 | version = "0.57.0" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" 1600 | dependencies = [ 1601 | "windows-core", 1602 | "windows-targets 0.52.6", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "windows-core" 1607 | version = "0.57.0" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" 1610 | dependencies = [ 1611 | "windows-implement", 1612 | "windows-interface", 1613 | "windows-result", 1614 | "windows-targets 0.52.6", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "windows-implement" 1619 | version = "0.57.0" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" 1622 | dependencies = [ 1623 | "proc-macro2", 1624 | "quote", 1625 | "syn 2.0.86", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "windows-interface" 1630 | version = "0.57.0" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" 1633 | dependencies = [ 1634 | "proc-macro2", 1635 | "quote", 1636 | "syn 2.0.86", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "windows-result" 1641 | version = "0.1.2" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 1644 | dependencies = [ 1645 | "windows-targets 0.52.6", 1646 | ] 1647 | 1648 | [[package]] 1649 | name = "windows-sys" 1650 | version = "0.42.0" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1653 | dependencies = [ 1654 | "windows_aarch64_gnullvm 0.42.2", 1655 | "windows_aarch64_msvc 0.42.2", 1656 | "windows_i686_gnu 0.42.2", 1657 | "windows_i686_msvc 0.42.2", 1658 | "windows_x86_64_gnu 0.42.2", 1659 | "windows_x86_64_gnullvm 0.42.2", 1660 | "windows_x86_64_msvc 0.42.2", 1661 | ] 1662 | 1663 | [[package]] 1664 | name = "windows-sys" 1665 | version = "0.45.0" 1666 | source = "registry+https://github.com/rust-lang/crates.io-index" 1667 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1668 | dependencies = [ 1669 | "windows-targets 0.42.2", 1670 | ] 1671 | 1672 | [[package]] 1673 | name = "windows-sys" 1674 | version = "0.48.0" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1677 | dependencies = [ 1678 | "windows-targets 0.48.0", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "windows-sys" 1683 | version = "0.59.0" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1686 | dependencies = [ 1687 | "windows-targets 0.52.6", 1688 | ] 1689 | 1690 | [[package]] 1691 | name = "windows-targets" 1692 | version = "0.42.2" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1695 | dependencies = [ 1696 | "windows_aarch64_gnullvm 0.42.2", 1697 | "windows_aarch64_msvc 0.42.2", 1698 | "windows_i686_gnu 0.42.2", 1699 | "windows_i686_msvc 0.42.2", 1700 | "windows_x86_64_gnu 0.42.2", 1701 | "windows_x86_64_gnullvm 0.42.2", 1702 | "windows_x86_64_msvc 0.42.2", 1703 | ] 1704 | 1705 | [[package]] 1706 | name = "windows-targets" 1707 | version = "0.48.0" 1708 | source = "registry+https://github.com/rust-lang/crates.io-index" 1709 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1710 | dependencies = [ 1711 | "windows_aarch64_gnullvm 0.48.0", 1712 | "windows_aarch64_msvc 0.48.0", 1713 | "windows_i686_gnu 0.48.0", 1714 | "windows_i686_msvc 0.48.0", 1715 | "windows_x86_64_gnu 0.48.0", 1716 | "windows_x86_64_gnullvm 0.48.0", 1717 | "windows_x86_64_msvc 0.48.0", 1718 | ] 1719 | 1720 | [[package]] 1721 | name = "windows-targets" 1722 | version = "0.52.6" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1725 | dependencies = [ 1726 | "windows_aarch64_gnullvm 0.52.6", 1727 | "windows_aarch64_msvc 0.52.6", 1728 | "windows_i686_gnu 0.52.6", 1729 | "windows_i686_gnullvm", 1730 | "windows_i686_msvc 0.52.6", 1731 | "windows_x86_64_gnu 0.52.6", 1732 | "windows_x86_64_gnullvm 0.52.6", 1733 | "windows_x86_64_msvc 0.52.6", 1734 | ] 1735 | 1736 | [[package]] 1737 | name = "windows_aarch64_gnullvm" 1738 | version = "0.42.2" 1739 | source = "registry+https://github.com/rust-lang/crates.io-index" 1740 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1741 | 1742 | [[package]] 1743 | name = "windows_aarch64_gnullvm" 1744 | version = "0.48.0" 1745 | source = "registry+https://github.com/rust-lang/crates.io-index" 1746 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1747 | 1748 | [[package]] 1749 | name = "windows_aarch64_gnullvm" 1750 | version = "0.52.6" 1751 | source = "registry+https://github.com/rust-lang/crates.io-index" 1752 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1753 | 1754 | [[package]] 1755 | name = "windows_aarch64_msvc" 1756 | version = "0.42.2" 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" 1758 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1759 | 1760 | [[package]] 1761 | name = "windows_aarch64_msvc" 1762 | version = "0.48.0" 1763 | source = "registry+https://github.com/rust-lang/crates.io-index" 1764 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1765 | 1766 | [[package]] 1767 | name = "windows_aarch64_msvc" 1768 | version = "0.52.6" 1769 | source = "registry+https://github.com/rust-lang/crates.io-index" 1770 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1771 | 1772 | [[package]] 1773 | name = "windows_i686_gnu" 1774 | version = "0.42.2" 1775 | source = "registry+https://github.com/rust-lang/crates.io-index" 1776 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1777 | 1778 | [[package]] 1779 | name = "windows_i686_gnu" 1780 | version = "0.48.0" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1783 | 1784 | [[package]] 1785 | name = "windows_i686_gnu" 1786 | version = "0.52.6" 1787 | source = "registry+https://github.com/rust-lang/crates.io-index" 1788 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1789 | 1790 | [[package]] 1791 | name = "windows_i686_gnullvm" 1792 | version = "0.52.6" 1793 | source = "registry+https://github.com/rust-lang/crates.io-index" 1794 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1795 | 1796 | [[package]] 1797 | name = "windows_i686_msvc" 1798 | version = "0.42.2" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1801 | 1802 | [[package]] 1803 | name = "windows_i686_msvc" 1804 | version = "0.48.0" 1805 | source = "registry+https://github.com/rust-lang/crates.io-index" 1806 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1807 | 1808 | [[package]] 1809 | name = "windows_i686_msvc" 1810 | version = "0.52.6" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1813 | 1814 | [[package]] 1815 | name = "windows_x86_64_gnu" 1816 | version = "0.42.2" 1817 | source = "registry+https://github.com/rust-lang/crates.io-index" 1818 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1819 | 1820 | [[package]] 1821 | name = "windows_x86_64_gnu" 1822 | version = "0.48.0" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1825 | 1826 | [[package]] 1827 | name = "windows_x86_64_gnu" 1828 | version = "0.52.6" 1829 | source = "registry+https://github.com/rust-lang/crates.io-index" 1830 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1831 | 1832 | [[package]] 1833 | name = "windows_x86_64_gnullvm" 1834 | version = "0.42.2" 1835 | source = "registry+https://github.com/rust-lang/crates.io-index" 1836 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1837 | 1838 | [[package]] 1839 | name = "windows_x86_64_gnullvm" 1840 | version = "0.48.0" 1841 | source = "registry+https://github.com/rust-lang/crates.io-index" 1842 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1843 | 1844 | [[package]] 1845 | name = "windows_x86_64_gnullvm" 1846 | version = "0.52.6" 1847 | source = "registry+https://github.com/rust-lang/crates.io-index" 1848 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1849 | 1850 | [[package]] 1851 | name = "windows_x86_64_msvc" 1852 | version = "0.42.2" 1853 | source = "registry+https://github.com/rust-lang/crates.io-index" 1854 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1855 | 1856 | [[package]] 1857 | name = "windows_x86_64_msvc" 1858 | version = "0.48.0" 1859 | source = "registry+https://github.com/rust-lang/crates.io-index" 1860 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1861 | 1862 | [[package]] 1863 | name = "windows_x86_64_msvc" 1864 | version = "0.52.6" 1865 | source = "registry+https://github.com/rust-lang/crates.io-index" 1866 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1867 | 1868 | [[package]] 1869 | name = "winnow" 1870 | version = "0.7.3" 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" 1872 | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" 1873 | dependencies = [ 1874 | "memchr", 1875 | ] 1876 | 1877 | [[package]] 1878 | name = "yansi" 1879 | version = "0.5.1" 1880 | source = "registry+https://github.com/rust-lang/crates.io-index" 1881 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 1882 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | [workspace.package] 5 | repository = "https://github.com/gitext-rs/git-dive" 6 | license = "MIT OR Apache-2.0" 7 | edition = "2021" 8 | rust-version = "1.85" # MSRV 9 | include = [ 10 | "build.rs", 11 | "src/**/*", 12 | "Cargo.toml", 13 | "Cargo.lock", 14 | "LICENSE*", 15 | "README.md", 16 | "benches/**/*", 17 | "examples/**/*" 18 | ] 19 | 20 | [workspace.lints.rust] 21 | rust_2018_idioms = { level = "warn", priority = -1 } 22 | unreachable_pub = "warn" 23 | unsafe_op_in_unsafe_fn = "warn" 24 | unused_lifetimes = "warn" 25 | unused_macro_rules = "warn" 26 | unused_qualifications = "warn" 27 | 28 | [workspace.lints.clippy] 29 | bool_assert_comparison = "allow" 30 | branches_sharing_code = "allow" 31 | checked_conversions = "warn" 32 | collapsible_else_if = "allow" 33 | create_dir = "warn" 34 | dbg_macro = "warn" 35 | debug_assert_with_mut_call = "warn" 36 | doc_markdown = "warn" 37 | empty_enum = "warn" 38 | enum_glob_use = "warn" 39 | expl_impl_clone_on_copy = "warn" 40 | explicit_deref_methods = "warn" 41 | explicit_into_iter_loop = "warn" 42 | fallible_impl_from = "warn" 43 | filter_map_next = "warn" 44 | flat_map_option = "warn" 45 | float_cmp_const = "warn" 46 | fn_params_excessive_bools = "warn" 47 | from_iter_instead_of_collect = "warn" 48 | if_same_then_else = "allow" 49 | implicit_clone = "warn" 50 | imprecise_flops = "warn" 51 | inconsistent_struct_constructor = "warn" 52 | inefficient_to_string = "warn" 53 | infinite_loop = "warn" 54 | invalid_upcast_comparisons = "warn" 55 | large_digit_groups = "warn" 56 | large_stack_arrays = "warn" 57 | large_types_passed_by_value = "warn" 58 | let_and_return = "allow" # sometimes good to name what you are returning 59 | linkedlist = "warn" 60 | lossy_float_literal = "warn" 61 | macro_use_imports = "warn" 62 | mem_forget = "warn" 63 | mutex_integer = "warn" 64 | needless_continue = "warn" 65 | needless_for_each = "warn" 66 | negative_feature_names = "warn" 67 | path_buf_push_overwrite = "warn" 68 | ptr_as_ptr = "warn" 69 | rc_mutex = "warn" 70 | redundant_feature_names = "warn" 71 | ref_option_ref = "warn" 72 | rest_pat_in_fully_bound_structs = "warn" 73 | result_large_err = "allow" 74 | same_functions_in_if_condition = "warn" 75 | self_named_module_files = "warn" 76 | semicolon_if_nothing_returned = "warn" 77 | str_to_string = "warn" 78 | string_add = "warn" 79 | string_add_assign = "warn" 80 | string_lit_as_bytes = "warn" 81 | string_to_string = "warn" 82 | todo = "warn" 83 | trait_duplication_in_bounds = "warn" 84 | uninlined_format_args = "warn" 85 | verbose_file_reads = "warn" 86 | wildcard_imports = "warn" 87 | zero_sized_map_values = "warn" 88 | 89 | [package] 90 | name = "git-dive" 91 | description = "Dive into a file's history to find root cause" 92 | version = "0.1.6" 93 | documentation = "https://github.com/gitext-rs/git-dive" 94 | readme = "README.md" 95 | categories = ["command-line-interface", "development-tools"] 96 | keywords = ["git", "cli"] 97 | repository.workspace = true 98 | license.workspace = true 99 | edition.workspace = true 100 | rust-version.workspace = true 101 | include.workspace = true 102 | 103 | [package.metadata.docs.rs] 104 | all-features = true 105 | rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] 106 | 107 | [package.metadata.release] 108 | pre-release-replacements = [ 109 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, 110 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 111 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, 112 | {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, 113 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/gitext-rs/git-dive/compare/{{tag_name}}...HEAD", exactly=1}, 114 | ] 115 | 116 | [dependencies] 117 | git2 = { version = ">=0.18, <=0.20", default-features = false } 118 | clap = { version = "4.5.4", features = ["derive", "wrap_help"] } 119 | clap-verbosity-flag = "3.0.0" 120 | log = "0.4.21" 121 | env_logger = { version = "0.11.3", default-features = false, features = ["color"] } 122 | colorchoice-clap = "1.0.3" 123 | proc-exit = "2.0.1" 124 | human-panic = "2.0.0" 125 | anyhow = "1.0.82" 126 | syntect = { version = "5.2.0", default-features = false, features = ["parsing", "regex-onig"] } 127 | terminal_size = "0.4.0" 128 | textwrap = "0.16.1" 129 | anstyle = "1.0.6" 130 | anstream = "0.6.13" 131 | is-terminal = "0.4.12" 132 | content_inspector = "0.2.4" 133 | git-config-env = "0.2.1" 134 | shlex = "1.3.0" 135 | anstyle-syntect = "1.0.0" 136 | bincode = { version = "2.0", default-features = false, features = ["std", "serde"] } 137 | serde = { version = "1.0.199", features = ["derive"] } 138 | flate2 = "1.0.30" 139 | once_cell = "1.19.0" 140 | bugreport = "0.5.0" 141 | dunce = "1.0.4" 142 | encoding_rs = "0.8.34" 143 | path-clean = "1.0.1" 144 | 145 | [dev-dependencies] 146 | git-fixture = "0.3.4" 147 | snapbox = { version = "0.6.0", features = ["dir"] } 148 | term-transcript = "0.3.0" 149 | automod = "1.0.14" 150 | 151 | [features] 152 | default = ["vendored-libgit2"] 153 | vendored-libgit2 = ["git2/vendored-libgit2"] 154 | 155 | [profile.dev] 156 | panic = "abort" 157 | 158 | [profile.release] 159 | panic = "abort" 160 | codegen-units = 1 161 | lto = true 162 | 163 | [lints] 164 | workspace = true 165 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-dive 2 | 3 | > **Dive into a file's history to find root cause** 4 | 5 | ![Screenshot](./assets/screenshot.svg) 6 | 7 | [![codecov](https://codecov.io/gh/gitext-rs/git-dive/branch/master/graph/badge.svg)](https://codecov.io/gh/gitext-rs/git-dive) 8 | [![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] 9 | ![License](https://img.shields.io/crates/l/git-dive.svg) 10 | [![Crates Status](https://img.shields.io/crates/v/git-dive.svg)][Crates.io] 11 | 12 | Dual-licensed under [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE) 13 | 14 | ## Documentation 15 | 16 | - [About](#about) 17 | - [Install](#install) 18 | - [Getting Started](#getting-started) 19 | - [FAQ](#faq) 20 | - [Comparison](docs/comparison.md) 21 | - [Contribute](CONTRIBUTING.md) 22 | - [CHANGELOG](CHANGELOG.md) 23 | 24 | ## About 25 | 26 | `git-dive` is for better understanding why a change was made. Frequently, we 27 | work on code bases we didn't start which have too little documentation. Even 28 | worse if the original authors are not around. `git-blame` is an invaluable 29 | tool for this but it requires a lot of ceremony to get the information you 30 | need. 31 | 32 | Features 33 | - Git-native experience 34 | - Syntax highlighting 35 | - Focuses on relative references (e.g. `HEAD~10`) 36 | - More room for code by merging the SHA and Time columns into a rev column 37 | - Easier to compare timestamps via the rev column (e.g. `HEAD~10`) 38 | - Easier to remember, avoiding the need for copy/pasting SHAs 39 | - Focuses on "blaming" merge-commits (PRs) to more quickly see the whole context for a change 40 | - Easy to find relevant config with `git dive --dump-config -` 41 | 42 | Planned Features 43 | - [Interactive pager that let's you browse through time](https://github.com/gitext-rs/git-dive/milestone/2) 44 | - [Reverse blame](https://github.com/gitext-rs/git-dive/issues/21) 45 | 46 | `git-dive` was inspired by [perforce time lapse 47 | view](https://www.perforce.com/video-tutorials/vcs/using-time-lapse-view). 48 | 49 | ## Install 50 | 51 | [Download](https://github.com/gitext-rs/git-dive/releases) a pre-built binary 52 | (installable via [gh-install](https://github.com/crate-ci/gh-install)). 53 | 54 | Or use rust to install: 55 | ```console 56 | $ cargo install git-dive 57 | ``` 58 | 59 | ### Uninstall 60 | 61 | See the uninstall method for your installer. 62 | 63 | ## Getting Started 64 | 65 | ## FAQ 66 | 67 | [Crates.io]: https://crates.io/crates/git-dive 68 | [Documentation]: https://docs.rs/git-dive 69 | -------------------------------------------------------------------------------- /assets/acknowledgements.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitext-rs/git-dive/6eea4ad7e627771796299dc5571a7a067ebe4dd6/assets/acknowledgements.bin -------------------------------------------------------------------------------- /assets/screenshot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ git-dive Cargo.toml
72 |
HEAD~81  1 │ [package]
 73 | ⋮        2 │ name = "git-dive"
 74 | HEAD~80  3 │ description = "Dive into a file's history to find root cause"
 75 | ⋮        4 │ version = "0.0.1"
 76 | ⋮        5 │ license = "MIT OR Apache-2.0"
 77 | ⋮        6 │ repository = "https://github.com/gitext-rs/git-dive"
 78 | ⋮        7 │ documentation = "https://github.com/gitext-rs/git-dive"
 79 | ⋮        8 │ readme = "README.md"
 80 | ⋮        9 │ categories = ["command-line-interface", "development-tools"]
 81 | ⋮       10 │ keywords = ["git", "cli"]
 82 | HEAD~81 11 │ edition = "2021"
 83 | HEAD~6  12 │ rust-version = "1.64.0"  # MSRV
 84 | HEAD~80 13 │ include = [
 85 | ⋮       14 │   "src/**/*",
 86 | ⋮       15 │   "Cargo.toml",
 87 | ⋮       16 │   "LICENSE*",
 88 | ⋮       17 │   "README.md",
 89 | ⋮       18 │   "examples/**/*"
 90 | ⋮       19 │ ]
 91 | HEAD~81 20 │ 
 92 | HEAD~80 21 │ [package.metadata.release]
 93 | ⋮       22 │ pre-release-replacements = [
 94 | ⋮       23 │   {file="CHANGELOG.md", search="Unreleased",
 95 | ⋮       24 │   {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...
 96 | ⋮       25 │   {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}",
 97 | ⋮       26 │   {file="CHANGELOG.md", search="<!-- next-header -->",
 98 | ⋮       27 │   {file="CHANGELOG.md", search="<!-- next-url -->", replace="<!--
 99 | ⋮       28 │ ]
100 | HEAD~81 29 │ 
101 | ⋮       30 │ [dependencies]
102 | HEAD~79 31 │ git2 = { version = "0.15", default-features = false, features =
103 | HEAD~4  32 │ clap = { version = "4.1.4", features = ["derive", "wrap_help"] }
104 | HEAD~44 33 │ clap-verbosity-flag = "2.0"
105 | HEAD~80 34 │ log = "0.4"
106 | HEAD~17 35 │ env_logger = { version = "0.10", default-features = false,
107 | HEAD    36 │ concolor = "0.1.1"
108 | ⋮       37 │ concolor-clap = { version = "0.1.0", features = ["api"] }
109 | HEAD~40 38 │ proc-exit = "2"
110 | HEAD~80 39 │ human-panic = "1"
111 | HEAD~9  40 │ anyhow = "1.0.68"
112 | HEAD~30 41 │ syntect = { version = "5.0.0", default-features = false, features
113 | HEAD~17 42 │ terminal_size = "0.2.3"
114 | HEAD~26 43 │ textwrap = "0.16.0"
115 | HEAD    44 │ anstyle = "0.3.1"
116 | ⋮       45 │ anstyle-stream = "0.2.2"
117 | ⋮       46 │ is-terminal = "0.4.4"
118 | HEAD~65 47 │ content_inspector = "0.2.4"
119 | ⋮       48 │ encoding = "0.2.33"
120 | HEAD~41 49 │ git-config-env = "0.1.2"
121 | HEAD~45 50 │ shlex = "1.1.0"
122 | HEAD    51 │ anstyle-syntect = "0.2.0"
123 | HEAD~30 52 │ bincode = "1.3.3"
124 | HEAD~9  53 │ serde = { version = "1.0.152", features = ["derive"] }
125 | HEAD~20 54 │ flate2 = "1.0.25"
126 | HEAD~9  55 │ once_cell = "1.17.0"
127 | HEAD~29 56 │ bugreport = "0.5.0"
128 |
129 |
130 |
131 |
132 | -------------------------------------------------------------------------------- /assets/syntaxes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitext-rs/git-dive/6eea4ad7e627771796299dc5571a7a067ebe4dd6/assets/syntaxes.bin -------------------------------------------------------------------------------- /assets/theme_preview.rs: -------------------------------------------------------------------------------- 1 | // Output the square of a number. 2 | fn print_square(num: f64) { 3 | let result = f64::powf(num, 2.0); 4 | println!("The square of {:.2} is {:.2}.", num, result); 5 | } 6 | -------------------------------------------------------------------------------- /assets/themes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitext-rs/git-dive/6eea4ad7e627771796299dc5571a7a067ebe4dd6/assets/themes.bin -------------------------------------------------------------------------------- /committed.toml: -------------------------------------------------------------------------------- 1 | style="conventional" 2 | ignore_author_re="(dependabot|renovate)" 3 | merge_commit = false 4 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # Note that all fields that take a lint level have these possible values: 2 | # * deny - An error will be produced and the check will fail 3 | # * warn - A warning will be produced, but the check will not fail 4 | # * allow - No warning or error will be produced, though in some cases a note 5 | # will be 6 | 7 | # Root options 8 | 9 | # The graph table configures how the dependency graph is constructed and thus 10 | # which crates the checks are performed against 11 | [graph] 12 | # If 1 or more target triples (and optionally, target_features) are specified, 13 | # only the specified targets will be checked when running `cargo deny check`. 14 | # This means, if a particular package is only ever used as a target specific 15 | # dependency, such as, for example, the `nix` crate only being used via the 16 | # `target_family = "unix"` configuration, that only having windows targets in 17 | # this list would mean the nix crate, as well as any of its exclusive 18 | # dependencies not shared by any other crates, would be ignored, as the target 19 | # list here is effectively saying which targets you are building for. 20 | targets = [ 21 | # The triple can be any string, but only the target triples built in to 22 | # rustc (as of 1.40) can be checked against actual config expressions 23 | #"x86_64-unknown-linux-musl", 24 | # You can also specify which target_features you promise are enabled for a 25 | # particular target. target_features are currently not validated against 26 | # the actual valid features supported by the target architecture. 27 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 28 | ] 29 | # When creating the dependency graph used as the source of truth when checks are 30 | # executed, this field can be used to prune crates from the graph, removing them 31 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 32 | # is pruned from the graph, all of its dependencies will also be pruned unless 33 | # they are connected to another crate in the graph that hasn't been pruned, 34 | # so it should be used with care. The identifiers are [Package ID Specifications] 35 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 36 | #exclude = [] 37 | # If true, metadata will be collected with `--all-features`. Note that this can't 38 | # be toggled off if true, if you want to conditionally enable `--all-features` it 39 | # is recommended to pass `--all-features` on the cmd line instead 40 | all-features = false 41 | # If true, metadata will be collected with `--no-default-features`. The same 42 | # caveat with `all-features` applies 43 | no-default-features = false 44 | # If set, these feature will be enabled when collecting metadata. If `--features` 45 | # is specified on the cmd line they will take precedence over this option. 46 | #features = [] 47 | 48 | # The output table provides options for how/if diagnostics are outputted 49 | [output] 50 | # When outputting inclusion graphs in diagnostics that include features, this 51 | # option can be used to specify the depth at which feature edges will be added. 52 | # This option is included since the graphs can be quite large and the addition 53 | # of features from the crate(s) to all of the graph roots can be far too verbose. 54 | # This option can be overridden via `--feature-depth` on the cmd line 55 | feature-depth = 1 56 | 57 | # This section is considered when running `cargo deny check advisories` 58 | # More documentation for the advisories section can be found here: 59 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 60 | [advisories] 61 | # The path where the advisory databases are cloned/fetched into 62 | #db-path = "$CARGO_HOME/advisory-dbs" 63 | # The url(s) of the advisory databases to use 64 | #db-urls = ["https://github.com/rustsec/advisory-db"] 65 | # A list of advisory IDs to ignore. Note that ignored advisories will still 66 | # output a note when they are encountered. 67 | ignore = [ 68 | #"RUSTSEC-0000-0000", 69 | #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, 70 | #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish 71 | #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, 72 | ] 73 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 74 | # If this is false, then it uses a built-in git library. 75 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 76 | # See Git Authentication for more information about setting up git authentication. 77 | #git-fetch-with-cli = true 78 | 79 | # This section is considered when running `cargo deny check licenses` 80 | # More documentation for the licenses section can be found here: 81 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 82 | [licenses] 83 | # List of explicitly allowed licenses 84 | # See https://spdx.org/licenses/ for list of possible licenses 85 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 86 | allow = [ 87 | "MIT", 88 | "MIT-0", 89 | "Apache-2.0", 90 | "BSD-2-Clause", 91 | "BSD-3-Clause", 92 | "MPL-2.0", 93 | "Unicode-DFS-2016", 94 | "CC0-1.0", 95 | "ISC", 96 | "BSD-2-Clause", 97 | "OpenSSL", 98 | ] 99 | # The confidence threshold for detecting a license from license text. 100 | # The higher the value, the more closely the license text must be to the 101 | # canonical license text of a valid SPDX license file. 102 | # [possible values: any between 0.0 and 1.0]. 103 | confidence-threshold = 0.8 104 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 105 | # aren't accepted for every possible crate as with the normal allow list 106 | exceptions = [ 107 | # Each entry is the crate and version constraint, and its specific allow 108 | # list 109 | #{ allow = ["Zlib"], crate = "adler32" }, 110 | ] 111 | 112 | # Some crates don't have (easily) machine readable licensing information, 113 | # adding a clarification entry for it allows you to manually specify the 114 | # licensing information 115 | [[licenses.clarify]] 116 | # The package spec the clarification applies to 117 | crate = "ring" 118 | # The SPDX expression for the license requirements of the crate 119 | expression = "MIT AND ISC AND OpenSSL" 120 | # One or more files in the crate's source used as the "source of truth" for 121 | # the license expression. If the contents match, the clarification will be used 122 | # when running the license check, otherwise the clarification will be ignored 123 | # and the crate will be checked normally, which may produce warnings or errors 124 | # depending on the rest of your configuration 125 | license-files = [ 126 | # Each entry is a crate relative path, and the (opaque) hash of its contents 127 | { path = "LICENSE", hash = 0xbd0eed23 } 128 | ] 129 | 130 | [licenses.private] 131 | # If true, ignores workspace crates that aren't published, or are only 132 | # published to private registries. 133 | # To see how to mark a crate as unpublished (to the official registry), 134 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 135 | ignore = true 136 | # One or more private registries that you might publish crates to, if a crate 137 | # is only published to private registries, and ignore is true, the crate will 138 | # not have its license(s) checked 139 | registries = [ 140 | #"https://sekretz.com/registry 141 | ] 142 | 143 | # This section is considered when running `cargo deny check bans`. 144 | # More documentation about the 'bans' section can be found here: 145 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 146 | [bans] 147 | # Lint level for when multiple versions of the same crate are detected 148 | multiple-versions = "warn" 149 | # Lint level for when a crate version requirement is `*` 150 | wildcards = "allow" 151 | # The graph highlighting used when creating dotgraphs for crates 152 | # with multiple versions 153 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 154 | # * simplest-path - The path to the version with the fewest edges is highlighted 155 | # * all - Both lowest-version and simplest-path are used 156 | highlight = "all" 157 | # The default lint level for `default` features for crates that are members of 158 | # the workspace that is being checked. This can be overridden by allowing/denying 159 | # `default` on a crate-by-crate basis if desired. 160 | workspace-default-features = "allow" 161 | # The default lint level for `default` features for external crates that are not 162 | # members of the workspace. This can be overridden by allowing/denying `default` 163 | # on a crate-by-crate basis if desired. 164 | external-default-features = "allow" 165 | # List of crates that are allowed. Use with care! 166 | allow = [ 167 | #"ansi_term@0.11.0", 168 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, 169 | ] 170 | # List of crates to deny 171 | deny = [ 172 | #"ansi_term@0.11.0", 173 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, 174 | # Wrapper crates can optionally be specified to allow the crate when it 175 | # is a direct dependency of the otherwise banned crate 176 | #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, 177 | ] 178 | 179 | # List of features to allow/deny 180 | # Each entry the name of a crate and a version range. If version is 181 | # not specified, all versions will be matched. 182 | #[[bans.features]] 183 | #crate = "reqwest" 184 | # Features to not allow 185 | #deny = ["json"] 186 | # Features to allow 187 | #allow = [ 188 | # "rustls", 189 | # "__rustls", 190 | # "__tls", 191 | # "hyper-rustls", 192 | # "rustls", 193 | # "rustls-pemfile", 194 | # "rustls-tls-webpki-roots", 195 | # "tokio-rustls", 196 | # "webpki-roots", 197 | #] 198 | # If true, the allowed features must exactly match the enabled feature set. If 199 | # this is set there is no point setting `deny` 200 | #exact = true 201 | 202 | # Certain crates/versions that will be skipped when doing duplicate detection. 203 | skip = [ 204 | #"ansi_term@0.11.0", 205 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, 206 | ] 207 | # Similarly to `skip` allows you to skip certain crates during duplicate 208 | # detection. Unlike skip, it also includes the entire tree of transitive 209 | # dependencies starting at the specified crate, up to a certain depth, which is 210 | # by default infinite. 211 | skip-tree = [ 212 | #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies 213 | #{ crate = "ansi_term@0.11.0", depth = 20 }, 214 | ] 215 | 216 | # This section is considered when running `cargo deny check sources`. 217 | # More documentation about the 'sources' section can be found here: 218 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 219 | [sources] 220 | # Lint level for what to happen when a crate from a crate registry that is not 221 | # in the allow list is encountered 222 | unknown-registry = "deny" 223 | # Lint level for what to happen when a crate from a git repository that is not 224 | # in the allow list is encountered 225 | unknown-git = "deny" 226 | # List of URLs for allowed crate registries. Defaults to the crates.io index 227 | # if not specified. If it is specified but empty, no registries are allowed. 228 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 229 | # List of URLs for allowed Git repositories 230 | allow-git = [] 231 | 232 | [sources.allow-org] 233 | # 1 or more github.com organizations to allow git sources for 234 | github = [] 235 | # 1 or more gitlab.com organizations to allow git sources for 236 | gitlab = [] 237 | # 1 or more bitbucket.org organizations to allow git sources for 238 | bitbucket = [] 239 | -------------------------------------------------------------------------------- /docs/comparison.md: -------------------------------------------------------------------------------- 1 | # Related Blame Tools 2 | 3 | ## Perforce Time Lapse View 4 | 5 | This GUI is bundled with the P4V client for Perforce SCM 6 | 7 | Blame View 8 | - Shows when hunks are last and next modified 9 | - By default stays within the current branch but can follow back to previous branches 10 | - Colors hunk by age 11 | - Visualizer for how long hunks have existed in file 12 | 13 | Incremental Diff View 14 | - Shows something like `git diff HEAD~..HEAD` but can slide backwards 15 | 16 | Diff View 17 | - Shows diff between two specified commits with slides for each commit 18 | 19 | See 20 | - [Documentation](https://www.perforce.com/manuals/p4v/Content/P4V/advanced_files.timelapse.html) 21 | - [Video: Tutorial](https://www.perforce.com/video-tutorials/vcs/using-time-lapse-view) 22 | 23 | ## `git blame` 24 | 25 | A CLI for showing a file with line numbers and annotations for when a change was introduced 26 | - Supports ignoring specific revisions via [blame.ignoreRevsFile](https://git-scm.com/docs/git-config#Documentation/git-config.txt-blameignoreRevsFile) 27 | - To investigate the commit, requires dropping out and doing `git show ` 28 | - If the commit isn't of interest, requires running `git blame ~ ` which will fail if the file had moved 29 | 30 | ## git "pickaxe" 31 | 32 | A CLI that shows when a change was introduced or remove so long as your search is correct 33 | 34 | See 35 | - [Why Git blame sucks for understanding WTF code (and what to use instead)](https://tekin.co.uk/2020/11/patterns-for-searching-git-revision-histories?utm_source=Reddit) 36 | 37 | ## DeepGit 38 | 39 | A blame GUI that let's you click through to look further back then a commit. 40 | 41 | Includes heuristics for detecting moves of code, showing level of confidence. 42 | 43 | See [DeepGit](https://www.syntevo.com/deepgit/) 44 | 45 | ## Git Tower 46 | 47 | A git GUI that includes `git blame` support 48 | 49 | Let's you click to get info about a commit but doesn't seem to have any other investigation features. 50 | 51 | See [Git Tower: Blame Window](https://www.git-tower.com/help/guides/commit-history/blame/windows) 52 | 53 | ## CLion 54 | 55 | An IDE that includes a GUI for `git blame` 56 | 57 | - Colors commits differently 58 | - Syntax highlighting 59 | - Click on commit to see `git show` and to jump to the commit 60 | - Can jump to a previous revision 61 | - Can hide revisions 62 | - A GUI hint that this happens 63 | - Can be turned on/off 64 | 65 | See [CLion: "Locate Code Author"](https://www.jetbrains.com/help/clion/investigate-changes.html#annotate_blame) 66 | 67 | ## Tig 68 | 69 | A git TUI that includes a `git blame` view 70 | 71 | No details on website to see how powerful it is 72 | 73 | See [Tig](https://jonas.github.io/tig/doc/manual.html) 74 | 75 | ## git spelunk 76 | 77 | A TUI for showing a file with line numbers and annotations for when a change was introduced 78 | - When a hunk is selected, it highlights all related hunks 79 | - `[` and `]` to move through history 80 | - Has a `git show` pane 81 | - `less`-like searching through the file 82 | 83 | See [git spelunk](https://github.com/osheroff/git-spelunk) 84 | 85 | ## git word-blame 86 | 87 | A tool to show the commit for each word 88 | 89 | See [git word-blame](https://framagit.org/mdamien/git-word-blame/) 90 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["main"] 2 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | #[derive(clap::Parser)] 2 | #[command(about, author, version)] 3 | #[command(allow_missing_positional = true)] 4 | #[command(group = clap::ArgGroup::new("mode").multiple(false).required(true))] 5 | pub(crate) struct Args { 6 | #[arg(default_value = "HEAD")] 7 | pub(crate) rev: String, 8 | 9 | #[arg(required = true, group = "mode")] 10 | pub(crate) file: Option, 11 | 12 | #[arg(long, value_name = "PATH", group = "mode")] 13 | pub(crate) dump_config: Option, 14 | 15 | /// Display all supported languages 16 | #[arg(long, group = "mode")] 17 | pub(crate) list_languages: bool, 18 | 19 | /// Display all supported highlighting themes 20 | #[arg(long, group = "mode")] 21 | pub(crate) list_themes: bool, 22 | 23 | /// Display acknowledgements 24 | #[arg(long, hide_short_help = true, group = "mode")] 25 | pub(crate) acknowledgements: bool, 26 | 27 | /// Display information for bug reports. 28 | #[arg(long, hide_short_help = true, group = "mode")] 29 | pub(crate) diagnostic: bool, 30 | 31 | /// Run as if git was started in `PATH` instead of the current working directory. 32 | /// 33 | /// When multiple `-C` options are given, each subsequent 34 | /// non-absolute `-C ` is interpreted relative to the preceding `-C `. If `` is present but empty, e.g. `-C ""`, then the 35 | /// current working directory is left unchanged. 36 | /// 37 | /// This option affects options that expect path name like `--git-dir` and `--work-tree` in that their interpretations of the path names 38 | /// would be made relative to the working directory caused by the `-C` option. For example the following invocations are equivalent: 39 | /// 40 | /// git --git-dir=a.git --work-tree=b -C c status 41 | /// git --git-dir=c/a.git --work-tree=c/b status 42 | #[arg(short = 'C', hide = true, value_name = "PATH")] 43 | pub(crate) current_dir: Option>, 44 | 45 | #[command(flatten)] 46 | pub(crate) color: colorchoice_clap::Color, 47 | 48 | #[command(flatten)] 49 | pub(crate) verbose: clap_verbosity_flag::Verbosity, 50 | } 51 | 52 | #[cfg(test)] 53 | mod test { 54 | use super::*; 55 | 56 | #[test] 57 | fn verify_app() { 58 | use clap::CommandFactory; 59 | Args::command().debug_assert(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/lazy_theme_set.rs: -------------------------------------------------------------------------------- 1 | use super::{asset_from_contents, Error, Result, COMPRESS_LAZY_THEMES}; 2 | 3 | use std::collections::BTreeMap; 4 | use std::convert::TryFrom; 5 | 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use once_cell::unsync::OnceCell; 10 | 11 | use syntect::highlighting::{Theme, ThemeSet}; 12 | 13 | /// Same structure as a [`syntect::highlighting::ThemeSet`] but with themes 14 | /// stored in raw serialized form, and deserialized on demand. 15 | #[derive(Debug, Default, Serialize, Deserialize)] 16 | pub struct LazyThemeSet { 17 | /// This is a [`BTreeMap`] because that's what [`syntect::highlighting::ThemeSet`] uses 18 | themes: BTreeMap, 19 | } 20 | 21 | /// Stores raw serialized data for a theme with methods to lazily deserialize 22 | /// (load) the theme. 23 | #[derive(Debug, Serialize, Deserialize)] 24 | struct LazyTheme { 25 | serialized: Vec, 26 | 27 | #[serde(skip, default = "OnceCell::new")] 28 | deserialized: OnceCell, 29 | } 30 | 31 | impl LazyThemeSet { 32 | /// Lazily load the given theme 33 | pub fn get(&self, name: &str) -> Option<&Theme> { 34 | self.themes.get(name).and_then(|lazy_theme| { 35 | lazy_theme 36 | .deserialized 37 | .get_or_try_init(|| lazy_theme.deserialize()) 38 | .ok() 39 | }) 40 | } 41 | 42 | /// Returns the name of all themes. 43 | pub fn themes(&self) -> impl Iterator { 44 | self.themes.keys().map(|name| name.as_ref()) 45 | } 46 | } 47 | 48 | impl LazyTheme { 49 | fn deserialize(&self) -> Result { 50 | asset_from_contents( 51 | &self.serialized[..], 52 | "lazy-loaded theme", 53 | COMPRESS_LAZY_THEMES, 54 | ) 55 | } 56 | } 57 | 58 | impl TryFrom for ThemeSet { 59 | type Error = Error; 60 | 61 | /// Since the user might want to add custom themes to bat, we need a way to 62 | /// convert from a `LazyThemeSet` to a regular [`ThemeSet`] so that more 63 | /// themes can be added. This function does that pretty straight-forward 64 | /// conversion. 65 | fn try_from(lazy_theme_set: LazyThemeSet) -> Result { 66 | let mut theme_set = ThemeSet::default(); 67 | 68 | for (name, lazy_theme) in lazy_theme_set.themes { 69 | theme_set.themes.insert(name, lazy_theme.deserialize()?); 70 | } 71 | 72 | Ok(theme_set) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/assets/mod.rs: -------------------------------------------------------------------------------- 1 | mod lazy_theme_set; 2 | 3 | use anyhow::Error; 4 | use anyhow::Result; 5 | 6 | pub(crate) use lazy_theme_set::LazyThemeSet; 7 | 8 | pub(crate) fn load_themes() -> LazyThemeSet { 9 | get_integrated_themeset() 10 | } 11 | 12 | pub(crate) fn load_syntaxes() -> syntect::parsing::SyntaxSet { 13 | from_binary(get_serialized_integrated_syntaxset(), COMPRESS_SYNTAXES) 14 | } 15 | 16 | pub(crate) fn to_anstyle_color(color: syntect::highlighting::Color) -> Option { 17 | if color.a == 0 { 18 | // Themes can specify one of the user-configurable terminal colors by 19 | // encoding them as #RRGGBBAA with AA set to 00 (transparent) and RR set 20 | // to the 8-bit color palette number. The built-in themes ansi, base16, 21 | // and base16-256 use this. 22 | Some(match color.r { 23 | // For the first 8 colors, use the Color enum to produce ANSI escape 24 | // sequences using codes 30-37 (foreground) and 40-47 (background). 25 | // For example, red foreground is \x1b[31m. This works on terminals 26 | // without 256-color support. 27 | 0x00 => anstyle::AnsiColor::Black.into(), 28 | 0x01 => anstyle::AnsiColor::Red.into(), 29 | 0x02 => anstyle::AnsiColor::Green.into(), 30 | 0x03 => anstyle::AnsiColor::Yellow.into(), 31 | 0x04 => anstyle::AnsiColor::Blue.into(), 32 | 0x05 => anstyle::AnsiColor::Magenta.into(), 33 | 0x06 => anstyle::AnsiColor::Cyan.into(), 34 | 0x07 => anstyle::AnsiColor::White.into(), 35 | // For all other colors, use Fixed to produce escape sequences using 36 | // codes 38;5 (foreground) and 48;5 (background). For example, 37 | // bright red foreground is \x1b[38;5;9m. This only works on 38 | // terminals with 256-color support. 39 | // 40 | // TODO: When ansi_term adds support for bright variants using codes 41 | // 90-97 (foreground) and 100-107 (background), we should use those 42 | // for values 0x08 to 0x0f and only use Fixed for 0x10 to 0xff. 43 | n => anstyle::Ansi256Color(n).into(), 44 | }) 45 | } else if color.a == 1 { 46 | // Themes can specify the terminal's default foreground/background color 47 | // (i.e. no escape sequence) using the encoding #RRGGBBAA with AA set to 48 | // 01. The built-in theme ansi uses this. 49 | None 50 | } else { 51 | Some(anstyle::RgbColor(color.r, color.g, color.b).into()) 52 | } 53 | } 54 | 55 | /// Lazy-loaded syntaxes are already compressed, and we don't want to compress 56 | /// already compressed data. 57 | const COMPRESS_SYNTAXES: bool = false; 58 | 59 | /// We don't want to compress our [`LazyThemeSet`] since the lazy-loaded themes 60 | /// within it are already compressed, and compressing another time just makes 61 | /// performance suffer 62 | const COMPRESS_THEMES: bool = false; 63 | 64 | /// Compress for size of ~40 kB instead of ~200 kB without much difference in 65 | /// performance due to lazy-loading 66 | const COMPRESS_LAZY_THEMES: bool = true; 67 | 68 | /// Compress for size of ~10 kB instead of ~120 kB 69 | const COMPRESS_ACKNOWLEDGEMENTS: bool = true; 70 | 71 | fn get_serialized_integrated_syntaxset() -> &'static [u8] { 72 | include_bytes!("../../assets/syntaxes.bin") 73 | } 74 | 75 | fn get_integrated_themeset() -> LazyThemeSet { 76 | from_binary(include_bytes!("../../assets/themes.bin"), COMPRESS_THEMES) 77 | } 78 | 79 | pub(crate) fn get_acknowledgements() -> String { 80 | from_binary( 81 | include_bytes!("../../assets/acknowledgements.bin"), 82 | COMPRESS_ACKNOWLEDGEMENTS, 83 | ) 84 | } 85 | 86 | fn from_binary(v: &[u8], compressed: bool) -> T { 87 | asset_from_contents(v, "n/a", compressed) 88 | .expect("data integrated in binary is never faulty, but make sure `compressed` is in sync!") 89 | } 90 | 91 | fn asset_from_contents( 92 | contents: &[u8], 93 | description: &str, 94 | compressed: bool, 95 | ) -> Result { 96 | let config = bincode::config::legacy(); 97 | if compressed { 98 | let mut reader = flate2::read::ZlibDecoder::new(contents); 99 | bincode::serde::decode_from_std_read(&mut reader, config) 100 | } else { 101 | bincode::serde::decode_from_slice(contents, config).map(|(a, _)| a) 102 | } 103 | .map_err(|_| anyhow::format_err!("Could not parse {}", description)) 104 | } 105 | -------------------------------------------------------------------------------- /src/blame.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use proc_exit::WithCodeResultExt; 3 | 4 | use crate::git2_config::Config; 5 | use crate::git2_config::DefaultField; 6 | use crate::git2_config::RawField; 7 | use crate::git_pager::Pager; 8 | 9 | pub(crate) fn blame( 10 | file_path: &std::path::Path, 11 | config: &mut Config, 12 | args: &crate::args::Args, 13 | ) -> proc_exit::ExitResult { 14 | let colored_stdout = 15 | anstream::AutoStream::choice(&std::io::stdout()) != anstream::ColorChoice::Never; 16 | let total_width = terminal_size::terminal_size() 17 | .map(|(w, _h)| w.0) 18 | .or_else(|| std::env::var_os("COLUMNS").and_then(|s| s.to_str()?.parse::().ok())) 19 | .unwrap_or(80) as usize; 20 | 21 | let cwd = std::env::current_dir().with_code(proc_exit::Code::FAILURE)?; 22 | let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::FAILURE)?; 23 | config.add_repo(&repo); 24 | let theme = config.get(&THEME); 25 | 26 | let rel_path = to_repo_relative(&cwd, file_path, &repo).with_code(proc_exit::Code::FAILURE)?; 27 | 28 | let rev_obj = repo 29 | .revparse_single(&args.rev) 30 | .with_code(proc_exit::Code::FAILURE)?; 31 | let rev_commit = rev_obj 32 | .peel_to_commit() 33 | .map_err(|_| { 34 | anyhow::format_err!( 35 | "Unsupported rev `{}` ({})", 36 | args.rev, 37 | rev_obj.kind().map(|k| k.str()).unwrap_or("unknown") 38 | ) 39 | }) 40 | .with_code(proc_exit::Code::FAILURE)?; 41 | let mut settings = git2::BlameOptions::new(); 42 | settings 43 | .track_copies_same_file(true) 44 | .track_copies_same_commit_moves(true) 45 | .track_copies_same_commit_copies(true) 46 | .track_copies_any_commit_copies(true) 47 | .first_parent(true) 48 | .ignore_whitespace(true) 49 | .newest_commit(rev_commit.id()); 50 | let blame = repo 51 | .blame_file(&rel_path, Some(&mut settings)) 52 | .with_code(proc_exit::Code::FAILURE)?; 53 | let mut annotations = Annotations::new(&repo, &blame); 54 | annotations 55 | .relative_origin(&repo, &args.rev) 56 | .with_code(proc_exit::Code::FAILURE)?; 57 | 58 | let file = read_file(&repo, &args.rev, &rel_path).with_code(proc_exit::Code::FAILURE)?; 59 | 60 | let syntax_set = crate::assets::load_syntaxes(); 61 | let theme_set = crate::assets::load_themes(); 62 | let theme = theme_set 63 | .get(&theme) 64 | .or_else(|| theme_set.get(THEME_DEFAULT)) 65 | .expect("default theme is present"); 66 | 67 | let syntax = syntax_set 68 | .find_syntax_for_file(file_path) 69 | .with_code(proc_exit::Code::FAILURE)? 70 | .unwrap_or_else(|| syntax_set.find_syntax_plain_text()); 71 | 72 | let file = convert_file(&file, file_path).with_code(proc_exit::Code::FAILURE)?; 73 | 74 | let line_count = file.lines().count(); 75 | let line_count_width = line_count.to_string().len(); // bytes = chars = columns with digits 76 | let sep = "│"; 77 | let space_count = 3; 78 | let origin_width = annotations 79 | .notes 80 | .values() 81 | .map(|a| { 82 | // HACK: when we support more than IDs, we'll need to respect UTF-8 83 | a.origin().len() 84 | }) 85 | .max() 86 | .unwrap_or(0); 87 | let gutter_width = origin_width + line_count_width + sep.len() + space_count; 88 | 89 | let code_width = total_width.saturating_sub(gutter_width); 90 | 91 | let mut highlighter = if colored_stdout { 92 | Highlighter::enabled(syntax, theme) 93 | } else { 94 | Highlighter::disabled() 95 | }; 96 | 97 | let reset = if colored_stdout { 98 | anstyle::Reset.render().to_string() 99 | } else { 100 | "".to_owned() 101 | }; 102 | let gutter_style = if colored_stdout { 103 | gutter_style(highlighter.theme()).render().to_string() 104 | } else { 105 | "".to_owned() 106 | }; 107 | let wrap = textwrap::Options::new(code_width) 108 | .break_words(false) 109 | .wrap_algorithm(textwrap::WrapAlgorithm::FirstFit); 110 | 111 | let pager = config.get(&crate::git2_config::PAGER); 112 | let mut pager = Pager::stdout(&pager); 113 | let mut pager = pager.start(); 114 | let pager = pager.as_writer().with_code(proc_exit::Code::FAILURE)?; 115 | let mut prev_hunk_id = git2::Oid::zero(); 116 | for (line_num, file_line) in file.lines().enumerate() { 117 | let line_num = line_num + 1; 118 | 119 | let file_line = if line_num == 1 { 120 | file_line.strip_prefix('\u{feff}').unwrap_or(file_line) 121 | } else { 122 | file_line 123 | }; 124 | 125 | let file_line = highlighter 126 | .highlight_line(file_line, &syntax_set) 127 | .with_code(proc_exit::Code::FAILURE)?; 128 | #[allow(clippy::never_loop)] 129 | for (i, visual_line) in textwrap::wrap(&file_line, &wrap).into_iter().enumerate() { 130 | let origin = if i == 0 { 131 | let hunk = blame.get_line(line_num).unwrap_or_else(|| { 132 | panic!("Mismatch in line numbers between dive ({line_num}) and git2") 133 | }); 134 | let hunk_id = hunk.orig_commit_id(); 135 | if hunk_id != prev_hunk_id { 136 | prev_hunk_id = hunk_id; 137 | let ann = annotations 138 | .notes 139 | .get(&hunk_id) 140 | .expect("all blame hunks are annotated"); 141 | ann.origin() 142 | } else { 143 | "⋮" 144 | } 145 | } else { 146 | "⋮" 147 | }; 148 | 149 | let line_num = if i == 0 { 150 | line_num.to_string() 151 | } else { 152 | "⋮".to_owned() 153 | }; 154 | let _ = write!( 155 | pager, 156 | "{gutter_style}{origin:origin_width$} {line_num:>line_count_width$} {sep} {reset}{visual_line}\n{reset}" 157 | ); 158 | // HACK: Truncate until we fix our coloring of wrapped lines 159 | break; 160 | } 161 | } 162 | 163 | Ok(()) 164 | } 165 | 166 | fn to_repo_relative( 167 | cwd: &std::path::Path, 168 | path: &std::path::Path, 169 | repo: &git2::Repository, 170 | ) -> anyhow::Result { 171 | let workdir = repo.workdir().ok_or_else(|| { 172 | anyhow::format_err!("No workdir found; Bare repositories are not supported") 173 | })?; 174 | let abs_path = 175 | dunce::canonicalize(path).unwrap_or_else(|_err| path_clean::clean(cwd.join(path))); 176 | let rel_path = abs_path.strip_prefix(workdir).map_err(|_| { 177 | anyhow::format_err!( 178 | "File {} is not in the repository's workdir {}", 179 | abs_path.display(), 180 | workdir.display() 181 | ) 182 | })?; 183 | Ok(rel_path.to_owned()) 184 | } 185 | 186 | fn read_file( 187 | repo: &git2::Repository, 188 | rev: &str, 189 | rel_path: &std::path::Path, 190 | ) -> anyhow::Result> { 191 | let rev_obj = repo.revparse_single(rev)?; 192 | let rev_tree = rev_obj.peel_to_tree().map_err(|_| { 193 | anyhow::format_err!( 194 | "Unsupported rev `{}` ({})", 195 | rev, 196 | rev_obj.kind().map(|k| k.str()).unwrap_or("unknown") 197 | ) 198 | })?; 199 | let file_entry = rev_tree 200 | .get_path(rel_path) 201 | .with_context(|| format!("Could not read {} at {}", rel_path.display(), rev))?; 202 | let file_obj = file_entry 203 | .to_object(repo) 204 | .with_context(|| format!("Could not read {} at {}", rel_path.display(), rev))?; 205 | let file_blob = file_obj 206 | .as_blob() 207 | .with_context(|| format!("Could not read {} at {}", rel_path.display(), rev))?; 208 | let file = file_blob.content(); 209 | Ok(file.to_owned()) 210 | } 211 | 212 | fn convert_file(buffer: &[u8], path: &std::path::Path) -> anyhow::Result { 213 | let content_type = content_inspector::inspect(buffer); 214 | 215 | let buffer = match content_type { 216 | content_inspector::ContentType::BINARY | 217 | // HACK: We don't support UTF-32 yet 218 | content_inspector::ContentType::UTF_32LE | 219 | content_inspector::ContentType::UTF_32BE => { 220 | anyhow::bail!("Could not ready binary file {}", path.display()) 221 | }, 222 | content_inspector::ContentType::UTF_8 | 223 | content_inspector::ContentType::UTF_8_BOM => { 224 | String::from_utf8_lossy(buffer).into_owned() 225 | }, 226 | content_inspector::ContentType::UTF_16LE => { 227 | // Despite accepting a `String`, decode_to_string_without_replacement` doesn't allocate 228 | // so to avoid `OutputFull` loops, we're going to assume any UTF-16 content can fit in 229 | // a buffer twice its size 230 | let mut decoded = String::with_capacity(buffer.len() * 2); 231 | let (r, written) = encoding_rs::UTF_16LE.new_decoder_with_bom_removal().decode_to_string_without_replacement(buffer, &mut decoded, true); 232 | let decoded = match r { 233 | encoding_rs::DecoderResult::InputEmpty => decoded, 234 | _ => anyhow::bail!("invalid UTF-16LE encoding at byte {} in {}", written, path.display()), 235 | }; 236 | decoded 237 | } 238 | content_inspector::ContentType::UTF_16BE => { 239 | // Despite accepting a `String`, decode_to_string_without_replacement` doesn't allocate 240 | // so to avoid `OutputFull` loops, we're going to assume any UTF-16 content can fit in 241 | // a buffer twice its size 242 | let mut decoded = String::with_capacity(buffer.len() * 2); 243 | let (r, written) = encoding_rs::UTF_16BE.new_decoder_with_bom_removal().decode_to_string_without_replacement(buffer, &mut decoded, true); 244 | let decoded = match r { 245 | encoding_rs::DecoderResult::InputEmpty => decoded, 246 | _ => anyhow::bail!("invalid UTF-16BE encoding at byte {} in {}", written, path.display()), 247 | }; 248 | decoded 249 | }, 250 | }; 251 | 252 | Ok(buffer) 253 | } 254 | 255 | pub(crate) struct Annotations { 256 | notes: std::collections::HashMap, 257 | } 258 | 259 | impl Annotations { 260 | pub(crate) fn new(repo: &git2::Repository, blame: &git2::Blame<'_>) -> Self { 261 | let mut notes = std::collections::HashMap::new(); 262 | for hunk in blame.iter() { 263 | let id = hunk.orig_commit_id(); 264 | notes.entry(id).or_insert_with(|| Annotation::new(repo, id)); 265 | } 266 | 267 | Annotations { notes } 268 | } 269 | 270 | pub(crate) fn relative_origin( 271 | &mut self, 272 | repo: &git2::Repository, 273 | head: &str, 274 | ) -> anyhow::Result<()> { 275 | let mut queue = self 276 | .notes 277 | .keys() 278 | .copied() 279 | .collect::>(); 280 | 281 | let rev_obj = repo.revparse_single(head)?; 282 | let rev_commit = rev_obj.peel_to_commit().map_err(|_| { 283 | anyhow::format_err!( 284 | "Unsupported rev `{}` ({})", 285 | head, 286 | rev_obj.kind().map(|k| k.str()).unwrap_or("unknown") 287 | ) 288 | })?; 289 | 290 | let (head, offset) = split_revset(head); 291 | 292 | let mut revwalk = repo.revwalk()?; 293 | revwalk.simplify_first_parent()?; 294 | // If just walking first parents, shouldn't really need to sort 295 | revwalk.set_sorting(git2::Sort::NONE)?; 296 | revwalk.push(rev_commit.id())?; 297 | for (i, id) in revwalk.enumerate() { 298 | let i = i + offset; 299 | let id = id?; 300 | let relative = if i == 0 { 301 | head.to_owned() 302 | } else { 303 | format!("{head}~{i}") 304 | }; 305 | self.notes 306 | .entry(id) 307 | .or_insert_with(|| Annotation::new(repo, id)) 308 | .relative = Some(relative); 309 | 310 | queue.remove(&id); 311 | if queue.is_empty() { 312 | break; 313 | } 314 | } 315 | Ok(()) 316 | } 317 | } 318 | 319 | fn split_revset(mut head: &str) -> (&str, usize) { 320 | let mut offset = 0; 321 | while let Some((start, end)) = head.rsplit_once('~') { 322 | if end.is_empty() { 323 | head = start; 324 | offset += 1; 325 | } else if let Ok(curr_offset) = end.parse::() { 326 | head = start; 327 | offset += curr_offset; 328 | } else { 329 | break; 330 | } 331 | } 332 | (head, offset) 333 | } 334 | 335 | pub(crate) struct Annotation { 336 | short: String, 337 | relative: Option, 338 | } 339 | 340 | impl Annotation { 341 | pub(crate) fn new(repo: &git2::Repository, id: git2::Oid) -> Self { 342 | let obj = repo.find_object(id, None).expect("blame has valid ids"); 343 | let short = obj 344 | .short_id() 345 | .unwrap_or_else(|e| panic!("unknown failure for short_id for {id}: {e}")) 346 | .as_str() 347 | .expect("short_id is always valid UTF-8") 348 | .to_owned(); 349 | Self { 350 | short, 351 | relative: None, 352 | } 353 | } 354 | 355 | pub(crate) fn origin(&self) -> &str { 356 | self.relative.as_deref().unwrap_or(self.short.as_str()) 357 | } 358 | } 359 | 360 | pub(crate) struct Highlighter<'a> { 361 | highlighter: Option>, 362 | theme: &'a syntect::highlighting::Theme, 363 | } 364 | 365 | impl<'a> Highlighter<'a> { 366 | pub(crate) fn enabled( 367 | syntax: &'a syntect::parsing::SyntaxReference, 368 | theme: &'a syntect::highlighting::Theme, 369 | ) -> Self { 370 | let highlighter = Some(syntect::easy::HighlightLines::new(syntax, theme)); 371 | Self { highlighter, theme } 372 | } 373 | 374 | pub(crate) fn disabled() -> Self { 375 | static THEME: syntect::highlighting::Theme = syntect::highlighting::Theme { 376 | name: None, 377 | author: None, 378 | settings: syntect::highlighting::ThemeSettings { 379 | foreground: None, 380 | background: None, 381 | caret: None, 382 | line_highlight: None, 383 | misspelling: None, 384 | minimap_border: None, 385 | accent: None, 386 | popup_css: None, 387 | phantom_css: None, 388 | bracket_contents_foreground: None, 389 | bracket_contents_options: None, 390 | brackets_foreground: None, 391 | brackets_background: None, 392 | brackets_options: None, 393 | tags_foreground: None, 394 | tags_options: None, 395 | highlight: None, 396 | find_highlight: None, 397 | find_highlight_foreground: None, 398 | gutter: None, 399 | gutter_foreground: None, 400 | selection: None, 401 | selection_foreground: None, 402 | selection_border: None, 403 | inactive_selection: None, 404 | inactive_selection_foreground: None, 405 | guide: None, 406 | active_guide: None, 407 | stack_guide: None, 408 | shadow: None, 409 | }, 410 | scopes: Vec::new(), 411 | }; 412 | let highlighter = None; 413 | Self { 414 | highlighter, 415 | theme: &THEME, 416 | } 417 | } 418 | 419 | fn theme(&self) -> &syntect::highlighting::Theme { 420 | self.theme 421 | } 422 | 423 | pub(crate) fn highlight_line( 424 | &mut self, 425 | line: &str, 426 | syntax_set: &syntect::parsing::SyntaxSet, 427 | ) -> anyhow::Result { 428 | if let Some(highlighter) = &mut self.highlighter { 429 | // skip syntax highlighting on long lines 430 | let too_long = line.len() > 1024 * 16; 431 | let for_highlighting: &str = if too_long { "\n" } else { line }; 432 | let mut ranges = highlighter.highlight_line(for_highlighting, syntax_set)?; 433 | if too_long { 434 | ranges[0].1 = line; 435 | } 436 | 437 | let mut escaped = String::new(); 438 | for (style, region) in ranges { 439 | use std::fmt::Write; 440 | let style = body_style(style); 441 | let _ = write!( 442 | &mut escaped, 443 | "{}{}{}", 444 | style.render(), 445 | region, 446 | anstyle::Reset.render() 447 | ); 448 | } 449 | Ok(escaped) 450 | } else { 451 | Ok(line.to_owned()) 452 | } 453 | } 454 | } 455 | 456 | fn body_style(style: syntect::highlighting::Style) -> anstyle::Style { 457 | let fg_color = crate::assets::to_anstyle_color(style.foreground); 458 | // intentionally not setting bg_color 459 | let effects = anstyle::Effects::new() 460 | .set( 461 | anstyle::Effects::BOLD, 462 | style 463 | .font_style 464 | .contains(syntect::highlighting::FontStyle::BOLD), 465 | ) 466 | .set( 467 | anstyle::Effects::UNDERLINE, 468 | style 469 | .font_style 470 | .contains(syntect::highlighting::FontStyle::UNDERLINE), 471 | ) 472 | .set( 473 | anstyle::Effects::ITALIC, 474 | style 475 | .font_style 476 | .contains(syntect::highlighting::FontStyle::ITALIC), 477 | ); 478 | let output = anstyle::Style::new().fg_color(fg_color).effects(effects); 479 | output 480 | } 481 | 482 | fn gutter_style(theme: &syntect::highlighting::Theme) -> anstyle::Style { 483 | const DEFAULT_GUTTER_COLOR: u8 = 238; 484 | 485 | // If the theme provides a gutter foreground color, use it. 486 | let fg_color = theme 487 | .settings 488 | .gutter_foreground 489 | .map(crate::assets::to_anstyle_color) 490 | .unwrap_or_else(|| Some(anstyle::Ansi256Color(DEFAULT_GUTTER_COLOR).into())); 491 | 492 | anstyle::Style::new().fg_color(fg_color) 493 | } 494 | 495 | const THEME_DEFAULT: &str = "Monokai Extended"; 496 | pub(crate) const THEME: DefaultField = 497 | RawField::::new("dive.theme").default_value(|| THEME_DEFAULT.to_owned()); 498 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use proc_exit::WithCodeResultExt; 2 | 3 | use crate::git2_config::Config; 4 | use crate::git2_config::ReflectField; 5 | 6 | pub(crate) fn dump_config( 7 | output_path: &std::path::Path, 8 | config: &mut Config, 9 | ) -> proc_exit::ExitResult { 10 | let cwd = std::env::current_dir().with_code(proc_exit::Code::FAILURE)?; 11 | let repo = git2::Repository::discover(cwd).with_code(proc_exit::Code::FAILURE)?; 12 | 13 | config.add_repo(&repo); 14 | let output = config.dump([ 15 | &crate::git2_config::COLOR_UI as &dyn ReflectField, 16 | &crate::git2_config::PAGER as &dyn ReflectField, 17 | &crate::blame::THEME as &dyn ReflectField, 18 | ]); 19 | 20 | if output_path == std::path::Path::new("-") { 21 | use std::io::Write; 22 | anstream::stdout() 23 | .write_all(output.as_bytes()) 24 | .with_code(proc_exit::Code::FAILURE)?; 25 | } else { 26 | std::fs::write(output_path, &output).with_code(proc_exit::Code::FAILURE)?; 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /src/git2_config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct Config { 5 | pager: InMemoryConfig, 6 | system: Option, 7 | xdg: Option, 8 | global: Option, 9 | local: Option, 10 | env: InMemoryConfig, 11 | cli: InMemoryConfig, 12 | git_pager: InMemoryConfig, 13 | } 14 | 15 | impl Config { 16 | pub(crate) fn system() -> Self { 17 | let pager = InMemoryConfig::pager(); 18 | let system = GitConfig::open_system(); 19 | let xdg = GitConfig::open_xdg(); 20 | let global = GitConfig::open_global(); 21 | let local = None; 22 | let env = InMemoryConfig::git_env(); 23 | let cli = InMemoryConfig::git_cli(); 24 | let git_pager = InMemoryConfig::git_pager(); 25 | Self { 26 | pager, 27 | system, 28 | xdg, 29 | global, 30 | local, 31 | env, 32 | cli, 33 | git_pager, 34 | } 35 | } 36 | 37 | pub(crate) fn add_repo(&mut self, repo: &git2::Repository) { 38 | let local = GitConfig::open_local(repo); 39 | self.local = local; 40 | } 41 | 42 | pub(crate) fn get(&self, field: &F) -> F::Output { 43 | field.get_from(self) 44 | } 45 | 46 | pub(crate) fn dump<'f>( 47 | &self, 48 | fields: impl IntoIterator, 49 | ) -> String { 50 | use std::fmt::Write; 51 | 52 | let mut output = String::new(); 53 | 54 | let mut prior_section = ""; 55 | for field in fields { 56 | let (section, name) = field 57 | .name() 58 | .split_once('.') 59 | .unwrap_or_else(|| panic!("field `{}` is missing a section", field.name())); 60 | if section != prior_section { 61 | let _ = writeln!(&mut output, "[{section}]"); 62 | prior_section = section; 63 | } 64 | let value = field.dump(self); 65 | let source = field.get_source(self); 66 | let _ = writeln!(&mut output, "\t{name} = {value} # {source}"); 67 | } 68 | 69 | output 70 | } 71 | 72 | pub(crate) fn sources(&self) -> impl Iterator { 73 | [ 74 | Some(&self.git_pager).map(|c| c as &dyn ConfigSource), 75 | Some(&self.cli).map(|c| c as &dyn ConfigSource), 76 | Some(&self.env).map(|c| c as &dyn ConfigSource), 77 | self.local.as_ref().map(|c| c as &dyn ConfigSource), 78 | self.global.as_ref().map(|c| c as &dyn ConfigSource), 79 | self.xdg.as_ref().map(|c| c as &dyn ConfigSource), 80 | self.system.as_ref().map(|c| c as &dyn ConfigSource), 81 | Some(&self.pager).map(|c| c as &dyn ConfigSource), 82 | ] 83 | .into_iter() 84 | .flatten() 85 | } 86 | } 87 | 88 | pub(crate) trait ConfigSource { 89 | fn name(&self) -> &str; 90 | 91 | fn get_source(&self, name: &str) -> anyhow::Result<&str>; 92 | fn get_bool(&self, name: &str) -> anyhow::Result; 93 | fn get_i32(&self, name: &str) -> anyhow::Result; 94 | fn get_i64(&self, name: &str) -> anyhow::Result; 95 | fn get_string(&self, name: &str) -> anyhow::Result; 96 | fn get_path(&self, name: &str) -> anyhow::Result; 97 | } 98 | 99 | impl ConfigSource for Config { 100 | fn name(&self) -> &str { 101 | "git" 102 | } 103 | 104 | fn get_source(&self, name: &str) -> anyhow::Result<&str> { 105 | for config in self.sources() { 106 | if let Ok(source) = config.get_source(name) { 107 | return Ok(source); 108 | } 109 | } 110 | // Fallback to the first error 111 | self.sources() 112 | .next() 113 | .expect("always a source") 114 | .get_source(name) 115 | } 116 | 117 | fn get_bool(&self, name: &str) -> anyhow::Result { 118 | for config in self.sources() { 119 | if let Ok(v) = config.get_bool(name) { 120 | return Ok(v); 121 | } 122 | } 123 | // Fallback to the first error 124 | self.sources() 125 | .next() 126 | .expect("always a source") 127 | .get_bool(name) 128 | } 129 | fn get_i32(&self, name: &str) -> anyhow::Result { 130 | for config in self.sources() { 131 | if let Ok(v) = config.get_i32(name) { 132 | return Ok(v); 133 | } 134 | } 135 | // Fallback to the first error 136 | self.sources() 137 | .next() 138 | .expect("always a source") 139 | .get_i32(name) 140 | } 141 | fn get_i64(&self, name: &str) -> anyhow::Result { 142 | for config in self.sources() { 143 | if let Ok(v) = config.get_i64(name) { 144 | return Ok(v); 145 | } 146 | } 147 | // Fallback to the first error 148 | self.sources() 149 | .next() 150 | .expect("always a source") 151 | .get_i64(name) 152 | } 153 | fn get_string(&self, name: &str) -> anyhow::Result { 154 | for config in self.sources() { 155 | if let Ok(v) = config.get_string(name) { 156 | return Ok(v); 157 | } 158 | } 159 | // Fallback to the first error 160 | self.sources() 161 | .next() 162 | .expect("always a source") 163 | .get_string(name) 164 | } 165 | fn get_path(&self, name: &str) -> anyhow::Result { 166 | for config in self.sources() { 167 | if let Ok(v) = config.get_path(name) { 168 | return Ok(v); 169 | } 170 | } 171 | // Fallback to the first error 172 | self.sources() 173 | .next() 174 | .expect("always a source") 175 | .get_path(name) 176 | } 177 | } 178 | 179 | impl ConfigSource for git2::Config { 180 | fn name(&self) -> &str { 181 | "gitconfig" 182 | } 183 | 184 | fn get_source(&self, name: &str) -> anyhow::Result<&str> { 185 | self.get_entry(name) 186 | .map(|_| self.name()) 187 | .map_err(|e| e.into()) 188 | } 189 | fn get_bool(&self, name: &str) -> anyhow::Result { 190 | self.get_bool(name).map_err(|e| e.into()) 191 | } 192 | fn get_i32(&self, name: &str) -> anyhow::Result { 193 | self.get_i32(name).map_err(|e| e.into()) 194 | } 195 | fn get_i64(&self, name: &str) -> anyhow::Result { 196 | self.get_i64(name).map_err(|e| e.into()) 197 | } 198 | fn get_string(&self, name: &str) -> anyhow::Result { 199 | self.get_string(name).map_err(|e| e.into()) 200 | } 201 | fn get_path(&self, name: &str) -> anyhow::Result { 202 | self.get_path(name).map_err(|e| e.into()) 203 | } 204 | } 205 | 206 | pub(crate) struct GitConfig { 207 | name: String, 208 | config: git2::Config, 209 | } 210 | 211 | impl GitConfig { 212 | pub(crate) fn open_system() -> Option { 213 | let path = git2::Config::find_system().ok()?; 214 | Self::open_path(&path) 215 | } 216 | 217 | pub(crate) fn open_xdg() -> Option { 218 | let path = git2::Config::find_xdg().ok()?; 219 | Self::open_path(&path) 220 | } 221 | 222 | pub(crate) fn open_global() -> Option { 223 | let path = git2::Config::find_global().ok()?; 224 | Self::open_path(&path) 225 | } 226 | 227 | pub(crate) fn open_local(repo: &git2::Repository) -> Option { 228 | let path = repo.path().join("config"); 229 | let config = git2::Config::open(&path).ok()?; 230 | let name = "$GIT_DIR/config".to_owned(); 231 | Some(Self { name, config }) 232 | } 233 | 234 | fn open_path(path: &std::path::Path) -> Option { 235 | let config = git2::Config::open(path).ok()?; 236 | let name = path.display().to_string(); 237 | Some(Self { name, config }) 238 | } 239 | 240 | fn inner(&self) -> &impl ConfigSource { 241 | &self.config 242 | } 243 | } 244 | 245 | impl ConfigSource for GitConfig { 246 | fn name(&self) -> &str { 247 | &self.name 248 | } 249 | 250 | fn get_source(&self, name: &str) -> anyhow::Result<&str> { 251 | self.inner().get_source(name) 252 | } 253 | fn get_bool(&self, name: &str) -> anyhow::Result { 254 | self.inner().get_bool(name) 255 | } 256 | fn get_i32(&self, name: &str) -> anyhow::Result { 257 | self.inner().get_i32(name) 258 | } 259 | fn get_i64(&self, name: &str) -> anyhow::Result { 260 | self.inner().get_i64(name) 261 | } 262 | fn get_string(&self, name: &str) -> anyhow::Result { 263 | self.inner().get_string(name) 264 | } 265 | fn get_path(&self, name: &str) -> anyhow::Result { 266 | self.inner().get_path(name) 267 | } 268 | } 269 | 270 | impl std::fmt::Debug for GitConfig { 271 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 272 | f.debug_struct("GitConfig") 273 | .field("name", &self.name) 274 | .finish() 275 | } 276 | } 277 | 278 | #[derive(Debug)] 279 | pub(crate) struct InMemoryConfig { 280 | name: String, 281 | values: std::collections::BTreeMap>, 282 | } 283 | 284 | impl InMemoryConfig { 285 | pub(crate) fn git_env() -> Self { 286 | Self::from_env("git-config-env", git_config_env::ConfigEnv::new().iter()) 287 | } 288 | 289 | pub(crate) fn git_cli() -> Self { 290 | Self::from_env( 291 | "git-cli", 292 | git_config_env::ConfigParameters::new() 293 | .iter() 294 | .map(|(k, v)| (k, v.unwrap_or(std::borrow::Cow::Borrowed("true")))), 295 | ) 296 | } 297 | 298 | pub(crate) fn git_pager() -> Self { 299 | let name = "GIT_PAGER"; 300 | let value = std::env::var_os(name).and_then(|v| v.into_string().ok()); 301 | Self::from_env(name, value.map(|v| ("core.pager", v))) 302 | } 303 | 304 | pub(crate) fn pager() -> Self { 305 | let name = "PAGER"; 306 | let value = std::env::var_os(name).and_then(|v| v.into_string().ok()); 307 | Self::from_env(name, value.map(|v| ("core.pager", v))) 308 | } 309 | 310 | pub(crate) fn from_env( 311 | name: impl Into, 312 | env: impl IntoIterator, impl Into)>, 313 | ) -> Self { 314 | let name = name.into(); 315 | let mut values = std::collections::BTreeMap::new(); 316 | for (key, value) in env { 317 | values 318 | .entry(key.into()) 319 | .or_insert_with(Vec::new) 320 | .push(value.into()); 321 | } 322 | Self { name, values } 323 | } 324 | 325 | fn get_str(&self, name: &str) -> anyhow::Result<&str> { 326 | let value = self 327 | .values 328 | .get(name) 329 | .context("field is missing")? 330 | .last() 331 | .expect("always at least one element"); 332 | Ok(value) 333 | } 334 | } 335 | 336 | impl Default for InMemoryConfig { 337 | fn default() -> Self { 338 | Self { 339 | name: "null".to_owned(), 340 | values: Default::default(), 341 | } 342 | } 343 | } 344 | 345 | impl ConfigSource for InMemoryConfig { 346 | fn name(&self) -> &str { 347 | &self.name 348 | } 349 | 350 | fn get_source(&self, name: &str) -> anyhow::Result<&str> { 351 | self.get_str(name).map(|_| self.name()) 352 | } 353 | fn get_bool(&self, name: &str) -> anyhow::Result { 354 | let v = self.get_str(name).unwrap_or("true"); 355 | v.parse::().map_err(|e| e.into()) 356 | } 357 | fn get_i32(&self, name: &str) -> anyhow::Result { 358 | self.get_str(name) 359 | .and_then(|v| v.parse::().map_err(|e| e.into())) 360 | } 361 | fn get_i64(&self, name: &str) -> anyhow::Result { 362 | self.get_str(name) 363 | .and_then(|v| v.parse::().map_err(|e| e.into())) 364 | } 365 | fn get_string(&self, name: &str) -> anyhow::Result { 366 | self.get_str(name).map(|v| v.to_owned()) 367 | } 368 | fn get_path(&self, name: &str) -> anyhow::Result { 369 | self.get_string(name).map(|v| v.into()) 370 | } 371 | } 372 | 373 | pub(crate) trait FieldReader { 374 | fn get_field(&self, name: &str) -> anyhow::Result; 375 | } 376 | 377 | impl FieldReader for C { 378 | fn get_field(&self, name: &str) -> anyhow::Result { 379 | self.get_bool(name) 380 | .with_context(|| anyhow::format_err!("failed to read `{}`", name)) 381 | } 382 | } 383 | 384 | impl FieldReader for C { 385 | fn get_field(&self, name: &str) -> anyhow::Result { 386 | self.get_i32(name) 387 | .with_context(|| anyhow::format_err!("failed to read `{}`", name)) 388 | } 389 | } 390 | 391 | impl FieldReader for C { 392 | fn get_field(&self, name: &str) -> anyhow::Result { 393 | self.get_i64(name) 394 | .with_context(|| anyhow::format_err!("failed to read `{}`", name)) 395 | } 396 | } 397 | 398 | impl FieldReader for C { 399 | fn get_field(&self, name: &str) -> anyhow::Result { 400 | self.get_string(name) 401 | .with_context(|| anyhow::format_err!("failed to read `{}`", name)) 402 | } 403 | } 404 | 405 | impl FieldReader for C { 406 | fn get_field(&self, name: &str) -> anyhow::Result { 407 | self.get_path(name) 408 | .with_context(|| anyhow::format_err!("failed to read `{}`", name)) 409 | } 410 | } 411 | 412 | impl FieldReader

for C { 413 | fn get_field(&self, name: &str) -> anyhow::Result

{ 414 | self.get_string(name) 415 | .with_context(|| anyhow::format_err!("failed to read `{}`", name)) 416 | .and_then(|s| P::parse(&s)) 417 | } 418 | } 419 | 420 | pub(crate) trait Parseable: Sized { 421 | fn parse(s: &str) -> anyhow::Result; 422 | } 423 | 424 | pub(crate) struct ParseWrapper(pub(crate) T); 425 | 426 | impl std::fmt::Display for ParseWrapper { 427 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 428 | self.0.fmt(f) 429 | } 430 | } 431 | 432 | impl std::str::FromStr for ParseWrapper 433 | where 434 | T: std::str::FromStr, 435 | T::Err: Into, 436 | { 437 | type Err = anyhow::Error; 438 | 439 | fn from_str(s: &str) -> Result { 440 | T::from_str(s).map(ParseWrapper).map_err(|e| e.into()) 441 | } 442 | } 443 | 444 | impl Parseable for ParseWrapper 445 | where 446 | T: Parseable, 447 | T: std::str::FromStr, 448 | T: std::fmt::Display, 449 | T::Err: Into, 450 | { 451 | fn parse(s: &str) -> anyhow::Result { 452 | ::from_str(s) 453 | } 454 | } 455 | 456 | pub(crate) trait Field { 457 | type Output; 458 | 459 | fn name(&self) -> &'static str; 460 | fn get_from(&self, config: &Config) -> Self::Output; 461 | fn get_source<'c>(&self, config: &'c Config) -> Option<&'c str>; 462 | } 463 | 464 | pub(crate) struct RawField { 465 | name: &'static str, 466 | _type: std::marker::PhantomData, 467 | } 468 | 469 | impl RawField { 470 | pub(crate) const fn new(name: &'static str) -> Self { 471 | Self { 472 | name, 473 | _type: std::marker::PhantomData, 474 | } 475 | } 476 | 477 | pub(crate) const fn default_value(self, default: DefaultFn) -> DefaultField { 478 | DefaultField { 479 | field: self, 480 | default, 481 | } 482 | } 483 | } 484 | 485 | impl RawField 486 | where 487 | R: Default, 488 | { 489 | pub(crate) const fn default(self) -> DefaultField { 490 | DefaultField { 491 | field: self, 492 | default: R::default, 493 | } 494 | } 495 | } 496 | 497 | impl Field for RawField 498 | where 499 | Config: FieldReader, 500 | { 501 | type Output = Option; 502 | 503 | fn name(&self) -> &'static str { 504 | self.name 505 | } 506 | 507 | fn get_from(&self, config: &Config) -> Self::Output { 508 | config.get_field(self.name).ok() 509 | } 510 | 511 | fn get_source<'c>(&self, config: &'c Config) -> Option<&'c str> { 512 | config.get_source(self.name).ok() 513 | } 514 | } 515 | 516 | type DefaultFn = fn() -> R; 517 | 518 | pub(crate) struct DefaultField { 519 | field: RawField, 520 | default: DefaultFn, 521 | } 522 | 523 | impl Field for DefaultField 524 | where 525 | Config: FieldReader, 526 | { 527 | type Output = R; 528 | 529 | fn name(&self) -> &'static str { 530 | self.field.name() 531 | } 532 | 533 | fn get_from(&self, config: &Config) -> Self::Output { 534 | self.field 535 | .get_from(config) 536 | .unwrap_or_else(|| (self.default)()) 537 | } 538 | 539 | fn get_source<'c>(&self, config: &'c Config) -> Option<&'c str> { 540 | Some(self.field.get_source(config).unwrap_or("default")) 541 | } 542 | } 543 | 544 | pub(crate) trait ReflectField { 545 | fn name(&self) -> &'static str; 546 | 547 | fn dump(&self, config: &Config) -> String; 548 | fn get_source<'c>(&self, config: &'c Config) -> &'c str; 549 | } 550 | 551 | impl ReflectField for F 552 | where 553 | F: Field, 554 | F::Output: std::fmt::Display, 555 | { 556 | fn name(&self) -> &'static str { 557 | self.name() 558 | } 559 | 560 | fn dump(&self, config: &Config) -> String { 561 | self.get_from(config).to_string() 562 | } 563 | fn get_source<'c>(&self, config: &'c Config) -> &'c str { 564 | F::get_source(self, config).expect("assuming if its Display then it has a source") 565 | } 566 | } 567 | 568 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 569 | pub(crate) enum ColorWhen { 570 | Always, 571 | Auto, 572 | Never, 573 | } 574 | 575 | impl ColorWhen { 576 | pub(crate) fn as_str(&self) -> &'static str { 577 | match self { 578 | Self::Always => "always", 579 | Self::Auto => "auto", 580 | Self::Never => "never", 581 | } 582 | } 583 | } 584 | 585 | impl Default for ColorWhen { 586 | fn default() -> Self { 587 | Self::Auto 588 | } 589 | } 590 | 591 | impl std::fmt::Display for ColorWhen { 592 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 593 | self.as_str().fmt(f) 594 | } 595 | } 596 | 597 | impl std::str::FromStr for ColorWhen { 598 | type Err = anyhow::Error; 599 | 600 | fn from_str(s: &str) -> Result { 601 | match s { 602 | "always" | "true" => Ok(Self::Always), 603 | "auto" => Ok(Self::Auto), 604 | "never" | "false" => Ok(Self::Never), 605 | _ => Err(anyhow::format_err!("unsupported color valued: `{}`", s)), 606 | } 607 | } 608 | } 609 | 610 | impl Parseable for ColorWhen { 611 | fn parse(s: &str) -> anyhow::Result { 612 | ::from_str(s) 613 | } 614 | } 615 | 616 | pub(crate) const COLOR_UI: DefaultField = 617 | RawField::::new("color.ui").default(); 618 | 619 | pub(crate) const PAGER: DefaultField = 620 | RawField::::new("core.pager").default_value(|| "less".to_owned()); 621 | -------------------------------------------------------------------------------- /src/git_pager.rs: -------------------------------------------------------------------------------- 1 | const DEFAULT_ENV: &[(&str, &str)] = &[("LESS", "FRX"), ("LV", "-c")]; 2 | const REQUIRED_ENV: &[(&str, &str)] = &[("LESSCHARSET", "UTF-8")]; 3 | 4 | pub(crate) struct Pager { 5 | cmd: Option, 6 | } 7 | 8 | impl Pager { 9 | pub(crate) fn stdout(args: &str) -> Self { 10 | let cmd = anstream::stdout() 11 | .is_terminal() 12 | .then(|| parse(args)) 13 | .flatten(); 14 | Self { cmd } 15 | } 16 | 17 | pub(crate) fn start(&mut self) -> ActivePager { 18 | let stdout = anstream::stdout().lock(); 19 | if let Some(cmd) = &mut self.cmd { 20 | // should use pager instead of stderr 21 | if let Ok(p) = cmd.spawn() { 22 | let stderr = anstream::stderr() 23 | .is_terminal() 24 | .then(|| anstream::stderr().lock()); 25 | ActivePager { 26 | primary: stdout, 27 | _secondary: stderr, 28 | pager: Some(p), 29 | } 30 | } else { 31 | ActivePager { 32 | primary: stdout, 33 | _secondary: None, 34 | pager: None, 35 | } 36 | } 37 | } else { 38 | ActivePager { 39 | primary: stdout, 40 | _secondary: None, 41 | pager: None, 42 | } 43 | } 44 | } 45 | } 46 | 47 | pub(crate) struct ActivePager { 48 | primary: anstream::AutoStream>, 49 | _secondary: Option>>, 50 | pager: Option, 51 | } 52 | 53 | impl ActivePager { 54 | pub(crate) fn as_writer(&mut self) -> std::io::Result<&mut dyn std::io::Write> { 55 | if let Some(pager) = &mut self.pager { 56 | pager 57 | .stdin 58 | .as_mut() 59 | .map(|s| { 60 | let s: &mut dyn std::io::Write = s; 61 | s 62 | }) 63 | .ok_or_else(|| { 64 | std::io::Error::new( 65 | std::io::ErrorKind::BrokenPipe, 66 | "could not access pager stdin", 67 | ) 68 | }) 69 | } else { 70 | Ok(&mut self.primary) 71 | } 72 | } 73 | } 74 | 75 | impl Drop for ActivePager { 76 | fn drop(&mut self) { 77 | if let Some(pager) = &mut self.pager { 78 | let _ = pager.wait(); 79 | } 80 | } 81 | } 82 | 83 | fn parse(args: &str) -> Option { 84 | let mut args = shlex::Shlex::new(args); 85 | let cmd = args.next()?; 86 | if cmd == "cat" { 87 | return None; 88 | } 89 | let mut cmd = std::process::Command::new(cmd); 90 | cmd.stdin(std::process::Stdio::piped()); 91 | cmd.args(args); 92 | cmd.envs(REQUIRED_ENV.iter().copied()); 93 | cmd.envs( 94 | DEFAULT_ENV 95 | .iter() 96 | .copied() 97 | .filter(|(k, _)| std::env::var_os(k).is_none()), 98 | ); 99 | Some(cmd) 100 | } 101 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | pub(crate) fn init_logging( 4 | level: clap_verbosity_flag::Verbosity, 5 | colored: bool, 6 | ) { 7 | if let Some(level) = level.log_level() { 8 | let palette = if colored { 9 | Palette::colored() 10 | } else { 11 | Palette::plain() 12 | }; 13 | 14 | let mut builder = env_logger::Builder::new(); 15 | builder.write_style(if colored { 16 | env_logger::WriteStyle::Always 17 | } else { 18 | env_logger::WriteStyle::Never 19 | }); 20 | 21 | builder.filter(None, level.to_level_filter()); 22 | 23 | if level == log::LevelFilter::Trace || level == log::LevelFilter::Debug { 24 | builder.format_timestamp_secs(); 25 | } else { 26 | builder.format(move |f, record| match record.level() { 27 | log::Level::Error => { 28 | writeln!(f, "{}: {}", palette.error(record.level()), record.args()) 29 | } 30 | log::Level::Warn => { 31 | writeln!(f, "{}: {}", palette.warn(record.level()), record.args()) 32 | } 33 | log::Level::Info => writeln!(f, "{}", record.args()), 34 | log::Level::Debug => { 35 | writeln!(f, "{}: {}", palette.debug(record.level()), record.args()) 36 | } 37 | log::Level::Trace => { 38 | writeln!(f, "{}: {}", palette.trace(record.level()), record.args()) 39 | } 40 | }); 41 | } 42 | 43 | builder.init(); 44 | } 45 | } 46 | 47 | #[derive(Copy, Clone, Default, Debug)] 48 | struct Palette { 49 | error: anstyle::Style, 50 | warn: anstyle::Style, 51 | debug: anstyle::Style, 52 | trace: anstyle::Style, 53 | } 54 | 55 | impl Palette { 56 | pub(crate) fn colored() -> Self { 57 | Self { 58 | error: anstyle::AnsiColor::Red.on_default() | anstyle::Effects::BOLD, 59 | warn: anstyle::AnsiColor::Yellow.on_default(), 60 | debug: anstyle::AnsiColor::Blue.on_default(), 61 | trace: anstyle::AnsiColor::Cyan.on_default(), 62 | } 63 | } 64 | 65 | pub(crate) fn plain() -> Self { 66 | Self::default() 67 | } 68 | 69 | pub(crate) fn error(self, display: D) -> Styled { 70 | Styled::new(display, self.error) 71 | } 72 | 73 | pub(crate) fn warn(self, display: D) -> Styled { 74 | Styled::new(display, self.warn) 75 | } 76 | 77 | pub(crate) fn debug(self, display: D) -> Styled { 78 | Styled::new(display, self.debug) 79 | } 80 | 81 | pub(crate) fn trace(self, display: D) -> Styled { 82 | Styled::new(display, self.trace) 83 | } 84 | } 85 | 86 | #[derive(Debug)] 87 | pub(crate) struct Styled { 88 | display: D, 89 | style: anstyle::Style, 90 | } 91 | 92 | impl Styled { 93 | pub(crate) fn new(display: D, style: anstyle::Style) -> Self { 94 | Self { display, style } 95 | } 96 | } 97 | 98 | impl std::fmt::Display for Styled { 99 | #[inline] 100 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 101 | if f.alternate() { 102 | write!(f, "{}", self.style.render())?; 103 | self.display.fmt(f)?; 104 | write!(f, "{}", self.style.render_reset())?; 105 | Ok(()) 106 | } else { 107 | self.display.fmt(f) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::collapsible_else_if)] 2 | #![allow(clippy::let_and_return)] 3 | #![allow(clippy::if_same_then_else)] 4 | 5 | use clap::Parser; 6 | use proc_exit::prelude::*; 7 | 8 | mod args; 9 | mod assets; 10 | mod blame; 11 | mod config; 12 | mod git2_config; 13 | mod git_pager; 14 | mod logger; 15 | 16 | use crate::git2_config::Config; 17 | use crate::git_pager::Pager; 18 | 19 | fn main() { 20 | human_panic::setup_panic!(); 21 | let result = run(); 22 | proc_exit::exit(result); 23 | } 24 | 25 | fn run() -> proc_exit::ExitResult { 26 | let mut config = Config::system(); 27 | match config.get(&git2_config::COLOR_UI) { 28 | git2_config::ColorWhen::Always => anstream::ColorChoice::Always, 29 | git2_config::ColorWhen::Auto => anstream::ColorChoice::Auto, 30 | git2_config::ColorWhen::Never => anstream::ColorChoice::Never, 31 | } 32 | .write_global(); 33 | 34 | // clap's `get_matches` uses Failure rather than Usage, so bypass it for `get_matches_safe`. 35 | let args = args::Args::parse(); 36 | 37 | args.color.write_global(); 38 | let colored_stderr = !matches!( 39 | anstream::AutoStream::choice(&std::io::stderr()), 40 | anstream::ColorChoice::Never 41 | ); 42 | 43 | logger::init_logging(args.verbose, colored_stderr); 44 | 45 | if let Some(current_dir) = args.current_dir.as_deref() { 46 | let current_dir = current_dir 47 | .iter() 48 | .fold(std::path::PathBuf::new(), |current, next| { 49 | current.join(next) 50 | }); 51 | log::trace!("CWD={}", current_dir.display()); 52 | std::env::set_current_dir(current_dir).with_code(proc_exit::Code::FAILURE)?; 53 | } 54 | 55 | if let Some(output_path) = args.dump_config.as_deref() { 56 | config::dump_config(output_path, &mut config)?; 57 | } else if args.list_languages { 58 | list_languages(&mut config)?; 59 | } else if args.list_themes { 60 | list_themes(&mut config)?; 61 | } else if args.acknowledgements { 62 | use std::io::Write; 63 | let _ = writeln!(anstream::stdout(), "{}", assets::get_acknowledgements()); 64 | } else if args.diagnostic { 65 | use bugreport::{ 66 | bugreport, 67 | collector::{ 68 | CommandLine, CompileTimeInformation, EnvironmentVariables, OperatingSystem, 69 | SoftwareVersion, 70 | }, 71 | format::Markdown, 72 | }; 73 | 74 | let mut report = bugreport!() 75 | .info(SoftwareVersion::default()) 76 | .info(OperatingSystem::default()) 77 | .info(CommandLine::default()) 78 | .info(EnvironmentVariables::list(&[ 79 | "SHELL", 80 | "PAGER", 81 | "LESS", 82 | "LESSCHARSET", 83 | "LANG", 84 | "LC_ALL", 85 | "GIT_PAGER", 86 | // Skipping `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_*` and `GIT_CONFIG_VALUE_*` 87 | "GIT_CONFIG_PARAMETERS", 88 | "COLORTERM", 89 | "TERM", 90 | "NO_COLOR", 91 | "CLICOLOR", 92 | "CLICOLOR_FORCE", 93 | ])) 94 | .info(CompileTimeInformation::default()); 95 | 96 | report.print::(); 97 | } else if let Some(file_path) = args.file.as_deref() { 98 | blame::blame(file_path, &mut config, &args)?; 99 | } else { 100 | unreachable!("clap ensured a mode exists"); 101 | } 102 | 103 | Ok(()) 104 | } 105 | 106 | fn list_languages(config: &mut Config) -> proc_exit::ExitResult { 107 | let total_width = terminal_size::terminal_size() 108 | .map(|(w, _h)| w.0) 109 | .or_else(|| std::env::var_os("COLUMNS").and_then(|s| s.to_str()?.parse::().ok())) 110 | .unwrap_or(80) as usize; 111 | 112 | let pager = config.get(&git2_config::PAGER); 113 | let mut pager = Pager::stdout(&pager); 114 | let mut pager = pager.start(); 115 | let pager = pager.as_writer().with_code(proc_exit::Code::FAILURE)?; 116 | 117 | let syntax_set = assets::load_syntaxes(); 118 | let name_width = syntax_set 119 | .syntaxes() 120 | .iter() 121 | .map(|s| s.name.len()) 122 | .max() 123 | .unwrap_or(0) 124 | + 1; 125 | let syntax_width = total_width - name_width; 126 | let wrap = textwrap::Options::new(syntax_width) 127 | .break_words(false) 128 | .word_separator(textwrap::WordSeparator::AsciiSpace) 129 | .wrap_algorithm(textwrap::WrapAlgorithm::FirstFit); 130 | for syntax in syntax_set.syntaxes() { 131 | let ext = syntax.file_extensions.join(", "); 132 | let ext = textwrap::wrap(&ext, &wrap); 133 | for (i, ext_line) in ext.into_iter().enumerate() { 134 | let mut name = if i == 0 { 135 | syntax.name.clone() 136 | } else { 137 | "".to_owned() 138 | }; 139 | let mut ext_line = ext_line.into_owned(); 140 | name = format!( 141 | "{}{}{}", 142 | anstyle::Effects::BOLD.render(), 143 | name, 144 | anstyle::Reset.render() 145 | ); 146 | ext_line = format!( 147 | "{}{}{}", 148 | anstyle::AnsiColor::Green.render_fg(), 149 | ext_line, 150 | anstyle::Reset.render() 151 | ); 152 | let _ = writeln!(pager, "{name: proc_exit::ExitResult { 160 | let colored_stdout = 161 | anstream::AutoStream::choice(&std::io::stdout()) != anstream::ColorChoice::Never; 162 | let pager = config.get(&git2_config::PAGER); 163 | let mut pager = Pager::stdout(&pager); 164 | let mut pager = pager.start(); 165 | let pager = pager.as_writer().with_code(proc_exit::Code::FAILURE)?; 166 | 167 | let theme_set = assets::load_themes(); 168 | if colored_stdout { 169 | let syntax_set = assets::load_syntaxes(); 170 | let syntax = syntax_set 171 | .find_syntax_by_name("Rust") 172 | .expect("always included"); 173 | for name in theme_set.themes() { 174 | let theme = theme_set.get(name).unwrap(); 175 | let mut highlighter = blame::Highlighter::enabled(syntax, theme); 176 | let _ = writeln!( 177 | pager, 178 | "Theme: {}{}{}", 179 | anstyle::Effects::BOLD.render(), 180 | name, 181 | anstyle::Reset.render() 182 | ); 183 | let _ = writeln!(pager); 184 | for line in THEME_PREVIEW_DATA.lines() { 185 | let _ = writeln!( 186 | pager, 187 | "{}{}", 188 | highlighter.highlight_line(line, &syntax_set).unwrap(), 189 | anstyle::Reset.render() 190 | ); 191 | } 192 | let _ = writeln!(pager); 193 | } 194 | } else { 195 | for name in theme_set.themes() { 196 | let _ = writeln!(pager, "{name}"); 197 | } 198 | } 199 | Ok(()) 200 | } 201 | 202 | const THEME_PREVIEW_DATA: &str = include_str!("../assets/theme_preview.rs"); 203 | -------------------------------------------------------------------------------- /tests/testsuite/cli.rs: -------------------------------------------------------------------------------- 1 | use snapbox::prelude::*; 2 | 3 | #[test] 4 | fn basic() { 5 | let root = snapbox::dir::DirRoot::mutable_temp().unwrap(); 6 | let root_path = root.path().unwrap(); 7 | let plan = git_fixture::TodoList { 8 | commands: vec![ 9 | git_fixture::Command::Tree(git_fixture::Tree { 10 | files: [("basic.js", "test('arg1');")] 11 | .into_iter() 12 | .map(|(p, c)| (p.into(), c.into())) 13 | .collect::>(), 14 | message: Some("A".to_owned()), 15 | author: None, 16 | }), 17 | git_fixture::Command::Branch("main".into()), 18 | ], 19 | ..Default::default() 20 | }; 21 | plan.run(root_path).unwrap(); 22 | 23 | snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("git-dive")) 24 | .arg("basic.js") 25 | .current_dir(root_path) 26 | .assert() 27 | .success() 28 | .stdout_eq( 29 | "\ 30 | HEAD 1 │ test('arg1'); 31 | " 32 | .raw(), 33 | ) 34 | .stderr_eq( 35 | "\ 36 | ", 37 | ); 38 | 39 | root.close().unwrap(); 40 | } 41 | 42 | #[test] 43 | fn js_highlight_panics() { 44 | let root = snapbox::dir::DirRoot::mutable_temp().unwrap(); 45 | let root_path = root.path().unwrap(); 46 | let plan = git_fixture::TodoList { 47 | commands: vec![ 48 | git_fixture::Command::Tree(git_fixture::Tree { 49 | files: [("basic.js", "test('arg1');")] 50 | .into_iter() 51 | .map(|(p, c)| (p.into(), c.into())) 52 | .collect::>(), 53 | message: Some("A".to_owned()), 54 | author: None, 55 | }), 56 | git_fixture::Command::Branch("main".into()), 57 | ], 58 | ..Default::default() 59 | }; 60 | plan.run(root_path).unwrap(); 61 | 62 | snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("git-dive")) 63 | .arg("basic.js") 64 | .current_dir(root_path) 65 | .env("CLICOLOR_FORCE", "1") 66 | .assert() 67 | .success() 68 | .stderr_eq( 69 | "\ 70 | ", 71 | ); 72 | 73 | root.close().unwrap(); 74 | } 75 | -------------------------------------------------------------------------------- /tests/testsuite/docs.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | #[cfg(unix)] 3 | fn screenshot() { 4 | use term_transcript::{test::TestConfig, ShellOptions}; 5 | 6 | let scratchpad = snapbox::dir::DirRoot::mutable_temp().unwrap(); 7 | let scratchpad_path = scratchpad.path().unwrap(); 8 | let status = std::process::Command::new("git") 9 | .arg("clone") 10 | .arg("https://github.com/gitext-rs/git-dive.git") 11 | .current_dir(scratchpad_path) 12 | .status() 13 | .unwrap(); 14 | assert!(status.success()); 15 | let repo_path = scratchpad_path.join("git-dive"); 16 | let status = std::process::Command::new("git") 17 | .arg("checkout") 18 | .arg("ae51fc8be9e4ec83d47a6d83c80d015212a396a5") 19 | .current_dir(&repo_path) 20 | .status() 21 | .unwrap(); 22 | assert!(status.success()); 23 | 24 | let cmd_path = snapbox::cmd::cargo_bin!("git-dive"); 25 | 26 | // HACK: term_transcript doesn't allow non-UTF8 paths 27 | let cmd = "git-dive Cargo.toml"; 28 | 29 | TestConfig::new( 30 | ShellOptions::::sh() 31 | .with_alias("git-dive", &cmd_path.to_string_lossy()) 32 | .with_current_dir(&repo_path) 33 | .with_env("CLICOLOR_FORCE", "1") 34 | // Make it independent of the tester's user config 35 | .with_env("GIT_CONFIG_PARAMETERS", "'dive.theme'='Monokai Extended'"), 36 | ) 37 | .test("assets/screenshot.svg", [cmd]); 38 | 39 | scratchpad.close().unwrap(); 40 | } 41 | -------------------------------------------------------------------------------- /tests/testsuite/main.rs: -------------------------------------------------------------------------------- 1 | automod::dir!("tests/testsuite"); 2 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | # flate crate 3 | flate = "flate" 4 | --------------------------------------------------------------------------------