diff --git a/.clippy.toml b/.clippy.toml index 2f3418ba..4bdda4fc 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,2 +1,2 @@ -msrv = "1.82.0" +msrv = "1.88.0" cognitive-complexity-threshold = 18 diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 65fb6646..50d52359 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,10 +2,13 @@ name: CD on: push: - tags: - - '*' + tags: + - "*" workflow_dispatch: +permissions: + contents: write + jobs: release: strategy: @@ -15,116 +18,117 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Get version - id: get_version - run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + - name: Get version + id: get_version + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT - - name: Restore cargo cache - uses: Swatinem/rust-cache@v2 - env: - cache-name: ci - with: - shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable + - name: Restore cargo cache + uses: Swatinem/rust-cache@v2 + env: + cache-name: ci + with: + shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: clippy + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy - - name: Build - if: matrix.os != 'ubuntu-22.04' - env: - GITUI_RELEASE: 1 - run: cargo build - - name: Run tests - if: matrix.os != 'ubuntu-22.04' - run: make test - - name: Run clippy - if: matrix.os != 'ubuntu-22.04' - run: | - cargo clean - make clippy + - uses: taiki-e/install-action@nextest - - name: Setup MUSL - if: matrix.os == 'ubuntu-latest' - run: | - rustup target add x86_64-unknown-linux-musl - sudo apt-get -qq install musl-tools + - name: Build + if: matrix.os != 'ubuntu-22.04' + env: + GITUI_RELEASE: 1 + run: cargo build + - name: Run tests + if: matrix.os != 'ubuntu-22.04' + run: make test + - name: Run clippy + if: matrix.os != 'ubuntu-22.04' + run: | + cargo clean + make clippy - - name: Setup ARM toolchain - if: matrix.os == 'ubuntu-22.04' - run: | - rustup target add aarch64-unknown-linux-gnu - rustup target add armv7-unknown-linux-gnueabihf - rustup target add arm-unknown-linux-gnueabihf + - name: Setup MUSL + if: matrix.os == 'ubuntu-latest' + run: | + rustup target add x86_64-unknown-linux-musl + sudo apt-get -qq install musl-tools - curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz - curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz + - name: Setup ARM toolchain + if: matrix.os == 'ubuntu-22.04' + run: | + rustup target add aarch64-unknown-linux-gnu + rustup target add armv7-unknown-linux-gnueabihf + rustup target add arm-unknown-linux-gnueabihf - tar xf $GITHUB_WORKSPACE/aarch64.tar.xz - tar xf $GITHUB_WORKSPACE/arm.tar.xz + curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz + curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz - echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH - echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH + tar xf $GITHUB_WORKSPACE/aarch64.tar.xz + tar xf $GITHUB_WORKSPACE/arm.tar.xz - - name: Build Release Mac - if: matrix.os == 'macos-latest' - env: - GITUI_RELEASE: 1 - run: make release-mac - - name: Build Release Mac x86 - if: matrix.os == 'macos-latest' - env: - GITUI_RELEASE: 1 - run: | - rustup target add x86_64-apple-darwin - make release-mac-x86 - - name: Build Release Linux - if: matrix.os == 'ubuntu-latest' - env: - GITUI_RELEASE: 1 - run: make release-linux-musl - - name: Build Release Win - if: matrix.os == 'windows-latest' - env: - GITUI_RELEASE: 1 - run: make release-win - - name: Build Release Linux ARM - if: matrix.os == 'ubuntu-22.04' - env: - GITUI_RELEASE: 1 - run: make release-linux-arm + echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH + echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH - - name: Set SHA - if: matrix.os == 'macos-latest' - id: shasum - run: | - echo sha="$(shasum -a 256 ./release/gitui-mac.tar.gz | awk '{printf $1}')" >> $GITHUB_OUTPUT + - name: Build Release Mac + if: matrix.os == 'macos-latest' + env: + GITUI_RELEASE: 1 + run: make release-mac + - name: Build Release Mac x86 + if: matrix.os == 'macos-latest' + env: + GITUI_RELEASE: 1 + run: | + rustup target add x86_64-apple-darwin + make release-mac-x86 + - name: Build Release Linux + if: matrix.os == 'ubuntu-latest' + env: + GITUI_RELEASE: 1 + run: make release-linux-musl + - name: Build Release Win + if: matrix.os == 'windows-latest' + env: + GITUI_RELEASE: 1 + run: make release-win + - name: Build Release Linux ARM + if: matrix.os == 'ubuntu-22.04' + env: + GITUI_RELEASE: 1 + run: make release-linux-arm - - name: Extract release notes - if: matrix.os == 'ubuntu-latest' - id: release_notes - uses: ffurrer2/extract-release-notes@v2 - - name: Release - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body: ${{ steps.release_notes.outputs.release_notes }} - prerelease: ${{ contains(github.ref, '-') }} - files: | - ./release/*.tar.gz - ./release/*.zip - ./release/*.msi + - name: Set SHA + if: matrix.os == 'macos-latest' + id: shasum + run: | + echo sha="$(shasum -a 256 ./release/gitui-mac.tar.gz | awk '{printf $1}')" >> $GITHUB_OUTPUT - - name: Bump homebrew-core formula - uses: mislav/bump-homebrew-formula-action@v3 - if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases - env: - COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }} - with: - formula-name: gitui - # https://github.com/mislav/bump-homebrew-formula-action/issues/58 - formula-path: Formula/g/gitui.rb + - name: Extract release notes + if: matrix.os == 'ubuntu-latest' + id: release_notes + uses: ffurrer2/extract-release-notes@v2 + + - name: Release + uses: softprops/action-gh-release@v2 + with: + body: ${{ steps.release_notes.outputs.release_notes }} + prerelease: ${{ contains(github.ref, '-') }} + files: | + ./release/*.tar.gz + ./release/*.zip + ./release/*.msi + + - name: Bump homebrew-core formula + uses: mislav/bump-homebrew-formula-action@v3 + if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases + env: + COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }} + with: + formula-name: gitui + # https://github.com/mislav/bump-homebrew-formula-action/issues/58 + formula-path: Formula/g/gitui.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d31649ed..e34dd840 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - rust: [nightly, stable, "1.82"] + rust: [nightly, stable, "1.88"] runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.rust == 'nightly' }} @@ -47,6 +47,8 @@ jobs: - name: Rustup Show run: rustup show + - uses: taiki-e/install-action@nextest + - name: Build Debug run: | cargo build @@ -92,7 +94,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [nightly, stable, "1.82"] + rust: [nightly, stable, "1.88"] continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - uses: actions/checkout@v4 @@ -121,6 +123,8 @@ jobs: - name: Rustup Show run: rustup show + - uses: taiki-e/install-action@nextest + - name: Setup MUSL run: | sudo apt-get -qq install musl-tools @@ -144,7 +148,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [nightly, stable, "1.82"] + rust: [nightly, stable, "1.88"] continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - uses: actions/checkout@v4 @@ -195,7 +199,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [nightly, stable, "1.82"] + rust: [nightly, stable, "1.88"] continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - uses: actions/checkout@v4 @@ -249,10 +253,16 @@ jobs: - run: cargo fmt -- --check - - name: cargo-sort + - name: tombi install + uses: tombi-toml/setup-tombi@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + version: '0.9.0' + + - name: tombi check run: | - cargo install cargo-sort --force - cargo sort -c -w + tombi format --check - name: cargo-deny install run: | @@ -303,26 +313,13 @@ jobs: name: Test Homebrew Formula (macOS) runs-on: macos-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@master - name: Install stable Rust uses: actions-rs/toolchain@v1 with: toolchain: stable - - name: Install Homebrew - run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - - - name: Set up Homebrew in PATH - run: | - echo "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_PATH - - - name: Update Homebrew - run: brew update - - name: Let Homebrew build gitui from source - run: brew install --head --build-from-source gitui - - - name: Run Homebrew test - run: brew test gitui + run: brew install --build-from-source gitui diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8946efa7..83080a59 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -2,7 +2,7 @@ name: Build Nightly Releases on: schedule: - - cron: '0 3 * * *' + - cron: "0 3 * * *" workflow_dispatch: env: @@ -14,112 +14,112 @@ jobs: strategy: fail-fast: false matrix: - os: [ - ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04 - ] + os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Restore cargo cache - uses: Swatinem/rust-cache@v2 - env: - cache-name: ci - with: - shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable + - name: Restore cargo cache + uses: Swatinem/rust-cache@v2 + env: + cache-name: ci + with: + shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: clippy + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy - # ideally we trigger the nightly build/deploy only if the normal nightly CI finished successfully - - name: Run tests - if: matrix.os != 'ubuntu-22.04' - run: make test - - name: Run clippy - if: matrix.os != 'ubuntu-22.04' - run: | - cargo clean - make clippy + - uses: taiki-e/install-action@nextest - - name: Setup MUSL - if: matrix.os == 'ubuntu-latest' - run: | - rustup target add x86_64-unknown-linux-musl - sudo apt-get -qq install musl-tools + # ideally we trigger the nightly build/deploy only if the normal nightly CI finished successfully + - name: Run tests + if: matrix.os != 'ubuntu-22.04' + run: make test + - name: Run clippy + if: matrix.os != 'ubuntu-22.04' + run: | + cargo clean + make clippy - - name: Setup ARM toolchain - if: matrix.os == 'ubuntu-22.04' - run: | - rustup target add aarch64-unknown-linux-gnu - rustup target add armv7-unknown-linux-gnueabihf - rustup target add arm-unknown-linux-gnueabihf + - name: Setup MUSL + if: matrix.os == 'ubuntu-latest' + run: | + rustup target add x86_64-unknown-linux-musl + sudo apt-get -qq install musl-tools - curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz - curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz + - name: Setup ARM toolchain + if: matrix.os == 'ubuntu-22.04' + run: | + rustup target add aarch64-unknown-linux-gnu + rustup target add armv7-unknown-linux-gnueabihf + rustup target add arm-unknown-linux-gnueabihf - tar xf $GITHUB_WORKSPACE/aarch64.tar.xz - tar xf $GITHUB_WORKSPACE/arm.tar.xz + curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz + curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz - echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH - echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH + tar xf $GITHUB_WORKSPACE/aarch64.tar.xz + tar xf $GITHUB_WORKSPACE/arm.tar.xz - - name: Build Release Mac - if: matrix.os == 'macos-latest' - run: make release-mac - - name: Build Release Mac x86 - if: matrix.os == 'macos-latest' - run: | - rustup target add x86_64-apple-darwin - make release-mac-x86 - - name: Build Release Linux - if: matrix.os == 'ubuntu-latest' - run: make release-linux-musl - - name: Build Release Win - if: matrix.os == 'windows-latest' - run: make release-win - - name: Build Release Linux ARM - if: matrix.os == 'ubuntu-22.04' - run: make release-linux-arm + echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH + echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH - - name: Ubuntu 22.04 Upload Artifact - if: matrix.os == 'ubuntu-22.04' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} - run: | - aws s3 cp ./release/gitui-linux-armv7.tar.gz $AWS_BUCKET_NAME - aws s3 cp ./release/gitui-linux-arm.tar.gz $AWS_BUCKET_NAME - aws s3 cp ./release/gitui-linux-aarch64.tar.gz $AWS_BUCKET_NAME + - name: Build Release Mac + if: matrix.os == 'macos-latest' + run: make release-mac + - name: Build Release Mac x86 + if: matrix.os == 'macos-latest' + run: | + rustup target add x86_64-apple-darwin + make release-mac-x86 + - name: Build Release Linux + if: matrix.os == 'ubuntu-latest' + run: make release-linux-musl + - name: Build Release Win + if: matrix.os == 'windows-latest' + run: make release-win + - name: Build Release Linux ARM + if: matrix.os == 'ubuntu-22.04' + run: make release-linux-arm - - name: Ubuntu Latest Upload Artifact - if: matrix.os == 'ubuntu-latest' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} - run: | - aws s3 cp ./release/gitui-linux-x86_64.tar.gz $AWS_BUCKET_NAME + - name: Ubuntu 22.04 Upload Artifact + if: matrix.os == 'ubuntu-22.04' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + run: | + aws s3 cp ./release/gitui-linux-armv7.tar.gz $AWS_BUCKET_NAME + aws s3 cp ./release/gitui-linux-arm.tar.gz $AWS_BUCKET_NAME + aws s3 cp ./release/gitui-linux-aarch64.tar.gz $AWS_BUCKET_NAME - - name: MacOS Upload Artifact - if: matrix.os == 'macos-latest' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} - run: | - aws s3 cp ./release/gitui-mac.tar.gz $AWS_BUCKET_NAME - aws s3 cp ./release/gitui-mac-x86.tar.gz $AWS_BUCKET_NAME + - name: Ubuntu Latest Upload Artifact + if: matrix.os == 'ubuntu-latest' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + run: | + aws s3 cp ./release/gitui-linux-x86_64.tar.gz $AWS_BUCKET_NAME - - name: Windows Upload Artifact - if: matrix.os == 'windows-latest' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} - run: | - aws s3 cp ./release/gitui-win.msi $env:AWS_BUCKET_NAME - aws s3 cp ./release/gitui-win.tar.gz $env:AWS_BUCKET_NAME + - name: MacOS Upload Artifact + if: matrix.os == 'macos-latest' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + run: | + aws s3 cp ./release/gitui-mac.tar.gz $AWS_BUCKET_NAME + aws s3 cp ./release/gitui-mac-x86.tar.gz $AWS_BUCKET_NAME + + - name: Windows Upload Artifact + if: matrix.os == 'windows-latest' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + run: | + aws s3 cp ./release/gitui-win.msi $env:AWS_BUCKET_NAME + aws s3 cp ./release/gitui-win.tar.gz $env:AWS_BUCKET_NAME diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b318c7..88823757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,40 +6,73 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased -* execute git-hooks directly if possible (on *nix) else use sh instead of bash (without reading SHELL variable) [[@Joshix](https://github.com/Joshix-1)] ([#2483](https://github.com/extrawurst/gitui/pull/2483)) -* increase MSRV from 1.81 to 1.82 [[@cruessler](https://github.com/cruessler)] + +### Changed +* Give more space to right-side diff view [[@hongquan](https://github.com/hongquan)] ([#2772](https://github.com/gitui-org/gitui/pull/2772)) +* use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting +* open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805)) + +### Fixes +* crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895)) +* when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748)) + +## [0.28.1] - 2026-03-21 + +### Changed +* support proper pre-push hook ([#2809](https://github.com/gitui-org/gitui/issues/2809)) +* improve `gitui --version` message [[@hlsxx](https://github.com/hlsxx)] ([#2838](https://github.com/gitui-org/gitui/issues/2838)) +* rust msrv bumped to `1.88` + +### Fixed +* fix extremely slow status loading in large repositories by replacing time-based cache invalidation with generation counter [[@DannyStoll1](https://github.com/DannyStoll1)] ([#2823](https://github.com/gitui-org/gitui/issues/2823)) +* fix panic when renaming or updating remote URL with no remotes configured [[@xvchris](https://github.com/xvchris)] ([#2868](https://github.com/gitui-org/gitui/issues/2868)) + +## [0.28.0] - 2025-12-14 + +**discard changes on checkout** +![discard-changes-on-checkout](assets/discard-changes-on-checkout.png) + +**go to line in blame** +![blame-goto-line](assets/blame-goto-line.png) ### Added -* Give more space to right-side diff view [[@hongquan](https://github.com/hongquan)] ([#2772](https://github.com/gitui-org/gitui/pull/2772)) -* Support pre-push hook [[@xlai89](https://github.com/xlai89)] ([#1933](https://github.com/extrawurst/gitui/issues/1933)) -* Message tab supports pageUp and pageDown [[@xlai89](https://github.com/xlai89)] ([#2623](https://github.com/extrawurst/gitui/issues/2623)) -* Files and status tab support pageUp and pageDown [[@fatpandac](https://github.com/fatpandac)] ([#1951](https://github.com/extrawurst/gitui/issues/1951)) +* support choosing checkout branch method when status is not empty [[@fatpandac](https://github.com/fatpandac)] ([#2404](https://github.com/extrawurst/gitui/issues/2404)) +* support pre-push hook [[@xlai89](https://github.com/xlai89)] ([#1933](https://github.com/extrawurst/gitui/issues/1933)) +* message tab supports pageUp and pageDown [[@xlai89](https://github.com/xlai89)] ([#2623](https://github.com/extrawurst/gitui/issues/2623)) +* files and status tab support pageUp and pageDown [[@fatpandac](https://github.com/fatpandac)] ([#1951](https://github.com/extrawurst/gitui/issues/1951)) * support loading custom syntax highlighting themes from a file [[@acuteenvy](https://github.com/acuteenvy)] ([#2565](https://github.com/gitui-org/gitui/pull/2565)) -* Select syntax highlighting theme out of the defaults from syntect [[@vasilismanol](https://github.com/vasilismanol)] ([#1931](https://github.com/extrawurst/gitui/issues/1931)) +* select syntax highlighting theme out of the defaults from syntect [[@vasilismanol](https://github.com/vasilismanol)] ([#1931](https://github.com/extrawurst/gitui/issues/1931)) * new command-line option to override the default log file path (`--logfile`) [[@acuteenvy](https://github.com/acuteenvy)] ([#2539](https://github.com/gitui-org/gitui/pull/2539)) * dx: `make check` checks Cargo.toml dependency ordering using `cargo sort` [[@naseschwarz](https://github.com/naseschwarz)] * add `use_selection_fg` to theme file to allow customizing selection foreground color [[@Upsylonbare](https://github.com/Upsylonbare)] ([#2515](https://github.com/gitui-org/gitui/pull/2515)) +* add "go to line" command for the blame view [[@andrea-berling](https://github.com/andrea-berling)] ([#2262](https://github.com/extrawurst/gitui/pull/2262)) +* add `--file` cli flag to open the files tab with the given file already selected [[@laktak](https://github.com/laktak)] ([#2510](https://github.com/gitui-org/gitui/issues/2510)) +* add the ability to specify a custom keybinding/symbols file via the cli [[@0x61nas](https://github.com/0x61nas)] ([#2731](https://github.com/gitui-org/gitui/pull/2731)) +* add mise alternative method installation [[@jylenhof](https://github.com/jylenhof)] ([#2817](https://github.com/gitui-org/gitui/pull/2817)) ### Changed +* execute git-hooks directly if possible (on *nix) else use sh instead of bash (without reading SHELL variable) [[@Joshix](https://github.com/Joshix-1)] ([#2483](https://github.com/extrawurst/gitui/pull/2483)) * improve error messages [[@acuteenvy](https://github.com/acuteenvy)] ([#2617](https://github.com/gitui-org/gitui/pull/2617)) -* increase MSRV from 1.70 to 1.81 [[@naseschwarz](https://github.com/naseschwarz)] ([#2094](https://github.com/gitui-org/gitui/issues/2094)) * improve syntax highlighting file detection [[@acuteenvy](https://github.com/acuteenvy)] ([#2524](https://github.com/extrawurst/gitui/pull/2524)) -* Updated project links to point to `gitui-org` instead of `extrawurst` [[@vasleymus](https://github.com/vasleymus)] ([#2538](https://github.com/gitui-org/gitui/pull/2538)) -* After commit: jump back to unstaged area [[@tommady](https://github.com/tommady)] ([#2476](https://github.com/extrawurst/gitui/issues/2476)) -* The default key to close the commit error message popup is now the Escape key [[@wessamfathi](https://github.com/wessamfathi)] ([#2552](https://github.com/extrawurst/gitui/issues/2552)) +* after commit: jump back to unstaged area [[@tommady](https://github.com/tommady)] ([#2476](https://github.com/extrawurst/gitui/issues/2476)) +* the default key to close the commit error message popup is now the Escape key [[@wessamfathi](https://github.com/wessamfathi)] ([#2552](https://github.com/extrawurst/gitui/issues/2552)) * use OSC52 copying in case other methods fail [[@naseschwarz](https://github.com/naseschwarz)] ([#2366](https://github.com/gitui-org/gitui/issues/2366)) * push: respect `branch.*.merge` when push default is upstream [[@vlad-anger](https://github.com/vlad-anger)] ([#2542](https://github.com/gitui-org/gitui/pull/2542)) * set the terminal title to `gitui ({repo_path})` [[@acuteenvy](https://github.com/acuteenvy)] ([#2462](https://github.com/gitui-org/gitui/issues/2462)) * respect `.mailmap` [[@acuteenvy](https://github.com/acuteenvy)] ([#2406](https://github.com/gitui-org/gitui/issues/2406)) +* use `gitoxide` for `get_tags` [[@cruessler](https://github.com/cruessler)] ([#2664](https://github.com/gitui-org/gitui/issues/2664)) +* increase MSRV to 1.82 ### Fixes * resolve `core.hooksPath` relative to `GIT_WORK_TREE` [[@naseschwarz](https://github.com/naseschwarz)] ([#2571](https://github.com/gitui-org/gitui/issues/2571)) -* yanking commit ranges no longer generates incorrect dotted range notations, but lists each individual commit [[@naseschwarz](https://github.com/naseschwarz)] (https://github.com/gitui-org/gitui/issues/2576) -* print slightly nicer errors when failing to create a directory [[@linkmauve](https://github.com/linkmauve)] (https://github.com/gitui-org/gitui/pull/2728) -* When the terminal is insufficient to display all the commands, the cmdbar_bg configuration color does not fully take effect. ([#2347](https://github.com/extrawurst/gitui/issues/2347)) +* yanking commit ranges no longer generates incorrect dotted range notations, but lists each individual commit [[@naseschwarz](https://github.com/naseschwarz)] ([#2576](https://github.com/gitui-org/gitui/issues/2576)) +* print slightly nicer errors when failing to create a directory [[@linkmauve](https://github.com/linkmauve)] ([#2728](https://github.com/gitui-org/gitui/pull/2728)) +* when the terminal is insufficient to display all the commands, the cmdbar_bg configuration color does not fully take effect. ([#2347](https://github.com/extrawurst/gitui/issues/2347)) * disable blame and history popup keybinds for untracked files [[@kpbaks](https://github.com/kpbaks)] ([#2489](https://github.com/gitui-org/gitui/pull/2489)) +* overwrites committer on amend of unsigned commits [[@cruessler](https://github.com/cruessler)] ([#2784](https://github.com/gitui-org/gitui/issues/2784)) +* Updated project links to point to `gitui-org` instead of `extrawurst` [[@vasleymus](https://github.com/vasleymus)] ([#2538](https://github.com/gitui-org/gitui/pull/2538)) -## [0.27.0] - 2024-01-14 +## [0.27.0] - 2025-01-14 **new: manage remotes** diff --git a/Cargo.lock b/Cargo.lock index 6b1f15ed..1534622d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -127,15 +127,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "arrayvec" @@ -145,7 +148,7 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asyncgit" -version = "0.27.0" +version = "0.28.1" dependencies = [ "bitflags 2.10.0", "crossbeam-channel", @@ -167,11 +170,20 @@ dependencies = [ "serial_test", "ssh-key", "tempfile", - "thiserror", - "unicode-truncate 2.0.0", + "thiserror 2.0.18", + "unicode-truncate", "url", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -231,15 +243,30 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -326,6 +353,12 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -334,15 +367,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytesize" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" - -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "castaway" @@ -375,9 +402,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" @@ -392,9 +425,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -413,18 +446,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -452,9 +485,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", "cfg-if", @@ -465,12 +498,33 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -546,6 +600,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.10.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.3", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -577,6 +650,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "ctr" version = "0.9.2" @@ -609,7 +692,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -633,7 +716,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -644,7 +727,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -661,6 +744,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + [[package]] name = "der" version = "0.7.9" @@ -673,13 +762,35 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "diff" version = "0.1.13" @@ -727,7 +838,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", ] [[package]] @@ -738,9 +858,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "easy-cast" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72852736692ec862655eca398c9bb1b476161b563c9f80f45f4808b9629750d6" +checksum = "23f40539c229fc2e4674bdecdf24bfcc2cb83631ca911c78a035fa9f2381c32b" [[package]] name = "ecdsa" @@ -802,6 +922,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -813,9 +939,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -829,9 +955,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -856,13 +982,32 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + [[package]] name = "fancy-regex" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata", "regex-syntax", ] @@ -899,6 +1044,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "filetime" version = "0.2.25" @@ -913,12 +1069,24 @@ dependencies = [ [[package]] name = "filetreelist" -version = "0.5.2" +version = "0.6.0" dependencies = [ "pretty_assertions", - "thiserror", + "thiserror 2.0.18", ] +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.1" @@ -965,31 +1133,6 @@ dependencies = [ "libc", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - [[package]] name = "futures-core" version = "0.3.31" @@ -1007,18 +1150,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - [[package]] name = "futures-task" version = "0.3.31" @@ -1031,12 +1162,8 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1081,10 +1208,23 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gh-emoji" version = "1.0.8" @@ -1128,14 +1268,14 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "git2" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ "bitflags 2.10.0", "libc", @@ -1148,7 +1288,7 @@ dependencies = [ [[package]] name = "git2-hooks" -version = "0.5.0" +version = "0.7.0" dependencies = [ "git2", "git2-testing", @@ -1157,7 +1297,7 @@ dependencies = [ "pretty_assertions", "shellexpand", "tempfile", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1172,7 +1312,7 @@ dependencies = [ [[package]] name = "gitui" -version = "0.27.0" +version = "0.28.1" dependencies = [ "anyhow", "asyncgit", @@ -1185,15 +1325,17 @@ dependencies = [ "chrono", "clap", "crossbeam-channel", - "crossterm", + "crossterm 0.29.0", "dirs", "easy-cast", "env_logger", "filetreelist", "fuzzy-matcher", "gh-emoji", + "git2-testing", "indexmap", - "itertools 0.14.0", + "insta", + "itertools", "log", "notify", "notify-debouncer-mini", @@ -1201,6 +1343,7 @@ dependencies = [ "parking_lot_core", "pretty_assertions", "ratatui", + "ratatui-textarea", "rayon-core", "ron", "scopeguard", @@ -1211,19 +1354,18 @@ dependencies = [ "struct-patch", "syntect", "tempfile", - "tui-textarea", "two-face", "unicode-segmentation", - "unicode-truncate 2.0.0", + "unicode-truncate", "unicode-width 0.2.0", "which", ] [[package]] name = "gix" -version = "0.74.1" +version = "0.78.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd3a6fea165debe0e80648495f894aa2371a771e3ceb7a7dcc304f1c4344c43" +checksum = "3428a03ace494ae40308bd3df0b37e7eb7403e24389f27abdff30abf2b5adf17" dependencies = [ "gix-actor", "gix-attributes", @@ -1234,6 +1376,7 @@ dependencies = [ "gix-diff", "gix-dir", "gix-discover", + "gix-error", "gix-features", "gix-filter", "gix-fs", @@ -1266,28 +1409,28 @@ dependencies = [ "gix-validate", "gix-worktree", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-actor" -version = "0.35.6" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987a51a7e66db6ef4dc030418eb2a42af6b913a79edd8670766122d8af3ba59e" +checksum = "b50ce5433eaa46187349e59089eea71b0397caa71991b2fa3e124120426d7d15" dependencies = [ "bstr", "gix-date", "gix-utils", "itoa", - "thiserror", + "thiserror 2.0.18", "winnow", ] [[package]] name = "gix-attributes" -version = "0.28.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6591add69314fc43db078076a8da6f07957c65abb0b21c3e1b6a3cf50aa18d" +checksum = "f868f013fee0ebb5c85fae848c34a0b9ef7438acfbaec0c82a3cdbd5eac730a0" dependencies = [ "bstr", "gix-glob", @@ -1296,7 +1439,7 @@ dependencies = [ "gix-trace", "kstring", "smallvec", - "thiserror", + "thiserror 2.0.18", "unicode-bom", ] @@ -1306,23 +1449,23 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e150161b8a75b5860521cb876b506879a3376d3adc857ec7a9d35e7c6a5e531" dependencies = [ - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-chunk" -version = "0.4.12" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c356b3825677cb6ff579551bb8311a81821e184453cbd105e2fc5311b288eeb" +checksum = "63e516efaac951ed21115b11d5514b120c26ccb493d0c0b9ea6cc10edf4fdf44" dependencies = [ - "thiserror", + "gix-error", ] [[package]] name = "gix-command" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "095c8367c9dc4872a7706fbc39c7f34271b88b541120a4365ff0e36366f66e62" +checksum = "745bc165b7da500acc26d24888379ae0dfd1ecabe3a47420cdcb92feefb0561d" dependencies = [ "bstr", "gix-path", @@ -1333,22 +1476,22 @@ dependencies = [ [[package]] name = "gix-commitgraph" -version = "0.30.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826994ff6c01f1ff00d6a1844d7506717810a91ffed143da71e3bf39369751ef" +checksum = "d0dda2e4d5a61d4a16a780f61f2b7e9406ad1f8da97c35c09ef501f3fdf74de0" dependencies = [ "bstr", "gix-chunk", + "gix-error", "gix-hash", "memmap2", - "thiserror", ] [[package]] name = "gix-config" -version = "0.47.1" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e74f57ea99025de9207db53488be4d59cf2000f617964c1b550880524fefbc3" +checksum = "9a153dd4f5789fdf242e19e3f7105f2a114df198570225976fe4a108bac9dee4" dependencies = [ "bstr", "gix-config-value", @@ -1359,42 +1502,42 @@ dependencies = [ "gix-sec", "memchr", "smallvec", - "thiserror", + "thiserror 2.0.18", "unicode-bom", "winnow", ] [[package]] name = "gix-config-value" -version = "0.15.3" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c489abb061c74b0c3ad790e24a606ef968cebab48ec673d6a891ece7d5aef64" +checksum = "563361198101cedc975fe5760c91ac2e4126eec22216e81b659b45289feaf1ea" dependencies = [ "bitflags 2.10.0", "bstr", "gix-path", "libc", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-date" -version = "0.10.7" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "661245d045aa7c16ba4244daaabd823c562c3e45f1f25b816be2c57ee09f2171" +checksum = "12553b32d1da25671f31c0b084bf1e5cb6d5ef529254d04ec33cdc890bd7f687" dependencies = [ "bstr", + "gix-error", "itoa", "jiff", "smallvec", - "thiserror", ] [[package]] name = "gix-diff" -version = "0.54.1" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd78d9da421baca219a650d71c797706117095635d7963f21bb6fdf2410abe04" +checksum = "26bcd367b2c5dbf6bec9ce02ca59eab179fc82cf39f15ec83549ee25c255c99f" dependencies = [ "bstr", "gix-attributes", @@ -1411,14 +1554,14 @@ dependencies = [ "gix-traverse", "gix-worktree", "imara-diff", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-dir" -version = "0.16.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99fb4dcba076453d791949bf3af977c5678a1cbd76740ec2cfe37e29431daf3" +checksum = "004129e2c93798141d68ff08cb7f1b3d909c0212747fb8b05c8989635ba90a4e" dependencies = [ "bstr", "gix-discover", @@ -1431,30 +1574,38 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-discover" -version = "0.42.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d24547153810634636471af88338240e6ab0831308cd41eb6ebfffea77811c6" +checksum = "950b027b861c6863ddf1b075672ec1ef2006b95c4d12284fc1ec4cdb1ab6639e" dependencies = [ "bstr", "dunce", "gix-fs", - "gix-hash", "gix-path", "gix-ref", "gix-sec", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-error" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dffc9ca4dfa4f519a3d2cf1c038919160544923577ac60f45bcb602a24d82c6" +dependencies = [ + "bstr", ] [[package]] name = "gix-features" -version = "0.44.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa64593d1586135102307fb57fb3a9d3868b6b1f45a4da1352cce5070f8916a" +checksum = "6a407957e21dc5e6c87086e50e5114a2f9240f9cb11699588a6d900d53cb6c70" dependencies = [ "crc32fast", "crossbeam-channel", @@ -1462,19 +1613,19 @@ dependencies = [ "gix-trace", "gix-utils", "libc", - "libz-rs-sys", "once_cell", "parking_lot", "prodash", - "thiserror", + "thiserror 2.0.18", "walkdir", + "zlib-rs", ] [[package]] name = "gix-filter" -version = "0.21.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1253452c9808da01eaaf9b1c4929b9982efec29ef0a668b3326b8046d9b8fb" +checksum = "7240442915cdd74e1f889566695ce0d0c23c7185b13318a1232ce646af0d18ad" dependencies = [ "bstr", "encoding_rs", @@ -1482,34 +1633,34 @@ dependencies = [ "gix-command", "gix-hash", "gix-object", - "gix-packetline-blocking", + "gix-packetline", "gix-path", "gix-quote", "gix-trace", "gix-utils", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-fs" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1ecd896258cdc5ccd94d18386d17906b8de265ad2ecf68e3bea6b007f6a28f" +checksum = "ba74fa163d3b2ba821d5cd207d55fe3daac3d1099613a8559c812d2b15b3c39a" dependencies = [ "bstr", "fastrand", "gix-features", "gix-path", "gix-utils", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-glob" -version = "0.22.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74254992150b0a88fdb3ad47635ab649512dff2cbbefca7916bb459894fc9d56" +checksum = "b03e6cd88cc0dc1eafa1fddac0fb719e4e74b6ea58dd016e71125fde4a326bee" dependencies = [ "bitflags 2.10.0", "bstr", @@ -1519,32 +1670,32 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826036a9bee95945b0be1e2394c64cd4289916c34a639818f8fd5153906985c1" +checksum = "2b8e11ea6bbd0fd4ab4a1c66812dd3cc25921a41315b120f352997725a4c79d6" dependencies = [ "faster-hex", "gix-features", "sha1-checked", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-hashtable" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27d4a3ea9640da504a2657fef3419c517fd71f1767ad8935298bcc805edd195" +checksum = "52f1eecdd006390cbed81f105417dbf82a6fe40842022006550f2e32484101da" dependencies = [ "gix-hash", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "parking_lot", ] [[package]] name = "gix-ignore" -version = "0.17.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b6a9679a1488123b7f2929684bacfd9cd2a24f286b52203b8752cbb8d7fc49" +checksum = "8953d87c13267e296d547f0fc7eaa8aa8fa5b2a9a34ab1cd5857f25240c7d299" dependencies = [ "bstr", "gix-glob", @@ -1555,9 +1706,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.42.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31244542fb98ea4f3e964a4f8deafc2f4c77ad42bed58a1e8424bca1965fae99" +checksum = "e31c6b3664efe5916c539c50e610f9958f2993faf8e29fa5a40fb80b6ac8486a" dependencies = [ "bitflags 2.10.0", "bstr", @@ -1572,43 +1723,43 @@ dependencies = [ "gix-traverse", "gix-utils", "gix-validate", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "itoa", "libc", "memmap2", - "rustix 1.1.2", + "rustix 1.1.3", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-lock" -version = "19.0.0" +version = "21.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729d7857429a66023bc0c29d60fa21d0d6ae8862f33c1937ba89e0f74dd5c67f" +checksum = "e16d406220ef9df105645a9ddcaa42e8c882ba920344ace866d0403570aea599" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-mailmap" -version = "0.27.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3fc0f07ce86acc94d93e5d10ef38bad322dede2622d5ff84f0799ac13b7e7d" +checksum = "2e6fd521cb582620b7066b5b420ace1951cb84fe2241fa239b227e1a94fa25dc" dependencies = [ "bstr", "gix-actor", "gix-date", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-object" -version = "0.51.1" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba1815638759c80d2318c8e98296fb396f577c2e588a3d9c13f9a5d5184051" +checksum = "4d3f705c977d90ace597049252ae1d7fec907edc0fa7616cc91bf5508d0f4006" dependencies = [ "bstr", "gix-actor", @@ -1621,18 +1772,17 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror", + "thiserror 2.0.18", "winnow", ] [[package]] name = "gix-odb" -version = "0.71.1" +version = "0.75.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6efc6736d3ea62640efe8c1be695fb0760af63614a7356d2091208a841f1a634" +checksum = "1d59882d2fdab5e609b0c452a6ef9a3bd12ef6b694be4f82ab8f126ad0969864" dependencies = [ "arc-swap", - "gix-date", "gix-features", "gix-fs", "gix-hash", @@ -1643,17 +1793,18 @@ dependencies = [ "gix-quote", "parking_lot", "tempfile", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-pack" -version = "0.61.1" +version = "0.65.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719c60524be76874f4769da20d525ad2c00a0e7059943cc4f31fcb65cfb6b260" +checksum = "8c44db57ebbbeaad9972c2a60662142660427a1f0a7529314d53fefb4fedad24" dependencies = [ "clru", "gix-chunk", + "gix-error", "gix-features", "gix-hash", "gix-hashtable", @@ -1661,52 +1812,39 @@ dependencies = [ "gix-path", "memmap2", "smallvec", - "thiserror", + "thiserror 2.0.18", "uluru", ] [[package]] name = "gix-packetline" -version = "0.19.3" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64286a8b5148e76ab80932e72762dd27ccf6169dd7a134b027c8a262a8262fcf" +checksum = "6c333badf342e9c2392800a96b9f2cf5bcb33906d2577d6ec923756ff4008a3f" dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror", -] - -[[package]] -name = "gix-packetline-blocking" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c59c3ad41e68cb38547d849e9ef5ccfc0d00f282244ba1441ae856be54d001" -dependencies = [ - "bstr", - "faster-hex", - "gix-trace", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-path" -version = "0.10.21" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0416b41cd00ff292af9b94b0660880c44bd2ed66828ddca9a2b333535cbb71b8" +checksum = "c7c3cd795cad18c7acbc6bafe34bfb34ac7273ee81133793f9d1516dd9faf922" dependencies = [ "bstr", "gix-trace", "gix-validate", - "home", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-pathspec" -version = "0.13.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e28457dca7c65a2dbe118869aab922a5bd382b7bb10cff5354f366845c128" +checksum = "3df6fd8e514d8b99ec5042ee17909a17750ccf54d0b8b30c850954209c800322" dependencies = [ "bitflags 2.10.0", "bstr", @@ -1714,14 +1852,14 @@ dependencies = [ "gix-config-value", "gix-glob", "gix-path", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-protocol" -version = "0.52.1" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f19873bbf924fd077580d4ccaaaeddb67c3b3c09a8ffb61e6b4cb67e3c9302" +checksum = "54f20837b0c70b65f6ac77886be033de3b69d5879f99128b47c42665ab0a17c2" dependencies = [ "bstr", "gix-date", @@ -1732,7 +1870,7 @@ dependencies = [ "gix-transport", "gix-utils", "maybe-async", - "thiserror", + "thiserror 2.0.18", "winnow", ] @@ -1744,14 +1882,14 @@ checksum = "e912ec04b7b1566a85ad486db0cab6b9955e3e32bcd3c3a734542ab3af084c5b" dependencies = [ "bstr", "gix-utils", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-ref" -version = "0.54.1" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8881d262f28eda39c244e60ae968f4f6e56c747f65addd6f4100b25f75ed8b88" +checksum = "5cf780dcd9ac99fd3fcfc8523479a0e2ffd55f5e0be63e5e3248fb7e46cff966" dependencies = [ "gix-actor", "gix-features", @@ -1764,62 +1902,65 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror", + "thiserror 2.0.18", "winnow", ] [[package]] name = "gix-refspec" -version = "0.32.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93147960f77695ba89b72019b789679278dd4dad6a0f9a4a5bf2fd07aba56912" +checksum = "60ce400a770a7952e45267803192cc2d1fe0afa08e2c08dde32e04c7908c6e61" dependencies = [ "bstr", + "gix-error", + "gix-glob", "gix-hash", "gix-revision", "gix-validate", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-revision" -version = "0.36.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c5267e530d8762842be7d51b48d2b134c9dec5b650ca607f735a56a4b12413" +checksum = "c719cf7d669439e1fca735bd1c4de54d43c5d30e8883fd6063c4924b213d70c9" dependencies = [ "bitflags 2.10.0", "bstr", "gix-commitgraph", "gix-date", + "gix-error", "gix-hash", "gix-hashtable", "gix-object", "gix-revwalk", "gix-trace", - "thiserror", ] [[package]] name = "gix-revwalk" -version = "0.22.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2de4f91d712b1f6873477f769225fe430ffce2af8c7c85721c3ff955783b3" +checksum = "194a50b30aa0c6e6de43c723359c5809a96275a3aa92d323ef7f58b1cdd60f16" dependencies = [ "gix-commitgraph", "gix-date", + "gix-error", "gix-hash", "gix-hashtable", "gix-object", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-sec" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9962ed6d9114f7f100efe038752f41283c225bb507a2888903ac593dffa6be" +checksum = "beeb3bc63696cf7acb5747a361693ebdbcaf25b5d27d2308f38e9782983e7bce" dependencies = [ "bitflags 2.10.0", "gix-path", @@ -1829,21 +1970,21 @@ dependencies = [ [[package]] name = "gix-shallow" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2374692db1ee1ffa0eddcb9e86ec218f7c4cdceda800ebc5a9fdf73a8c08223" +checksum = "f4f4660fed3786d28e7e57d31b2de9ab3bf846068e187ccc52ee513de19a0073" dependencies = [ "bstr", "gix-hash", "gix-lock", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-status" -version = "0.21.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c64039358f66c955a471432aef0ea1eeebc7afe0e0a4be7b6b737cc19925e3b" +checksum = "b0c994dbca7f038cfcde6337673523bab6e6b4f544b5046f5120a02bdeafff33" dependencies = [ "bstr", "filetime", @@ -1859,14 +2000,14 @@ dependencies = [ "gix-pathspec", "gix-worktree", "portable-atomic", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-submodule" -version = "0.21.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bacc06333b50abc4fc06204622c2dd92850de2066bb5d421ac776d2bef7ae55" +checksum = "db1840fe723c6264ee596e5a179e1b9a2df59351f09bae9cea570a472a790bc0" dependencies = [ "bstr", "gix-config", @@ -1874,14 +2015,14 @@ dependencies = [ "gix-pathspec", "gix-refspec", "gix-url", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-tempfile" -version = "19.0.1" +version = "21.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e265fc6b54e57693232a79d84038381ebfda7b1a3b1b8a9320d4d5fe6e820086" +checksum = "d280bba7c547170e42d5228fc6e76c191fb5a7c88808ff61af06460404d1fd91" dependencies = [ "dashmap", "gix-fs", @@ -1892,15 +2033,15 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3f59a8de2934f6391b6b3a1a7654eae18961fcb9f9c843533fed34ad0f3457" +checksum = "6e42a4c2583357721ba2d887916e78df504980f22f1182df06997ce197b89504" [[package]] name = "gix-transport" -version = "0.49.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8da4a77922accb1e26e610c7a84ef7e6b34fd07112e6a84afd68d7f3e795957" +checksum = "de1064c7ffa5a915014a6a5b71fbc5299462ae655348bed23e083b4a735076c3" dependencies = [ "bstr", "gix-command", @@ -1909,14 +2050,14 @@ dependencies = [ "gix-quote", "gix-sec", "gix-url", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-traverse" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "412126bade03a34f5d4125fd64878852718575b3b360eaae3b29970cb555e2a2" +checksum = "37f8b53b4c56b01c43a4491c4edfe2ce66c654eb86232205172ceb1650d21c55" dependencies = [ "bitflags 2.10.0", "gix-commitgraph", @@ -1926,21 +2067,19 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "gix-url" -version = "0.33.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79b07b48dd9285485eb10429696ddcd1bfe6fb942ec0e5efb401ae7e40238e5" +checksum = "1ca2e50308a8373069e71970939f43ea4a1b5f422cf807d048ebcf07dcc02b2c" dependencies = [ "bstr", - "gix-features", "gix-path", "percent-encoding", - "thiserror", - "url", + "thiserror 2.0.18", ] [[package]] @@ -1956,23 +2095,21 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" +checksum = "0ec1eff98d91941f47766367cba1be746bab662bad761d9891ae6f7882f7840b" dependencies = [ "bstr", - "thiserror", ] [[package]] name = "gix-worktree" -version = "0.43.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df3dfc8b62b0eccc923c757b40f488abc357c85c03d798622edfc3eb5137e04" +checksum = "ef2ad658586ec0039b03e96c664f08b7cb7a2b7cca6947a9c856c9ed59b807b1" dependencies = [ "bstr", "gix-attributes", - "gix-features", "gix-fs", "gix-glob", "gix-hash", @@ -2015,16 +2152,14 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.4", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", @@ -2047,6 +2182,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -2056,15 +2197,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "iana-time-zone" version = "0.1.61" @@ -2203,9 +2335,15 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2244,12 +2382,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -2288,6 +2428,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.44.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" +dependencies = [ + "console", + "once_cell", + "regex", + "similar", +] + [[package]] name = "instability" version = "0.3.6" @@ -2298,7 +2450,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2311,15 +2463,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2331,34 +2474,34 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", - "serde", - "windows-sys 0.59.0", + "serde_core", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2387,14 +2530,24 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481b4381c813cebeca86bd55c781d21f902f34cf927ec08d6df3dfebcfd2002" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -2424,6 +2577,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" @@ -2434,16 +2593,22 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.177" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libgit2-sys" -version = "0.18.1+1.9.0" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -2472,9 +2637,9 @@ dependencies = [ [[package]] name = "libssh2-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" dependencies = [ "cc", "libc", @@ -2484,15 +2649,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] - [[package]] name = "libz-sys" version = "1.1.21" @@ -2505,6 +2661,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2523,6 +2688,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -2534,17 +2705,27 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.16.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", ] [[package]] @@ -2555,7 +2736,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2573,6 +2754,27 @@ dependencies = [ "libc", ] +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.7" @@ -2594,6 +2796,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "8.2.0" @@ -2658,9 +2883,20 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "num-integer" @@ -2692,6 +2928,21 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + [[package]] name = "object" version = "0.37.3" @@ -2752,9 +3003,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -2769,6 +3020,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "p256" version = "0.13.2" @@ -2830,12 +3090,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.12.2" @@ -2860,15 +3114,92 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ + "phf_macros", "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "phf_shared" version = "0.11.3" @@ -2993,6 +3324,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -3013,9 +3354,9 @@ dependencies = [ [[package]] name = "prodash" -version = "30.0.1" +version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6efc566849d3d9d737c5cb06cc50e48950ebe3d3f9d70631490fff3a07b139" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" dependencies = [ "parking_lot", ] @@ -3044,6 +3385,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -3075,23 +3422,103 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-termion", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ "bitflags 2.10.0", - "cassowary", "compact_str", - "crossterm", + "hashbrown 0.16.1", "indoc", - "instability", - "itertools 0.13.0", + "itertools", + "kasuari", "lru", - "paste", "serde", "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm 0.28.1", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-termion" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cade85a8591fbc911e147951422f0d6fd40f4948b271b6216c7dc01838996f8" +dependencies = [ + "instability", + "ratatui-core", + "termion", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-textarea" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de236b7cc74b3f7dea227b3fbad97bf459cddf552b6503d888fb9a106eda59ab" +dependencies = [ + "ratatui-core", + "ratatui-crossterm", + "ratatui-widgets", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", "unicode-segmentation", - "unicode-truncate 1.1.0", "unicode-width 0.2.0", ] @@ -3132,7 +3559,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -3176,14 +3603,15 @@ dependencies = [ [[package]] name = "ron" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "base64", "bitflags 2.10.0", + "once_cell", "serde", "serde_derive", + "typeid", "unicode-ident", ] @@ -3238,9 +3666,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -3345,7 +3773,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3362,11 +3790,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", @@ -3376,13 +3805,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3484,6 +3913,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "simplelog" version = "0.12.2" @@ -3617,29 +4052,28 @@ checksum = "68c6387c1c7b53060605101b63d93edca618c6cf7ce61839f2ec2a527419fdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "strum" -version = "0.26.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.117", ] [[package]] @@ -3650,9 +4084,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3667,7 +4112,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3677,7 +4122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" dependencies = [ "bincode", - "fancy-regex", + "fancy-regex 0.16.2", "flate2", "fnv", "once_cell", @@ -3687,7 +4132,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 2.0.18", "walkdir", ] @@ -3707,35 +4152,130 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] [[package]] -name = "thiserror" -version = "2.0.17" +name = "terminfo" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ - "thiserror-impl", + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termion" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb" +dependencies = [ + "libc", + "numtoa", + "serde", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.10.0", + "fancy-regex 0.11.0", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "serde", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -3750,30 +4290,32 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -3804,34 +4346,35 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tui-textarea" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" -dependencies = [ - "crossterm", - "ratatui", - "unicode-width 0.2.0", -] - [[package]] name = "two-face" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d112cfd41c1387546416bcf49c4ae2a1fcacda0d42c9e97120e9798c90c0923" +checksum = "39e51b6e60e545cfdae5a4639ff423818f52372211a8d9a3e892b4b0761f76b2" dependencies = [ "serde", "serde_derive", "syntect", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uluru" version = "3.1.0" @@ -3870,22 +4413,11 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.14", -] - -[[package]] -name = "unicode-truncate" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330" -dependencies = [ - "itertools 0.13.0", + "itertools", "unicode-segmentation", "unicode-width 0.2.0", ] @@ -3902,6 +4434,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -3941,6 +4479,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -3953,6 +4504,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -3975,39 +4535,36 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4015,22 +4572,133 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.2", + "indexmap", + "semver", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "serde", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] [[package]] name = "which" @@ -4039,7 +4707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ "env_home", - "rustix 1.1.2", + "rustix 1.1.3", "winsafe", ] @@ -4113,7 +4781,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4124,7 +4792,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4309,9 +4977,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -4328,6 +4996,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "write16" version = "1.0.0" @@ -4366,7 +5122,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4388,7 +5144,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4408,15 +5164,15 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerovec" @@ -4437,11 +5193,11 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" diff --git a/Cargo.toml b/Cargo.toml index 5687b8f7..68297102 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,92 @@ [package] name = "gitui" -version = "0.27.0" +version = "0.28.1" authors = ["extrawurst "] description = "blazing fast terminal-ui for git" edition = "2021" -rust-version = "1.82" +rust-version = "1.88" exclude = [".github/*", ".vscode/*", "assets/*"] homepage = "https://github.com/gitui-org/gitui" repository = "https://github.com/gitui-org/gitui" readme = "README.md" license = "MIT" categories = ["command-line-utilities"] -keywords = ["git", "gui", "cli", "terminal", "ui"] +keywords = ["cli", "git", "gui", "terminal", "ui"] build = "build.rs" [workspace] members = [ - "asyncgit", - "filetreelist", - "git2-hooks", - "git2-testing", - "scopetime", + "asyncgit", + "filetreelist", + "git2-hooks", + "git2-testing", + "scopetime", ] +[dependencies] +anyhow = "1.0" +asyncgit = { path = "./asyncgit", version = "0.28.1", default-features = false } +backtrace = "0.3" +base64 = "0.22" +bitflags = "2.10" +bugreport = "0.5.1" +bwrap = { version = "1.3", features = ["use_std"] } +bytesize = { version = "2.3", default-features = false } +chrono = { version = "0.4", default-features = false, features = ["clock"] } +clap = { version = "4.5", features = ["cargo", "env"] } +crossbeam-channel = "0.5" +crossterm = { version = "0.29", features = ["serde"] } +dirs = "6.0" +easy-cast = "0.5" +filetreelist = { path = "./filetreelist", version = ">=0.6" } +fuzzy-matcher = "0.3" +gh-emoji = { version = "1.0", optional = true } +indexmap = "2" +itertools = "0.14" +log = "0.4" +notify = "8" +notify-debouncer-mini = "0.7" +once_cell = "1" +parking_lot_core = "0.9" +ratatui = { version = "0.30", default-features = false, features = [ + "crossterm", + "serde", +] } +ratatui-textarea = "0.8" +rayon-core = "1.13" +ron = "0.12" +scopeguard = "1.2" +scopetime = { path = "./scopetime", version = "0.1" } +serde = "1.0" +shellexpand = "3.1" +simplelog = { version = "0.12", default-features = false } +struct-patch = "0.10" +syntect = { version = "5.3", default-features = false, features = [ + "default-syntaxes", + "default-themes", + "html", + "parsing", + "plist-load", +] } +two-face = { version = "0.4.4", default-features = false } +unicode-segmentation = "1.12" +unicode-truncate = "2.0" +unicode-width = "0.2" +which = "8.0" + +[dev-dependencies] +env_logger = "0.11" +git2-testing = { path = "./git2-testing" } +insta = { version = "1.41.0", features = ["filters"] } +pretty_assertions = "1.4" +tempfile = "3" + +[build-dependencies] +chrono = { version = "0.4", default-features = false, features = ["clock"] } + +[badges] +maintenance = { status = "actively-developed" } + [features] default = ["ghemoji", "regex-fancy", "trace-libgit", "vendor-openssl"] ghemoji = ["gh-emoji"] @@ -33,76 +97,14 @@ timing = ["scopetime/enabled"] trace-libgit = ["asyncgit/trace-libgit"] vendor-openssl = ["asyncgit/vendor-openssl"] -[dependencies] -anyhow = "1.0" -asyncgit = { path = "./asyncgit", version = "0.27.0", default-features = false } -backtrace = "0.3" -base64 = "0.22" -bitflags = "2.10" -bugreport = "0.5.1" -bwrap = { version = "1.3", features = ["use_std"] } -bytesize = { version = "2.1", default-features = false } -chrono = { version = "0.4", default-features = false, features = ["clock"] } -clap = { version = "4.5", features = ["env", "cargo"] } -crossbeam-channel = "0.5" -crossterm = { version = "0.28", features = ["serde"] } -dirs = "6.0" -easy-cast = "0.5" -filetreelist = { path = "./filetreelist", version = "0.5" } -fuzzy-matcher = "0.3" -gh-emoji = { version = "1.0", optional = true } -indexmap = "2" -itertools = "0.14" -log = "0.4" -notify = "8" -notify-debouncer-mini = "0.7" -once_cell = "1" -parking_lot_core = "0.9" -ratatui = { version = "0.29", default-features = false, features = [ - 'crossterm', - 'serde', -] } -rayon-core = "1.13" -ron = "0.11" -scopeguard = "1.2" -scopetime = { path = "./scopetime", version = "0.1" } -serde = "1.0" -shellexpand = "3.1" -simplelog = { version = "0.12", default-features = false } -struct-patch = "0.10" -syntect = { version = "5.3", default-features = false, features = [ - "parsing", - "default-syntaxes", - "default-themes", - "plist-load", - "html", -] } -tui-textarea = "0.7" -two-face = { version = "0.4.4", default-features = false } -unicode-segmentation = "1.12" -unicode-truncate = "2.0" -unicode-width = "0.2" -which = "8.0" - -[build-dependencies] -chrono = { version = "0.4", default-features = false, features = ["clock"] } - -[dev-dependencies] -env_logger = "0.11" -pretty_assertions = "1.4" -tempfile = "3" - -[badges] -maintenance = { status = "actively-developed" } - -[profile.release] -lto = true -opt-level = 'z' # Optimize for size. -codegen-units = 1 -strip = "debuginfo" - # make debug build as fast as release # usage of utf8 encoding inside tui # makes their debug profile slow [profile.dev.package."ratatui"] opt-level = 3 + +[profile.release] +opt-level = "z" # Optimize for size. +strip = "debuginfo" +lto = true +codegen-units = 1 diff --git a/Makefile b/Makefile index 919c9aed..939c8118 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ build-linux-musl-release: cargo build --release --target=x86_64-unknown-linux-musl --locked test-linux-musl: - cargo test --workspace --target=x86_64-unknown-linux-musl + cargo nextest run --workspace --target=x86_64-unknown-linux-musl release-linux-arm: build-linux-arm-release mkdir -p release @@ -83,7 +83,7 @@ build-linux-arm-release: cargo build --release --target=arm-unknown-linux-gnueabihf --locked test: - cargo test --workspace + cargo nextest run --workspace fmt: cargo fmt -- --check @@ -94,7 +94,7 @@ clippy: clippy-nightly: cargo +nightly clippy --workspace --all-features -check: fmt clippy test sort +check: fmt clippy test sort deny check-nightly: cargo +nightly c @@ -105,7 +105,7 @@ deny: cargo deny check sort: - cargo sort -c -w "." + tombi format --check install: cargo install --path "." --offline --locked diff --git a/README.md b/README.md index 34f0bced..ec4127f5 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,11 @@ These are the high level goals before calling out `1.0`: * visualize branching structure in log tab ([#81](https://github.com/gitui-org/gitui/issues/81)) * interactive rebase ([#32](https://github.com/gitui-org/gitui/issues/32)) +- no git-lfs support (see [#2812](https://github.com/gitui-org/gitui/issues/2812)) ## 5. Known Limitations [Top ▲](#table-of-contents) - no sparse repo support (see [#1226](https://github.com/gitui-org/gitui/issues/1226)) -- no git-lfs support (see [#1089](https://github.com/gitui-org/gitui/discussions/1089)) - *credential.helper* for https needs to be **explicitly** configured (see [#800](https://github.com/gitui-org/gitui/issues/800)) Currently, this tool does not fully substitute the _git shell_, however both tools work well in tandem. @@ -163,6 +163,12 @@ scoop install gitui choco install gitui ``` +##### [Mise](https://github.com/jdx/mise) + +```shell +mise use -g gitui@latest +``` + ##### [Nix](https://search.nixos.org/packages?channel=unstable&show=gitui&from=0&size=50&sort=relevance&query=gitui) (Nix/NixOS) Nixpkg @@ -220,7 +226,7 @@ see [NIGHTLIES.md](./NIGHTLIES.md) ### Requirements -- Minimum supported `rust`/`cargo` version: `1.82` +- Minimum supported `rust`/`cargo` version: `1.88` - See [Install Rust](https://www.rust-lang.org/tools/install) - To build openssl dependency (see https://docs.rs/openssl/latest/openssl/) diff --git a/assets/blame-goto-line.png b/assets/blame-goto-line.png new file mode 100644 index 00000000..d9112686 Binary files /dev/null and b/assets/blame-goto-line.png differ diff --git a/assets/discard-changes-on-checkout.png b/assets/discard-changes-on-checkout.png new file mode 100644 index 00000000..c320fbe9 Binary files /dev/null and b/assets/discard-changes-on-checkout.png differ diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 00000000..eeef871d --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index f1d5bb18..5e77498a 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "asyncgit" -version = "0.27.0" +version = "0.28.1" authors = ["extrawurst "] edition = "2021" description = "allow using git2 in a asynchronous context" @@ -8,14 +8,9 @@ homepage = "https://github.com/gitui-org/gitui" repository = "https://github.com/gitui-org/gitui" readme = "README.md" license = "MIT" -categories = ["concurrency", "asynchronous"] +categories = ["asynchronous", "concurrency"] keywords = ["git"] -[features] -default = ["trace-libgit"] -trace-libgit = [] -vendor-openssl = ["openssl-sys"] - [dependencies] bitflags = "2" crossbeam-channel = "0.5" @@ -23,18 +18,18 @@ dirs = "6.0" easy-cast = "0.5" fuzzy-matcher = "0.3" git2 = "0.20" -git2-hooks = { path = "../git2-hooks", version = ">=0.5" } -gix = { version = "0.74.1", default-features = false, features = [ - "max-performance", - "revision", - "mailmap", - "status", +git2-hooks = { path = "../git2-hooks", version = "0.7" } +gix = { version = "0.78.0", default-features = false, features = [ + "mailmap", + "max-performance", + "revision", + "status", ] } log = "0.4" # git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]} # git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]} # pinning to vendored openssl, using the git2 feature this gets lost with new resolver -openssl-sys = { version = '0.9', features = ["vendored"], optional = true } +openssl-sys = { version = "0.9", features = ["vendored"], optional = true } rayon = "1.11" rayon-core = "1.13" scopetime = { path = "../scopetime", version = "0.1" } @@ -48,5 +43,10 @@ url = "2.5" env_logger = "0.11" invalidstring = { path = "../invalidstring", version = "0.1" } pretty_assertions = "1.4" -serial_test = "3.2" +serial_test = "3.3" tempfile = "3" + +[features] +default = ["trace-libgit"] +trace-libgit = [] +vendor-openssl = ["openssl-sys"] diff --git a/asyncgit/src/push.rs b/asyncgit/src/push.rs index 867ecd8d..863abfac 100644 --- a/asyncgit/src/push.rs +++ b/asyncgit/src/push.rs @@ -164,7 +164,7 @@ impl AsyncPush { *last_res = match res { Ok(()) => None, Err(e) => { - log::error!("push error: {e}",); + log::error!("push error: {e}"); Some(e.to_string()) } }; diff --git a/asyncgit/src/revlog.rs b/asyncgit/src/revlog.rs index f738830a..774d5140 100644 --- a/asyncgit/src/revlog.rs +++ b/asyncgit/src/revlog.rs @@ -49,7 +49,7 @@ pub struct AsyncLog { static LIMIT_COUNT: usize = 3000; static SLEEP_FOREGROUND: Duration = Duration::from_millis(2); -static SLEEP_BACKGROUND: Duration = Duration::from_millis(1000); +static SLEEP_BACKGROUND: Duration = Duration::from_secs(1); impl AsyncLog { /// diff --git a/asyncgit/src/status.rs b/asyncgit/src/status.rs index 749272fe..9728c6f3 100644 --- a/asyncgit/src/status.rs +++ b/asyncgit/src/status.rs @@ -10,19 +10,11 @@ use crossbeam_channel::Sender; use std::{ hash::Hash, sync::{ - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicU64, AtomicUsize, Ordering}, Arc, Mutex, }, - time::{SystemTime, UNIX_EPOCH}, }; -fn current_tick() -> u128 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("time before unix epoch!") - .as_millis() -} - #[derive(Default, Hash, Clone)] pub struct Status { pub items: Vec, @@ -31,19 +23,17 @@ pub struct Status { /// #[derive(Default, Hash, Copy, Clone, PartialEq, Eq)] pub struct StatusParams { - tick: u128, status_type: StatusType, config: Option, } impl StatusParams { /// - pub fn new( + pub const fn new( status_type: StatusType, config: Option, ) -> Self { Self { - tick: current_tick(), status_type, config, } @@ -59,6 +49,8 @@ pub struct AsyncStatus { sender: Sender, pending: Arc, repo: RepoPath, + /// Counter that increments after each completed fetch. + generation: Arc, } impl AsyncStatus { @@ -73,6 +65,7 @@ impl AsyncStatus { last: Arc::new(Mutex::new(Status::default())), sender, pending: Arc::new(AtomicUsize::new(0)), + generation: Arc::new(AtomicU64::new(0)), } } @@ -97,12 +90,14 @@ impl AsyncStatus { return Ok(None); } - let hash_request = hash(¶ms); + let generation = self.generation.load(Ordering::Relaxed); + let hash_request = hash(&(params, generation)); log::trace!( - "request: [hash: {}] (type: {:?})", + "request: [hash: {}] (type: {:?}, gen: {})", hash_request, params.status_type, + generation, ); { @@ -118,6 +113,7 @@ impl AsyncStatus { let arc_current = Arc::clone(&self.current); let arc_last = Arc::clone(&self.last); + let arc_generation = Arc::clone(&self.generation); let sender = self.sender.clone(); let arc_pending = Arc::clone(&self.pending); let status_type = params.status_type; @@ -138,11 +134,14 @@ impl AsyncStatus { log::error!("fetch_helper: {e}"); } + // Increment generation to invalidate cache for next request + arc_generation.fetch_add(1, Ordering::Relaxed); arc_pending.fetch_sub(1, Ordering::Relaxed); - sender - .send(AsyncGitNotification::Status) - .expect("error sending status"); + if let Err(e) = sender.send(AsyncGitNotification::Status) + { + log::error!("send status error: {e}"); + } }); Ok(None) diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index 9b613897..6490142b 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -104,6 +104,11 @@ impl BranchInfo { None } + + /// returns whether branch is local + pub const fn is_local(&self) -> bool { + matches!(self.details, BranchDetails::Local(_)) + } } /// diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index f4e0b194..b88f055c 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -40,10 +40,12 @@ pub fn amend( return Err(Error::SignAmendNonLastCommit); } + let committer = signature_allow_undefined_name(&repo)?; + let new_id = commit.amend( Some("HEAD"), None, - None, + Some(&committer), // Passing a value will overwrite the committer. None, Some(msg), Some(&tree), @@ -307,6 +309,55 @@ mod tests { Ok(()) } + #[test] + fn test_amend_with_different_user() { + let file_path1 = Path::new("foo"); + let file_path2 = Path::new("foo2"); + let (_td, repo) = repo_init_empty().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path: &RepoPath = + &root.as_os_str().to_str().unwrap().into(); + + File::create(root.join(file_path1)) + .unwrap() + .write_all(b"test1") + .unwrap(); + + stage_add_file(repo_path, file_path1).unwrap(); + let id = commit(repo_path, "commit msg").unwrap(); + + let amended_details = + get_commit_details(repo_path, id).unwrap(); + + assert_eq!(amended_details.committer, None); + + File::create(root.join(file_path2)) + .unwrap() + .write_all(b"test2") + .unwrap(); + + stage_add_file(repo_path, file_path2).unwrap(); + + repo.config() + .unwrap() + .set_str("user.name", "changed name") + .unwrap(); + repo.config() + .unwrap() + .set_str("user.email", "changed@example.com") + .unwrap(); + + let new_id = amend(repo_path, id, "amended").unwrap(); + + let amended_details = + get_commit_details(repo_path, new_id).unwrap(); + assert_eq!(amended_details.author.name, "name"); + assert_eq!(amended_details.author.email, "email"); + let committer = amended_details.committer.unwrap(); + assert_eq!(committer.name, "changed name"); + assert_eq!(committer.email, "changed@example.com"); + } + #[test] fn test_tag() -> Result<()> { let file_path = Path::new("foo"); diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index 0c31b470..89d847c8 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -177,7 +177,7 @@ pub fn get_commit_info( let message = gix_get_message(&commit_ref, None); - let author = commit_ref.author(); + let author = commit_ref.author()?; let author = mailmap.try_resolve(author).map_or_else( || author.name.into(), @@ -187,7 +187,7 @@ pub fn get_commit_info( Ok(CommitInfo { message, author: author.to_string(), - time: commit_ref.time().seconds, + time: commit_ref.time()?.seconds, id: commit.id().detach().into(), }) } diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs index fe1a9191..fe3438f6 100644 --- a/asyncgit/src/sync/hooks.rs +++ b/asyncgit/src/sync/hooks.rs @@ -1,7 +1,19 @@ use super::{repository::repo, RepoPath}; -use crate::error::Result; -pub use git2_hooks::PrepareCommitMsgSource; +use crate::{ + error::Result, + sync::{ + branch::get_branch_upstream_merge, + config::{ + push_default_strategy_config_repo, + PushDefaultStrategyConfig, + }, + remotes::{proxy_auto, tags::tags_missing_remote, Callbacks}, + }, +}; +use git2::{BranchType, Direction, Oid}; +pub use git2_hooks::{PrePushRef, PrepareCommitMsgSource}; use scopetime::scope_time; +use std::collections::HashMap; /// #[derive(Debug, PartialEq, Eq)] @@ -15,17 +27,91 @@ pub enum HookResult { impl From for HookResult { fn from(v: git2_hooks::HookResult) -> Self { match v { - git2_hooks::HookResult::Ok { .. } - | git2_hooks::HookResult::NoHookFound => Self::Ok, - git2_hooks::HookResult::RunNotSuccessful { - stdout, - stderr, - .. - } => Self::NotOk(format!("{stdout}{stderr}")), + git2_hooks::HookResult::NoHookFound => Self::Ok, + git2_hooks::HookResult::Run(response) => { + if response.is_successful() { + Self::Ok + } else { + Self::NotOk(if response.stderr.is_empty() { + response.stdout + } else if response.stdout.is_empty() { + response.stderr + } else { + format!( + "{}\n{}", + response.stdout, response.stderr + ) + }) + } + } } } } +/// Retrieve advertised refs from the remote for the upcoming push. +fn advertised_remote_refs( + repo_path: &RepoPath, + remote: Option<&str>, + url: &str, + basic_credential: Option, +) -> Result> { + let repo = repo(repo_path)?; + let mut remote_handle = if let Some(name) = remote { + repo.find_remote(name)? + } else { + repo.remote_anonymous(url)? + }; + + let callbacks = Callbacks::new(None, basic_credential); + let conn = remote_handle.connect_auth( + Direction::Push, + Some(callbacks.callbacks()), + Some(proxy_auto()), + )?; + + let mut map = HashMap::new(); + for head in conn.list()? { + map.insert(head.name().to_string(), head.oid()); + } + + Ok(map) +} + +/// Determine the remote ref name for a branch push. +/// +/// Respects `push.default=upstream` config when set and upstream is configured. +/// Otherwise defaults to `refs/heads/{branch}`. Delete operations always use +/// the simple ref name. +fn get_remote_ref_for_push( + repo_path: &RepoPath, + branch: &str, + delete: bool, +) -> Result { + // For delete operations, always use the simple ref name + // regardless of push.default configuration + if delete { + return Ok(format!("refs/heads/{branch}")); + } + + let repo = repo(repo_path)?; + let push_default_strategy = + push_default_strategy_config_repo(&repo)?; + + // When push.default=upstream, use the configured upstream ref if available + if push_default_strategy == PushDefaultStrategyConfig::Upstream { + if let Ok(Some(upstream_ref)) = + get_branch_upstream_merge(repo_path, branch) + { + return Ok(upstream_ref); + } + // If upstream strategy is set but no upstream is configured, + // fall through to default behavior + } + + // Default: push to remote branch with same name as local + Ok(format!("refs/heads/{branch}")) +} + /// see `git2_hooks::hooks_commit_msg` pub fn hooks_commit_msg( repo_path: &RepoPath, @@ -73,12 +159,133 @@ pub fn hooks_prepare_commit_msg( } /// see `git2_hooks::hooks_pre_push` -pub fn hooks_pre_push(repo_path: &RepoPath) -> Result { +pub fn hooks_pre_push( + repo_path: &RepoPath, + remote: &str, + push: &PrePushTarget<'_>, + basic_credential: Option, +) -> Result { scope_time!("hooks_pre_push"); let repo = repo(repo_path)?; + if !git2_hooks::hook_available( + &repo, + None, + git2_hooks::HOOK_PRE_PUSH, + )? { + return Ok(HookResult::Ok); + } - Ok(git2_hooks::hooks_pre_push(&repo, None)?.into()) + let git_remote = repo.find_remote(remote)?; + let url = git_remote + .pushurl() + .or_else(|| git_remote.url()) + .ok_or_else(|| { + crate::error::Error::Generic(format!( + "remote '{remote}' has no URL configured" + )) + })? + .to_string(); + + let advertised = advertised_remote_refs( + repo_path, + Some(remote), + &url, + basic_credential, + )?; + let updates = match push { + PrePushTarget::Branch { branch, delete } => { + let remote_ref = + get_remote_ref_for_push(repo_path, branch, *delete)?; + vec![pre_push_branch_update( + repo_path, + branch, + &remote_ref, + *delete, + &advertised, + )?] + } + PrePushTarget::Tags => { + pre_push_tag_updates(repo_path, remote, &advertised)? + } + }; + + Ok(git2_hooks::hooks_pre_push( + &repo, + None, + Some(remote), + &url, + &updates, + )? + .into()) +} + +/// Build a single pre-push update line for a branch. +fn pre_push_branch_update( + repo_path: &RepoPath, + branch_name: &str, + remote_ref: &str, + delete: bool, + advertised: &HashMap, +) -> Result { + let repo = repo(repo_path)?; + let local_ref = format!("refs/heads/{branch_name}"); + let local_oid = (!delete) + .then(|| { + repo.find_branch(branch_name, BranchType::Local) + .ok() + .and_then(|branch| branch.get().peel_to_commit().ok()) + .map(|commit| commit.id()) + }) + .flatten(); + + let remote_oid = advertised.get(remote_ref).copied(); + + Ok(PrePushRef::new( + local_ref, local_oid, remote_ref, remote_oid, + )) +} + +/// Build pre-push updates for tags that are missing on the remote. +fn pre_push_tag_updates( + repo_path: &RepoPath, + remote: &str, + advertised: &HashMap, +) -> Result> { + let repo = repo(repo_path)?; + let tags = tags_missing_remote(repo_path, remote, None)?; + let mut updates = Vec::with_capacity(tags.len()); + + for tag_ref in tags { + if let Ok(reference) = repo.find_reference(&tag_ref) { + let tag_oid = reference.target().or_else(|| { + reference.peel_to_commit().ok().map(|c| c.id()) + }); + let remote_ref = tag_ref.clone(); + let advertised_oid = advertised.get(&remote_ref).copied(); + updates.push(PrePushRef::new( + tag_ref.clone(), + tag_oid, + remote_ref, + advertised_oid, + )); + } + } + + Ok(updates) +} + +/// What is being pushed. +pub enum PrePushTarget<'a> { + /// Push a single branch. + Branch { + /// Local branch name being pushed. + branch: &'a str, + /// Whether this is a delete push. + delete: bool, + }, + /// Push tags. + Tags, } #[cfg(test)] @@ -248,4 +455,47 @@ mod tests { assert_eq!(msg, String::from("msg\n")); } + + #[test] + fn test_pre_push_hook_rejects_based_on_stdin() { + let (_td, repo) = repo_init().unwrap(); + + let hook = b"#!/bin/sh +cat +exit 1 + "; + + git2_hooks::create_hook( + &repo, + git2_hooks::HOOK_PRE_PUSH, + hook, + ); + + let commit_id = repo.head().unwrap().target().unwrap(); + let update = git2_hooks::PrePushRef::new( + "refs/heads/master", + Some(commit_id), + "refs/heads/master", + None, + ); + + let expected_stdin = + git2_hooks::PrePushRef::to_stdin(&[update.clone()]); + + let res = git2_hooks::hooks_pre_push( + &repo, + None, + Some("origin"), + "https://github.com/test/repo.git", + &[update], + ) + .unwrap(); + + let git2_hooks::HookResult::Run(response) = res else { + panic!("Expected Run result"); + }; + assert!(!response.is_successful()); + assert_eq!(response.stdout, expected_stdin); + assert!(expected_stdin.contains("refs/heads/master")); + } } diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index c5c7901c..2a5f413e 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -68,7 +68,7 @@ pub use git2::BranchType; pub use hooks::{ hooks_commit_msg, hooks_post_commit, hooks_pre_commit, hooks_pre_push, hooks_prepare_commit_msg, HookResult, - PrepareCommitMsgSource, + PrePushTarget, PrepareCommitMsgSource, }; pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; diff --git a/asyncgit/src/sync/remotes/callbacks.rs b/asyncgit/src/sync/remotes/callbacks.rs index e74b0e4e..88da8881 100644 --- a/asyncgit/src/sync/remotes/callbacks.rs +++ b/asyncgit/src/sync/remotes/callbacks.rs @@ -107,7 +107,7 @@ impl Callbacks { reference: &str, msg: Option<&str>, ) { - log::debug!("push_update_reference: '{reference}' {msg:?}",); + log::debug!("push_update_reference: '{reference}' {msg:?}"); if let Ok(mut stats) = self.stats.lock() { stats.push_rejected_msg = msg @@ -162,7 +162,7 @@ impl Callbacks { total: usize, bytes: usize, ) { - log::debug!("progress: {current}/{total} ({bytes} B)",); + log::debug!("progress: {current}/{total} ({bytes} B)"); self.sender.clone().map(|sender| { sender.send(ProgressNotification::PushTransfer { current, diff --git a/asyncgit/src/sync/repository.rs b/asyncgit/src/sync/repository.rs index 5c11abdb..c49795fb 100644 --- a/asyncgit/src/sync/repository.rs +++ b/asyncgit/src/sync/repository.rs @@ -69,10 +69,14 @@ pub fn repo(repo_path: &RepoPath) -> Result { } pub fn gix_repo(repo_path: &RepoPath) -> Result { - let repo = gix::ThreadSafeRepository::discover_with_environment_overrides( + let mut repo: gix::Repository = gix::ThreadSafeRepository::discover_with_environment_overrides( repo_path.gitpath(), ) .map(Into::into)?; + if let Some(workdir) = repo_path.workdir() { + repo.set_workdir(Some(workdir.into()))?; + } + Ok(repo) } diff --git a/asyncgit/src/sync/staging/mod.rs b/asyncgit/src/sync/staging/mod.rs index 06e09fe6..fc163408 100644 --- a/asyncgit/src/sync/staging/mod.rs +++ b/asyncgit/src/sync/staging/mod.rs @@ -34,7 +34,7 @@ impl NewFromOldContent { Ok(()) } - fn skip_old_line(&mut self) { + const fn skip_old_line(&mut self) { self.old_index += 1; } diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index 18366fa5..8afdf2dd 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -202,7 +202,11 @@ pub fn get_status( let iter = status.into_index_worktree_iter(Vec::new())?; for item in iter { - let item = item?; + let Ok(item) = item else { + log::warn!("[status] the status iter returned an error for an item: {item:?}"); + + continue; + }; let status = item.summary().map(Into::into); @@ -239,7 +243,7 @@ pub fn get_status( res.push(StatusItem { path, status }); - Ok(gix::diff::index::Action::Continue) + Ok(gix::diff::index::Action::Continue(())) }; repo.tree_index_status( @@ -280,3 +284,88 @@ pub fn get_status( Ok(res) } + +/// discard all changes in the working directory +pub fn discard_status(repo_path: &RepoPath) -> Result { + let repo = repo(repo_path)?; + let commit = repo.head()?.peel_to_commit()?; + + repo.reset(commit.as_object(), git2::ResetType::Hard, None)?; + + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + sync::{ + commit, stage_add_file, + status::{get_status, StatusType}, + tests::{repo_init, repo_init_bare}, + RepoPath, + }, + StatusItem, StatusItemType, + }; + use std::{fs::File, io::Write, path::Path}; + use tempfile::TempDir; + + #[test] + fn test_discard_status() { + let file_path = Path::new("README.md"); + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path: &RepoPath = + &root.as_os_str().to_str().unwrap().into(); + + let mut file = File::create(root.join(file_path)).unwrap(); + + // initial commit + stage_add_file(repo_path, file_path).unwrap(); + commit(repo_path, "commit msg").unwrap(); + + writeln!(file, "Test for discard_status").unwrap(); + + let statuses = + get_status(repo_path, StatusType::WorkingDir, None) + .unwrap(); + assert_eq!(statuses.len(), 1); + + discard_status(repo_path).unwrap(); + + let statuses = + get_status(repo_path, StatusType::WorkingDir, None) + .unwrap(); + assert_eq!(statuses.len(), 0); + } + + #[test] + fn test_get_status_with_workdir() { + let (git_dir, _repo) = repo_init_bare().unwrap(); + + let separate_workdir = TempDir::new().unwrap(); + + let file_path = Path::new("foo"); + File::create(separate_workdir.path().join(file_path)) + .unwrap() + .write_all(b"a") + .unwrap(); + + let repo_path = RepoPath::Workdir { + gitdir: git_dir.path().into(), + workdir: separate_workdir.path().into(), + }; + + let status = + get_status(&repo_path, StatusType::WorkingDir, None) + .unwrap(); + + assert_eq!( + status, + vec![StatusItem { + path: "foo".into(), + status: StatusItemType::New + }] + ); + } +} diff --git a/asyncgit/src/sync/tags.rs b/asyncgit/src/sync/tags.rs index f2193b21..7630b6dc 100644 --- a/asyncgit/src/sync/tags.rs +++ b/asyncgit/src/sync/tags.rs @@ -1,5 +1,8 @@ use super::{get_commits_info, CommitId, RepoPath}; -use crate::{error::Result, sync::repository::repo}; +use crate::{ + error::Result, + sync::{gix_repo, repository::repo}, +}; use scopetime::scope_time; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -58,10 +61,8 @@ pub fn get_tags(repo_path: &RepoPath) -> Result { } }; - let gix_repo: gix::Repository = - gix::ThreadSafeRepository::discover_with_environment_overrides(repo_path.gitpath()) - .map(Into::into)?; - let platform = gix_repo.references()?; + let repo: gix::Repository = gix_repo(repo_path)?; + let platform = repo.references()?; for mut reference in (platform.tags()?).flatten() { let commit = reference.peel_to_commit(); let tag = reference.peel_to_tag(); @@ -140,7 +141,7 @@ pub fn get_tags_with_metadata( }) .collect(); - tags.sort_unstable_by(|a, b| b.time.cmp(&a.time)); + tags.sort_unstable_by_key(|b| std::cmp::Reverse(b.time)); Ok(tags) } diff --git a/build.rs b/build.rs index 382490cf..c421e47d 100644 --- a/build.rs +++ b/build.rs @@ -34,14 +34,14 @@ fn main() { let build_date = now.date_naive(); let build_name = if std::env::var("GITUI_RELEASE").is_ok() { + env!("CARGO_PKG_VERSION").to_string() + } else { format!( - "{} {} ({})", + "{}-nightly {} ({})", env!("CARGO_PKG_VERSION"), build_date, get_git_hash() ) - } else { - format!("nightly {} ({})", build_date, get_git_hash()) }; println!("cargo:warning=buildname '{build_name}'"); diff --git a/deny.toml b/deny.toml index b8a4de30..92c14f0d 100644 --- a/deny.toml +++ b/deny.toml @@ -1,65 +1,63 @@ [licenses] allow = [ - "MIT", - "Apache-2.0", - "BSD-2-Clause", - "BSD-3-Clause", - "CC0-1.0", - "ISC", - "MPL-2.0", - "Unicode-3.0", - "Zlib", + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "CC0-1.0", + "ISC", + "MPL-2.0", + "Unicode-3.0", + "Zlib", ] [advisories] version = 2 ignore = [ - # No fix for RSA, and this is a dependency from ssh_key crate to handle rsa ssh key. - # https://rustsec.org/advisories/RUSTSEC-2023-0071 - "RUSTSEC-2023-0071", - # Crate paste is unmaintained. The dependency is already removed in - # ratatui:master. Until a new release is available, ignore this in - # order to pass CI. (https://github.com/gitui-org/gitui/issues/2554) - { id = "RUSTSEC-2024-0436", reason = "The paste dependency is already removed from ratatui." } + # No fix for RSA, and this is a dependency from ssh_key crate to handle rsa ssh key. + # https://rustsec.org/advisories/RUSTSEC-2023-0071 + "RUSTSEC-2023-0071", + # See https://github.com/trishume/syntect/issues/606 + { id = "RUSTSEC-2025-0141", reason = "Only brought in via syntect" }, ] [bans] multiple-versions = "deny" skip-tree = [ - # currently needed due to: - # * `dirs-sys v0.4.1` (https://github.com/dirs-dev/dirs-sys-rs/issues/29) - { name = "windows-sys" }, - # this is needed for: - # `bwrap v1.3.0` (https://github.com/micl2e2/bwrap/pull/4) - { name = "unicode-width" }, - # currently needed due to `ratatui v0.29.0` - { name = "unicode-truncate" }, - # currently needed due to: - # * `redox_users v0.4.6` - # * `syntect v5.2.0` - { name = "thiserror" }, - # currently needed due to: - # * `windows v0.57.0` - # * `iana-time-zone v0.1.60` - { name = "windows-core" }, - # currently needed due to: - # * `parking_lot_core v0.9.10` - # * `filetime v0.2.23` - { name = "redox_syscall" }, - # currently needed due to: - # * `gix-hashtable v0.6.0` - { name = "hashbrown" }, - # 2022-10-26 `getrandom` and `rustix` were added when `gitoxide` was - # upgraded from 0.71.0 to 0.74.1. - # currently needed due to: - # * `tempfile v3.23.0` - # * `rand_core v0.6.4` - # * `redox_users v0.5.0` - { name = "getrandom" }, - # currently needed due to: - # * `crossterm v0.28.1` - # * `which v7.0.2` - # * `gix-index v0.42.1` - # * `tempfile v3.23.0` - { name = "rustix" }, + # currently needed due to: + # * `dirs-sys v0.4.1` (https://github.com/dirs-dev/dirs-sys-rs/issues/29) + { name = "windows-sys" }, + # this is needed for: + # `bwrap v1.3.0` (https://github.com/micl2e2/bwrap/pull/4) + { name = "unicode-width" }, + # currently needed due to `ratatui v0.29.0` + { name = "unicode-truncate" }, + # currently needed due to: + # * `redox_users v0.4.6` + # * `syntect v5.2.0` + { name = "thiserror" }, + # currently needed due to: + # * `windows v0.57.0` + # * `iana-time-zone v0.1.60` + { name = "windows-core" }, + # currently needed due to: + # * `parking_lot_core v0.9.10` + # * `filetime v0.2.23` + { name = "redox_syscall" }, + # currently needed due to: + # * `gix-hashtable v0.6.0` + { name = "hashbrown" }, + # 2022-10-26 `getrandom` and `rustix` were added when `gitoxide` was + # upgraded from 0.71.0 to 0.74.1. + # currently needed due to: + # * `tempfile v3.23.0` + # * `rand_core v0.6.4` + # * `redox_users v0.5.0` + { name = "getrandom" }, + # currently needed due to: + # * `crossterm v0.28.1` + # * `which v7.0.2` + # * `gix-index v0.42.1` + # * `tempfile v3.23.0` + { name = "rustix" }, ] diff --git a/filetreelist/Cargo.toml b/filetreelist/Cargo.toml index aeba7eda..bc2b075e 100644 --- a/filetreelist/Cargo.toml +++ b/filetreelist/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "filetreelist" -version = "0.5.2" +version = "0.6.0" authors = ["extrawurst "] edition = "2021" description = "filetree abstraction based on a sorted path list, supports key based navigation events, folding, scrolling and more" @@ -9,7 +9,7 @@ repository = "https://github.com/gitui-org/gitui" readme = "README.md" license = "MIT" categories = ["command-line-utilities"] -keywords = ["gui", "cli", "terminal", "ui", "tui"] +keywords = ["cli", "gui", "terminal", "tui", "ui"] exclude = ["/demo.gif"] [dependencies] diff --git a/filetreelist/src/filetreeitems.rs b/filetreelist/src/filetreeitems.rs index 957eff36..08a850bf 100644 --- a/filetreelist/src/filetreeitems.rs +++ b/filetreelist/src/filetreeitems.rs @@ -59,7 +59,7 @@ impl FileTreeItems { } /// how many individual items (files/paths) are in the list - pub fn len(&self) -> usize { + pub const fn len(&self) -> usize { self.tree_items.len() } diff --git a/filetreelist/src/item.rs b/filetreelist/src/item.rs index b06c275d..e96aa80f 100644 --- a/filetreelist/src/item.rs +++ b/filetreelist/src/item.rs @@ -70,11 +70,11 @@ impl TreeItemInfo { } /// - pub fn unindent(&mut self) { + pub const fn unindent(&mut self) { self.indent = self.indent.saturating_sub(1); } - pub fn set_visible(&mut self, visible: bool) { + pub const fn set_visible(&mut self, visible: bool) { self.visible = visible; } } @@ -152,7 +152,7 @@ impl FileTreeItem { } /// - pub fn info_mut(&mut self) -> &mut TreeItemInfo { + pub const fn info_mut(&mut self) -> &mut TreeItemInfo { &mut self.info } @@ -176,12 +176,12 @@ impl FileTreeItem { } /// - pub fn hide(&mut self) { + pub const fn hide(&mut self) { self.info.visible = false; } /// - pub fn show(&mut self) { + pub const fn show(&mut self) { self.info.visible = true; } } diff --git a/git2-hooks/Cargo.toml b/git2-hooks/Cargo.toml index 34bbd45d..a6aae611 100644 --- a/git2-hooks/Cargo.toml +++ b/git2-hooks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git2-hooks" -version = "0.5.0" +version = "0.7.0" authors = ["extrawurst "] edition = "2021" description = "adds git hooks support based on git2-rs" @@ -14,7 +14,7 @@ keywords = ["git"] [dependencies] git2 = ">=0.17" -gix-path = "0.10" +gix-path = "0.11" log = "0.4" shellexpand = "3.1" thiserror = "2.0" diff --git a/git2-hooks/src/error.rs b/git2-hooks/src/error.rs index dd88440b..81dae71d 100644 --- a/git2-hooks/src/error.rs +++ b/git2-hooks/src/error.rs @@ -14,6 +14,9 @@ pub enum HooksError { #[error("shellexpand error:{0}")] ShellExpand(#[from] shellexpand::LookupError), + + #[error("hook process terminated by signal without exit code")] + NoExitCode, } /// crate specific `Result` type diff --git a/git2-hooks/src/hookspath.rs b/git2-hooks/src/hookspath.rs index 33fe1bf6..5c35c9df 100644 --- a/git2-hooks/src/hookspath.rs +++ b/git2-hooks/src/hookspath.rs @@ -141,6 +141,20 @@ impl HookPaths { /// this function calls hook scripts based on conventions documented here /// see pub fn run_hook_os_str(&self, args: I) -> Result + where + I: IntoIterator + Copy, + S: AsRef, + { + self.run_hook_os_str_with_stdin(args, None) + } + + /// this function calls hook scripts with stdin input based on conventions documented here + /// see + pub fn run_hook_os_str_with_stdin( + &self, + args: I, + stdin: Option<&[u8]>, + ) -> Result where I: IntoIterator + Copy, S: AsRef, @@ -153,11 +167,42 @@ impl HookPaths { ); let run_command = |command: &mut Command| { - command + let mut child = command .args(args) .current_dir(&self.pwd) .with_no_window() - .output() + .stdin(if stdin.is_some() { + std::process::Stdio::piped() + } else { + std::process::Stdio::null() + }) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn()?; + + if let (Some(mut stdin_handle), Some(input)) = + (child.stdin.take(), stdin) + { + use std::io::{ErrorKind, Write}; + + // Write stdin to hook process + // Ignore broken pipe - hook may exit early without reading all input + let _ = + stdin_handle.write_all(input).inspect_err(|e| { + match e.kind() { + ErrorKind::BrokenPipe => { + log::debug!( + "Hook closed stdin early" + ); + } + _ => log::warn!( + "Failed to write stdin to hook: {e}" + ), + } + }); + } + + child.wait_with_output() }; let output = if cfg!(windows) { @@ -210,21 +255,21 @@ impl HookPaths { } }?; - if output.status.success() { - Ok(HookResult::Ok { hook }) - } else { - let stderr = - String::from_utf8_lossy(&output.stderr).to_string(); - let stdout = - String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = + String::from_utf8_lossy(&output.stderr).to_string(); + let stdout = + String::from_utf8_lossy(&output.stdout).to_string(); - Ok(HookResult::RunNotSuccessful { - code: output.status.code(), - stdout, - stderr, - hook, - }) - } + // Get exit code, or fail if process was killed by signal + let code = + output.status.code().ok_or(HooksError::NoExitCode)?; + + Ok(HookResult::Run(crate::HookRunResponse { + hook, + stdout, + stderr, + code, + })) } } diff --git a/git2-hooks/src/lib.rs b/git2-hooks/src/lib.rs index dd1fb664..de077122 100644 --- a/git2-hooks/src/lib.rs +++ b/git2-hooks/src/lib.rs @@ -38,7 +38,7 @@ pub use error::HooksError; use error::Result; use hookspath::HookPaths; -use git2::Repository; +use git2::{Oid, Repository}; pub const HOOK_POST_COMMIT: &str = "post-commit"; pub const HOOK_PRE_COMMIT: &str = "pre-commit"; @@ -48,37 +48,98 @@ pub const HOOK_PRE_PUSH: &str = "pre-push"; const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG"; +/// Check if a given hook is present considering config/paths and optional extra paths. +pub fn hook_available( + repo: &Repository, + other_paths: Option<&[&str]>, + hook: &str, +) -> Result { + let hook = HookPaths::new(repo, other_paths, hook)?; + Ok(hook.found()) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PrePushRef { + pub local_ref: String, + pub local_oid: Option, + pub remote_ref: String, + pub remote_oid: Option, +} + +impl PrePushRef { + pub fn new( + local_ref: impl Into, + local_oid: Option, + remote_ref: impl Into, + remote_oid: Option, + ) -> Self { + Self { + local_ref: local_ref.into(), + local_oid, + remote_ref: remote_ref.into(), + remote_oid, + } + } + + fn format_oid(oid: Option) -> String { + // "If the foreign ref does not yet exist the will be the all-zeroes object name" + // see https://git-scm.com/docs/githooks#_pre_push + oid.map_or_else(|| "0".repeat(40), |id| id.to_string()) + } + + pub fn to_line(&self) -> String { + format!( + "{} {} {} {}", + self.local_ref, + Self::format_oid(self.local_oid), + self.remote_ref, + Self::format_oid(self.remote_oid) + ) + } + + /// Build stdin content from a slice of updates (for pre-push hook) + pub fn to_stdin(updates: &[Self]) -> String { + let mut stdin = String::new(); + for update in updates { + stdin.push_str(&update.to_line()); + stdin.push('\n'); + } + stdin + } +} + +/// Response from running a hook +#[derive(Debug, PartialEq, Eq)] +pub struct HookRunResponse { + /// path of the hook that was run + pub hook: PathBuf, + /// stdout output emitted by hook + pub stdout: String, + /// stderr output emitted by hook + pub stderr: String, + /// exit code as reported back from process calling the hook (0 = success) + pub code: i32, +} + #[derive(Debug, PartialEq, Eq)] pub enum HookResult { /// No hook found NoHookFound, - /// Hook executed with non error return code - Ok { - /// path of the hook that was run - hook: PathBuf, - }, - /// Hook executed and returned an error code - RunNotSuccessful { - /// exit code as reported back from process calling the hook - code: Option, - /// stderr output emitted by hook - stdout: String, - /// stderr output emitted by hook - stderr: String, - /// path of the hook that was run - hook: PathBuf, - }, + /// Hook executed (check `HookRunResponse.code` for success/failure) + Run(HookRunResponse), } impl HookResult { - /// helper to check if result is ok - pub const fn is_ok(&self) -> bool { - matches!(self, Self::Ok { .. }) + /// helper to check if hook ran successfully (found and exit code 0) + pub const fn is_successful(&self) -> bool { + matches!(self, Self::Run(response) if response.is_successful()) } +} - /// helper to check if result was run and not rejected - pub const fn is_not_successful(&self) -> bool { - matches!(self, Self::RunNotSuccessful { .. }) +impl HookRunResponse { + /// Check if the hook succeeded (exit code 0) + pub const fn is_successful(&self) -> bool { + self.code == 0 } } @@ -172,9 +233,23 @@ pub fn hooks_post_commit( } /// this hook is documented here +/// +/// According to git documentation, pre-push hook receives: +/// - remote name as first argument (or URL if remote is not named) +/// - remote URL as second argument +/// - information about refs being pushed via stdin in format: +/// ` SP SP SP LF` +/// +/// If `remote` is `None` or empty, the `url` is used for both arguments as per Git spec. +/// +/// Note: The hook is called even when `updates` is empty (matching Git's behavior). +/// This can occur when pushing tags that already exist on the remote. pub fn hooks_pre_push( repo: &Repository, other_paths: Option<&[&str]>, + remote: Option<&str>, + url: &str, + updates: &[PrePushRef], ) -> Result { let hook = HookPaths::new(repo, other_paths, HOOK_PRE_PUSH)?; @@ -182,7 +257,18 @@ pub fn hooks_pre_push( return Ok(HookResult::NoHookFound); } - hook.run_hook(&[]) + // If a remote is not named (None or empty), the URL is passed for both arguments + let remote_name = match remote { + Some(r) if !r.is_empty() => r, + _ => url, + }; + + let stdin_data = PrePushRef::to_stdin(updates); + + hook.run_hook_os_str_with_stdin( + [remote_name, url], + Some(stdin_data.as_bytes()), + ) } pub enum PrepareCommitMsgSource { @@ -251,6 +337,110 @@ mod tests { use pretty_assertions::assert_eq; use tempfile::TempDir; + fn branch_update( + repo: &Repository, + remote: Option<&str>, + branch: &str, + remote_branch: Option<&str>, + delete: bool, + ) -> PrePushRef { + let local_ref = format!("refs/heads/{branch}"); + let local_oid = (!delete).then(|| { + repo.find_branch(branch, git2::BranchType::Local) + .unwrap() + .get() + .peel_to_commit() + .unwrap() + .id() + }); + + let remote_branch = remote_branch.unwrap_or(branch); + let remote_ref = format!("refs/heads/{remote_branch}"); + let remote_oid = remote.and_then(|remote_name| { + repo.find_reference(&format!( + "refs/remotes/{remote_name}/{remote_branch}" + )) + .ok() + .and_then(|r| r.peel_to_commit().ok()) + .map(|c| c.id()) + }); + + PrePushRef::new(local_ref, local_oid, remote_ref, remote_oid) + } + + fn head_branch(repo: &Repository) -> String { + repo.head().unwrap().shorthand().unwrap().to_string() + } + + #[test] + fn test_pre_push_ref_format() { + let zero_oid = "0".repeat(40); + let oid_a = "a".repeat(40); + let oid_b = "b".repeat(40); + + // Both oids present + let update = PrePushRef::new( + "refs/heads/main", + Some(git2::Oid::from_str(&oid_a).unwrap()), + "refs/heads/main", + Some(git2::Oid::from_str(&oid_b).unwrap()), + ); + assert_eq!( + update.to_line(), + format!( + "refs/heads/main {oid_a} refs/heads/main {oid_b}" + ) + ); + + // No remote oid (new branch) + let update = PrePushRef::new( + "refs/heads/feature", + Some(git2::Oid::from_str(&oid_a).unwrap()), + "refs/heads/feature", + None, + ); + assert_eq!( + update.to_line(), + format!("refs/heads/feature {oid_a} refs/heads/feature {zero_oid}") + ); + + // No local oid (delete) + let update = PrePushRef::new( + "refs/heads/old", + None, + "refs/heads/old", + Some(git2::Oid::from_str(&oid_b).unwrap()), + ); + assert_eq!( + update.to_line(), + format!( + "refs/heads/old {zero_oid} refs/heads/old {oid_b}" + ) + ); + + // to_stdin adds newlines + let updates = [ + PrePushRef::new( + "refs/heads/a", + Some(git2::Oid::from_str(&oid_a).unwrap()), + "refs/heads/a", + None, + ), + PrePushRef::new( + "refs/heads/b", + Some(git2::Oid::from_str(&oid_b).unwrap()), + "refs/heads/b", + None, + ), + ]; + assert_eq!( + PrePushRef::to_stdin(&updates), + format!( + "refs/heads/a {oid_a} refs/heads/a {zero_oid}\nrefs/heads/b {oid_b} refs/heads/b {zero_oid}\n" + ) + ); + } + #[test] fn test_smoke() { let (_td, repo) = repo_init(); @@ -268,7 +458,7 @@ exit 0 let res = hooks_post_commit(&repo, None).unwrap(); - assert!(res.is_ok()); + assert!(res.is_successful()); } #[test] @@ -284,7 +474,7 @@ exit 0 let mut msg = String::from("test"); let res = hooks_commit_msg(&repo, None, &mut msg).unwrap(); - assert!(res.is_ok()); + assert!(res.is_successful()); assert_eq!(msg, String::from("test")); } @@ -304,7 +494,7 @@ exit 0 let mut msg = String::from("test_sth"); let res = hooks_commit_msg(&repo, None, &mut msg).unwrap(); - assert!(res.is_ok()); + assert!(res.is_successful()); assert_eq!(msg, String::from("test_shell_command")); } @@ -319,7 +509,7 @@ exit 0 create_hook(&repo, HOOK_PRE_COMMIT, hook); let res = hooks_pre_commit(&repo, None).unwrap(); - assert!(res.is_ok()); + assert!(res.is_successful()); } #[test] @@ -339,22 +529,16 @@ exit 0 let result = hook.run_hook(&[TEXT]).unwrap(); - let HookResult::RunNotSuccessful { - code, - stdout, - stderr, - hook: h, - } = result - else { - unreachable!("run_hook should've failed"); + let HookResult::Run(response) = result else { + unreachable!("run_hook should've run"); }; - let stdout = stdout.as_str().trim_ascii_end(); + let stdout = response.stdout.as_str().trim_ascii_end(); - assert_eq!(code, Some(42)); - assert_eq!(h, hook.hook); + assert_eq!(response.code, 42); + assert_eq!(response.hook, hook.hook); assert_eq!(stdout, TEXT, "{:?} != {TEXT:?}", stdout); - assert!(stderr.is_empty()); + assert!(response.stderr.is_empty()); } #[test] @@ -384,7 +568,7 @@ exit 0 let res = hooks_pre_commit(&repo, Some(&["../.myhooks"])).unwrap(); - assert!(res.is_ok()); + assert!(res.is_successful()); } #[test] @@ -417,7 +601,7 @@ exit 1 let res = hooks_pre_commit(&repo, Some(&["../.myhooks"])).unwrap(); - assert!(res.is_ok()); + assert!(res.is_successful()); } #[test] @@ -431,7 +615,7 @@ exit 1 create_hook(&repo, HOOK_PRE_COMMIT, hook); let res = hooks_pre_commit(&repo, None).unwrap(); - assert!(res.is_not_successful()); + assert!(!res.is_successful()); } #[test] @@ -448,15 +632,17 @@ exit 1 create_hook(&repo, HOOK_PRE_COMMIT, hook); let res = hooks_pre_commit(&repo, None).unwrap(); - let HookResult::RunNotSuccessful { stdout, .. } = res else { + let HookResult::Run(response) = res else { unreachable!() }; assert!( - stdout + response + .stdout .lines() .any(|line| line.starts_with(PATH_EXPORT)), - "Could not find line starting with {PATH_EXPORT:?} in: {stdout:?}" + "Could not find line starting with {PATH_EXPORT:?} in: {:?}", + response.stdout ); } @@ -482,13 +668,12 @@ exit 1 let res = hooks_pre_commit(&repo, None).unwrap(); - let HookResult::RunNotSuccessful { code, stdout, .. } = res - else { + let HookResult::Run(response) = res else { unreachable!() }; - assert_eq!(code.unwrap(), 1); - assert_eq!(&stdout, "rejected\n"); + assert_eq!(response.code, 1); + assert_eq!(&response.stdout, "rejected\n"); } #[test] @@ -502,7 +687,7 @@ exit 1 create_hook(&repo, HOOK_PRE_COMMIT, hook); let res = hooks_pre_commit(&repo, None).unwrap(); - assert!(res.is_not_successful()); + assert!(!res.is_successful()); } #[test] @@ -523,7 +708,7 @@ sys.exit(0) create_hook(&repo, HOOK_PRE_COMMIT, hook); let res = hooks_pre_commit(&repo, None).unwrap(); - assert!(res.is_ok(), "{res:?}"); + assert!(res.is_successful(), "{res:?}"); } #[test] @@ -544,7 +729,7 @@ sys.exit(1) create_hook(&repo, HOOK_PRE_COMMIT, hook); let res = hooks_pre_commit(&repo, None).unwrap(); - assert!(res.is_not_successful()); + assert!(!res.is_successful()); } #[test] @@ -562,13 +747,12 @@ sys.exit(1) let mut msg = String::from("test"); let res = hooks_commit_msg(&repo, None, &mut msg).unwrap(); - let HookResult::RunNotSuccessful { code, stdout, .. } = res - else { + let HookResult::Run(response) = res else { unreachable!() }; - assert_eq!(code.unwrap(), 1); - assert_eq!(&stdout, "rejected\n"); + assert_eq!(response.code, 1); + assert_eq!(&response.stdout, "rejected\n"); assert_eq!(msg, String::from("msg\n")); } @@ -587,7 +771,7 @@ exit 0 let mut msg = String::from("test"); let res = hooks_commit_msg(&repo, None, &mut msg).unwrap(); - assert!(res.is_ok()); + assert!(res.is_successful()); assert_eq!(msg, String::from("msg\n")); } @@ -633,7 +817,7 @@ exit 0 ) .unwrap(); - assert!(matches!(res, HookResult::Ok { .. })); + assert!(res.is_successful()); assert_eq!(msg, String::from("msg:message\n")); } @@ -658,13 +842,12 @@ exit 2 ) .unwrap(); - let HookResult::RunNotSuccessful { code, stdout, .. } = res - else { + let HookResult::Run(response) = res else { unreachable!() }; - assert_eq!(code.unwrap(), 2); - assert_eq!(&stdout, "rejected\n"); + assert_eq!(response.code, 2); + assert_eq!(&response.stdout, "rejected\n"); assert_eq!( msg, @@ -684,9 +867,25 @@ exit 0 create_hook(&repo, HOOK_PRE_PUSH, hook); - let res = hooks_pre_push(&repo, None).unwrap(); + let branch = head_branch(&repo); + let updates = [branch_update( + &repo, + Some("origin"), + &branch, + None, + false, + )]; - assert!(matches!(res, HookResult::Ok { .. })); + let res = hooks_pre_push( + &repo, + None, + Some("origin"), + "https://example.com/repo.git", + &updates, + ) + .unwrap(); + + assert!(res.is_successful()); } #[test] @@ -698,12 +897,331 @@ echo 'failed' exit 3 "; create_hook(&repo, HOOK_PRE_PUSH, hook); - let res = hooks_pre_push(&repo, None).unwrap(); - let HookResult::RunNotSuccessful { code, stdout, .. } = res - else { + + let branch = head_branch(&repo); + let updates = [branch_update( + &repo, + Some("origin"), + &branch, + None, + false, + )]; + + let res = hooks_pre_push( + &repo, + None, + Some("origin"), + "https://example.com/repo.git", + &updates, + ) + .unwrap(); + let HookResult::Run(response) = res else { unreachable!() }; - assert_eq!(code.unwrap(), 3); - assert_eq!(&stdout, "failed\n"); + assert_eq!(response.code, 3); + assert_eq!(&response.stdout, "failed\n"); + } + + #[test] + fn test_pre_push_no_remote_name() { + let (_td, repo) = repo_init(); + + let hook = b"#!/bin/sh +# Verify that when remote is None, URL is passed for both arguments +echo \"arg1=$1 arg2=$2\" +exit 0 + "; + + create_hook(&repo, HOOK_PRE_PUSH, hook); + + let branch = head_branch(&repo); + let updates = + [branch_update(&repo, None, &branch, None, false)]; + + let res = hooks_pre_push( + &repo, + None, + None, + "https://example.com/repo.git", + &updates, + ) + .unwrap(); + + let HookResult::Run(response) = res else { + panic!("Expected Run result, got: {res:?}"); + }; + + assert!(response.is_successful()); + // When remote is None, URL should be passed for both arguments + assert_eq!( + response.stdout, + "arg1=https://example.com/repo.git arg2=https://example.com/repo.git\n" + ); + } + + #[test] + fn test_pre_push_with_arguments() { + let (_td, repo) = repo_init(); + + let hook = b"#!/bin/sh +echo \"remote_name=$1\" +echo \"remote_url=$2\" +exit 0 + "; + + create_hook(&repo, HOOK_PRE_PUSH, hook); + + let branch = head_branch(&repo); + let updates = [branch_update( + &repo, + Some("origin"), + &branch, + None, + false, + )]; + + let res = hooks_pre_push( + &repo, + None, + Some("origin"), + "https://example.com/repo.git", + &updates, + ) + .unwrap(); + + let HookResult::Run(response) = res else { + unreachable!("Expected Run result, got: {res:?}") + }; + + assert!(response.is_successful()); + assert_eq!( + response.stdout, + "remote_name=origin\nremote_url=https://example.com/repo.git\n" + ); + } + + #[test] + fn test_pre_push_multiple_updates() { + let (_td, repo) = repo_init(); + + let hook = b"#!/bin/sh +cat +exit 0 + "; + + create_hook(&repo, HOOK_PRE_PUSH, hook); + + let branch = head_branch(&repo); + let branch_update = branch_update( + &repo, + Some("origin"), + &branch, + None, + false, + ); + + // create a tag to add a second refspec + let head_commit = + repo.head().unwrap().peel_to_commit().unwrap(); + repo.tag_lightweight("v1", head_commit.as_object(), false) + .unwrap(); + let tag_ref = repo.find_reference("refs/tags/v1").unwrap(); + let tag_oid = tag_ref.target().unwrap(); + let tag_update = PrePushRef::new( + "refs/tags/v1", + Some(tag_oid), + "refs/tags/v1", + None, + ); + + let updates = [branch_update, tag_update]; + let expected_stdin = PrePushRef::to_stdin(&updates); + + let res = hooks_pre_push( + &repo, + None, + Some("origin"), + "https://example.com/repo.git", + &updates, + ) + .unwrap(); + + let HookResult::Run(response) = res else { + unreachable!("Expected Run result, got: {res:?}") + }; + + assert!( + response.is_successful(), + "Hook should succeed: stdout {} stderr {}", + response.stdout, + response.stderr + ); + assert_eq!( + response.stdout, expected_stdin, + "stdin should include all refspec lines" + ); + } + + #[test] + fn test_pre_push_delete_ref_uses_zero_oid() { + let (_td, repo) = repo_init(); + + let hook = b"#!/bin/sh +cat +exit 0 + "; + + create_hook(&repo, HOOK_PRE_PUSH, hook); + + let branch = head_branch(&repo); + let updates = [branch_update( + &repo, + Some("origin"), + &branch, + None, + true, + )]; + let expected_stdin = PrePushRef::to_stdin(&updates); + + let res = hooks_pre_push( + &repo, + None, + Some("origin"), + "https://example.com/repo.git", + &updates, + ) + .unwrap(); + + let HookResult::Run(response) = res else { + unreachable!("Expected Run result, got: {res:?}") + }; + + assert!(response.is_successful()); + assert_eq!(response.stdout, expected_stdin); + } + + #[test] + fn test_pre_push_stdin() { + let (_td, repo) = repo_init(); + + let hook = b"#!/bin/sh +cat +exit 0 + "; + + create_hook(&repo, HOOK_PRE_PUSH, hook); + + let branch = head_branch(&repo); + let updates = [branch_update( + &repo, + Some("origin"), + &branch, + None, + false, + )]; + let expected_stdin = PrePushRef::to_stdin(&updates); + + let res = hooks_pre_push( + &repo, + None, + Some("origin"), + "https://github.com/user/repo.git", + &updates, + ) + .unwrap(); + + let HookResult::Run(response) = res else { + unreachable!("Expected Run result, got: {res:?}") + }; + + assert!(response.is_successful()); + assert_eq!(response.stdout, expected_stdin); + } + + #[test] + fn test_pre_push_uses_push_target_remote_not_upstream() { + let (_td, repo) = repo_init(); + + // repo_init() already creates an initial commit on master + let head = repo.head().unwrap(); + let local_commit = head.target().unwrap(); + + // Set up scenario: + // - Local master is at local_commit (latest) + // - origin/master exists at local_commit (fully synced - upstream) + // - backup/master exists at old_commit (behind/different) + // - Branch tracks origin/master as upstream + // - We push to "backup" remote + // - Expected: remote SHA should be old_commit (not origin/master) + + // Create origin/master tracking branch (at same commit as local) + repo.reference( + "refs/remotes/origin/master", + local_commit, + true, + "create origin/master", + ) + .unwrap(); + + // Create backup/master at a different commit + let sig = repo.signature().unwrap(); + let tree_id = { + let mut index = repo.index().unwrap(); + index.write_tree().unwrap() + }; + let tree = repo.find_tree(tree_id).unwrap(); + let old_commit = repo + .commit(None, &sig, &sig, "old backup commit", &tree, &[]) + .unwrap(); + + repo.reference( + "refs/remotes/backup/master", + old_commit, + true, + "create backup/master at old commit", + ) + .unwrap(); + + // Configure upstream to origin + { + let mut config = repo.config().unwrap(); + config.set_str("branch.master.remote", "origin").unwrap(); + config + .set_str("branch.master.merge", "refs/heads/master") + .unwrap(); + } + + let hook = b"#!/bin/sh +cat +exit 0 +"; + + create_hook(&repo, HOOK_PRE_PUSH, hook); + + let branch = head_branch(&repo); + let updates = [branch_update( + &repo, + Some("backup"), + &branch, + None, + false, + )]; + let expected_stdin = PrePushRef::to_stdin(&updates); + + let res = hooks_pre_push( + &repo, + None, + Some("backup"), + "https://github.com/user/backup-repo.git", + &updates, + ) + .unwrap(); + + let HookResult::Run(response) = res else { + panic!("Expected Run result, got: {res:?}") + }; + + assert!(response.is_successful()); + assert_eq!(response.stdout, expected_stdin); } } diff --git a/git2-testing/src/lib.rs b/git2-testing/src/lib.rs index 40e16790..838e3013 100644 --- a/git2-testing/src/lib.rs +++ b/git2-testing/src/lib.rs @@ -20,13 +20,18 @@ pub fn repo_init_empty() -> (TempDir, Repository) { (td, repo) } -/// initialize test repo in temp path with an empty first commit -pub fn repo_init() -> (TempDir, Repository) { +/// initialize test repo in temp path with given suffix and an empty first commit +pub fn repo_init_suffix>( + suffix: Option, +) -> (TempDir, Repository) { init_log(); sandbox_config_files(); - let td = TempDir::new().unwrap(); + let td = match suffix { + Some(suffix) => TempDir::with_suffix(suffix).unwrap(), + None => TempDir::new().unwrap(), + }; let repo = Repository::init(td.path()).unwrap(); { let mut config = repo.config().unwrap(); @@ -45,6 +50,11 @@ pub fn repo_init() -> (TempDir, Repository) { (td, repo) } +/// initialize test repo in temp path with an empty first commit +pub fn repo_init() -> (TempDir, Repository) { + repo_init_suffix::<&std::ffi::OsStr>(None) +} + // init log fn init_log() { let _ = env_logger::builder() diff --git a/rustfmt.toml b/rustfmt.toml index aec36615..da5d80cd 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,3 @@ -max_width=70 -hard_tabs=true -newline_style="Unix" \ No newline at end of file +max_width = 70 +hard_tabs = true +newline_style = "Unix" diff --git a/scopetime/Cargo.toml b/scopetime/Cargo.toml index 2cce308a..9343673c 100644 --- a/scopetime/Cargo.toml +++ b/scopetime/Cargo.toml @@ -9,11 +9,11 @@ repository = "https://github.com/gitui-org/gitui" license = "MIT" readme = "README.md" categories = ["development-tools::profiling"] -keywords = ["profiling", "logging"] +keywords = ["logging", "profiling"] + +[dependencies] +log = "0.4" [features] default = [] enabled = [] - -[dependencies] -log = "0.4" diff --git a/src/app.rs b/src/app.rs index 479e2868..8626aa3b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::{ accessors, + args::CliArgs, cmdbar::CommandBar, components::{ command_pump, event_pump, CommandInfo, Component, @@ -10,16 +11,16 @@ use crate::{ options::{Options, SharedOptions}, popup_stack::PopupStack, popups::{ - AppOption, BlameFilePopup, BranchListPopup, CommitPopup, - CompareCommitsPopup, ConfirmPopup, CreateBranchPopup, - CreateRemotePopup, ExternalEditorPopup, FetchPopup, - FileRevlogPopup, FuzzyFindPopup, HelpPopup, - InspectCommitPopup, LogSearchPopupPopup, MsgPopup, - OptionsPopup, PullPopup, PushPopup, PushTagsPopup, - RemoteListPopup, RenameBranchPopup, RenameRemotePopup, - ResetPopup, RevisionFilesPopup, StashMsgPopup, - SubmodulesListPopup, TagCommitPopup, TagListPopup, - UpdateRemoteUrlPopup, + AppOption, BlameFilePopup, BranchListPopup, + CheckoutOptionPopup, CommitPopup, CompareCommitsPopup, + ConfirmPopup, CreateBranchPopup, CreateRemotePopup, + ExternalEditorPopup, FetchPopup, FileRevlogPopup, + FuzzyFindPopup, GotoLinePopup, HelpPopup, InspectCommitPopup, + LogSearchPopupPopup, MsgPopup, OptionsPopup, PullPopup, + PushPopup, PushTagsPopup, RemoteListPopup, RenameBranchPopup, + RenameRemotePopup, ResetPopup, RevisionFilesPopup, + StashMsgPopup, SubmodulesListPopup, TagCommitPopup, + TagListPopup, UpdateRemoteUrlPopup, }, queue::{ Action, AppTabs, InternalEvent, NeedsUpdate, Queue, @@ -98,6 +99,7 @@ pub struct App { submodule_popup: SubmodulesListPopup, tags_popup: TagListPopup, reset_popup: ResetPopup, + checkout_option_popup: CheckoutOptionPopup, cmdbar: RefCell, tab: usize, revlog: Revlog, @@ -112,6 +114,7 @@ pub struct App { popup_stack: PopupStack, options: SharedOptions, repo_path_text: String, + goto_line_popup: GotoLinePopup, // "Flags" requires_redraw: Cell, @@ -150,13 +153,14 @@ impl App { /// #[allow(clippy::too_many_lines)] pub fn new( - repo: RepoPathRef, + cliargs: CliArgs, sender_git: Sender, sender_app: Sender, input: Input, theme: Theme, key_config: KeyConfig, ) -> Result { + let repo = RefCell::new(cliargs.repo_path.clone()); log::trace!("open repo at: {:?}", &repo); let repo_path_text = @@ -172,7 +176,20 @@ impl App { sender_app, }; - let tab = env.options.borrow().current_tab(); + let mut select_file: Option = None; + let tab = if let Some(file) = cliargs.select_file { + // convert to relative git path + if let Ok(abs) = file.canonicalize() { + if let Ok(path) = abs.strip_prefix( + env.repo.borrow().gitpath().canonicalize()?, + ) { + select_file = Some(Path::new(".").join(path)); + } + } + 2 + } else { + env.options.borrow().current_tab() + }; let mut app = Self { input, @@ -217,7 +234,9 @@ impl App { status_tab: Status::new(&env), stashing_tab: Stashing::new(&env), stashlist_tab: StashList::new(&env), - files_tab: FilesTab::new(&env), + files_tab: FilesTab::new(&env, select_file), + checkout_option_popup: CheckoutOptionPopup::new(&env), + goto_line_popup: GotoLinePopup::new(&env), tab: 0, queue: env.queue, theme: env.theme, @@ -481,6 +500,7 @@ impl App { msg_popup, confirm_popup, commit_popup, + goto_line_popup, blame_file_popup, file_revlog_popup, stashmsg_popup, @@ -493,6 +513,7 @@ impl App { fetch_popup, tag_commit_popup, reset_popup, + checkout_option_popup, create_branch_popup, create_remote_popup, rename_remote_popup, @@ -533,6 +554,7 @@ impl App { submodule_popup, tags_popup, reset_popup, + checkout_option_popup, create_branch_popup, rename_branch_popup, revision_files_popup, @@ -544,7 +566,8 @@ impl App { fetch_popup, options_popup, confirm_popup, - msg_popup + msg_popup, + goto_line_popup ] ); @@ -905,6 +928,17 @@ impl App { InternalEvent::CommitSearch(options) => { self.revlog.search(options); } + InternalEvent::OpenGotoLinePopup(max_line) => { + self.goto_line_popup.open(max_line); + } + InternalEvent::GotoLine(line) => { + if self.blame_file_popup.is_visible() { + self.blame_file_popup.goto_line(line); + } + } + InternalEvent::CheckoutOption(branch) => { + self.checkout_option_popup.open(branch)?; + } } Ok(flags) @@ -1063,7 +1097,7 @@ impl App { Err(e) => { log::error!("delete remote: {e:?}"); self.queue.push(InternalEvent::ShowErrorMsg( - format!("delete remote error:\n{e}",), + format!("delete remote error:\n{e}"), )); } } diff --git a/src/args.rs b/src/args.rs index a7d9d995..22c6cc8d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -17,15 +17,22 @@ const LOG_FILE_FLAG_ID: &str = "logfile"; const LOGGING_FLAG_ID: &str = "logging"; const THEME_FLAG_ID: &str = "theme"; const WORKDIR_FLAG_ID: &str = "workdir"; +const FILE_FLAG_ID: &str = "file"; const GIT_DIR_FLAG_ID: &str = "directory"; const WATCHER_FLAG_ID: &str = "watcher"; +const KEY_BINDINGS_FLAG_ID: &str = "key_bindings"; +const KEY_SYMBOLS_FLAG_ID: &str = "key_symbols"; const DEFAULT_THEME: &str = "theme.ron"; const DEFAULT_GIT_DIR: &str = "."; +#[derive(Clone)] pub struct CliArgs { pub theme: PathBuf, + pub select_file: Option, pub repo_path: RepoPath, pub notify_watcher: bool, + pub key_bindings_path: Option, + pub key_symbols_path: Option, } pub fn process_cmdline() -> Result { @@ -51,6 +58,10 @@ pub fn process_cmdline() -> Result { PathBuf::from, ); + let select_file = arg_matches + .get_one::(FILE_FLAG_ID) + .map(PathBuf::from); + let repo_path = if let Some(w) = workdir { RepoPath::Workdir { gitdir, workdir: w } } else { @@ -73,10 +84,21 @@ pub fn process_cmdline() -> Result { let notify_watcher: bool = *arg_matches.get_one(WATCHER_FLAG_ID).unwrap_or(&false); + let key_bindings_path = arg_matches + .get_one::(KEY_BINDINGS_FLAG_ID) + .map(PathBuf::from); + + let key_symbols_path = arg_matches + .get_one::(KEY_SYMBOLS_FLAG_ID) + .map(PathBuf::from); + Ok(CliArgs { theme, + select_file, repo_path, notify_watcher, + key_bindings_path, + key_symbols_path, }) } @@ -95,6 +117,22 @@ fn app() -> ClapApp { {all-args}{after-help} ", + ) + .arg( + Arg::new(KEY_BINDINGS_FLAG_ID) + .help("Use a custom keybindings file") + .short('k') + .long("key-bindings") + .value_name("KEY_LIST_FILENAME") + .num_args(1), + ) + .arg( + Arg::new(KEY_SYMBOLS_FLAG_ID) + .help("Use a custom symbols file") + .short('s') + .long("key-symbols") + .value_name("KEY_SYMBOLS_FILENAME") + .num_args(1), ) .arg( Arg::new(THEME_FLAG_ID) @@ -129,6 +167,13 @@ fn app() -> ClapApp { .long("bugreport") .action(clap::ArgAction::SetTrue), ) + .arg( + Arg::new(FILE_FLAG_ID) + .help("Select the file in the file tab") + .short('f') + .long("file") + .num_args(1), + ) .arg( Arg::new(GIT_DIR_FLAG_ID) .help("Set the git directory") diff --git a/src/cmdbar.rs b/src/cmdbar.rs index 6fce784d..aa7f6309 100644 --- a/src/cmdbar.rs +++ b/src/cmdbar.rs @@ -130,7 +130,7 @@ impl CommandBar { } } - pub fn toggle_more(&mut self) { + pub const fn toggle_more(&mut self) { if self.expandable { self.expanded = !self.expanded; } diff --git a/src/components/changes.rs b/src/components/changes.rs index 48883d20..74f11e58 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -67,7 +67,7 @@ impl ChangesComponent { } /// returns true if list is empty - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.files.is_empty() } diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index fa21185d..a8406015 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -113,7 +113,7 @@ impl CommitList { } /// - pub fn marked_count(&self) -> usize { + pub const fn marked_count(&self) -> usize { self.marked.len() } @@ -284,7 +284,7 @@ impl CommitList { } /// will return view size or None before the first render - fn current_size(&self) -> Option<(u16, u16)> { + const fn current_size(&self) -> Option<(u16, u16)> { self.current_size.get() } diff --git a/src/components/diff.rs b/src/components/diff.rs index 2eb492db..04779caa 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -152,6 +152,10 @@ impl DiffComponent { (self.current.path.clone(), self.current.is_stage) } /// + const fn can_edit_file(&self) -> bool { + !self.is_immutable && !self.current.path.is_empty() + } + /// pub fn clear(&mut self, pending: bool) { self.current = Current::default(); self.diff = None; @@ -770,6 +774,11 @@ impl Component for DiffComponent { ); if !self.is_immutable { + out.push(CommandInfo::new( + strings::commands::edit_item(&self.key_config), + self.can_edit_file(), + self.focused() && self.can_edit_file(), + )); out.push(CommandInfo::new( strings::commands::diff_hunk_remove(&self.key_config), self.selected_hunk.is_some(), @@ -876,6 +885,15 @@ impl Component for DiffComponent { ) { self.diff_hunk_move_up_down(-1); Ok(EventState::Consumed) + } else if key_match(e, self.key_config.keys.edit_file) + && self.can_edit_file() + { + self.queue.push( + InternalEvent::OpenExternalEditor(Some( + self.current.path.clone(), + )), + ); + Ok(EventState::Consumed) } else if key_match( e, self.key_config.keys.stage_unstage_item, @@ -945,7 +963,10 @@ impl Component for DiffComponent { #[cfg(test)] mod tests { use super::*; - use crate::ui::style::Theme; + use crate::{ + app::Environment, queue::InternalEvent, ui::style::Theme, + }; + use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use std::io::Write; use std::rc::Rc; use tempfile::NamedTempFile; @@ -1012,4 +1033,30 @@ mod tests { ); } } + + #[test] + fn diff_component_opens_editor_for_current_file() { + let env = Environment::test_env(); + let mut diff = DiffComponent::new(&env, false); + + diff.focus(true); + diff.current.path = String::from("src/main.rs"); + + let event = Event::Key(KeyEvent::new( + KeyCode::Char('e'), + KeyModifiers::empty(), + )); + + assert!(matches!( + diff.event(&event).unwrap(), + EventState::Consumed + )); + + let event = env.queue.pop(); + assert!(matches!( + event, + Some(InternalEvent::OpenExternalEditor(Some(path))) + if path == "src/main.rs" + )); + } } diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index f3fec043..1e15ec08 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -30,7 +30,10 @@ use ratatui::{ Frame, }; use std::{borrow::Cow, fmt::Write}; -use std::{collections::BTreeSet, path::Path}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; use unicode_truncate::UnicodeTruncateStr; use unicode_width::UnicodeWidthStr; @@ -53,11 +56,15 @@ pub struct RevisionFilesComponent { revision: Option, focus: Focus, key_config: SharedKeyConfig, + select_file: Option, } impl RevisionFilesComponent { /// - pub fn new(env: &Environment) -> Self { + pub fn new( + env: &Environment, + select_file: Option, + ) -> Self { Self { queue: env.queue.clone(), tree: FileTree::default(), @@ -72,6 +79,7 @@ impl RevisionFilesComponent { focus: Focus::Tree, key_config: env.key_config.clone(), repo: env.repo.clone(), + select_file, visible: false, } } @@ -134,6 +142,12 @@ impl RevisionFilesComponent { self.tree.collapse_but_root(); self.files = Some(last); + + let select_file = self.select_file.clone(); + self.select_file = None; + if let Some(file) = select_file { + self.find_file(file.as_path()); + } } } else if let Some(rev) = &self.revision { self.request_files(rev.id); diff --git a/src/components/status_tree.rs b/src/components/status_tree.rs index 591152c3..ac4fc9f6 100644 --- a/src/components/status_tree.rs +++ b/src/components/status_tree.rs @@ -58,7 +58,7 @@ impl StatusTreeComponent { } } - pub fn set_commit(&mut self, revision: Option) { + pub const fn set_commit(&mut self, revision: Option) { self.revision = revision; } @@ -92,12 +92,12 @@ impl StatusTreeComponent { } /// - pub fn show_selection(&mut self, show: bool) { + pub const fn show_selection(&mut self, show: bool) { self.show_selection = show; } /// returns true if list is empty - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.tree.is_empty() } @@ -208,7 +208,7 @@ impl StatusTreeComponent { w = width as usize ) } else { - format!(" {indent_str}{collapse_char}{string}",) + format!(" {indent_str}{collapse_char}{string}") }; Some(Span::styled( diff --git a/src/components/textinput.rs b/src/components/textinput.rs index e67d19ea..357fef1d 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -18,9 +18,9 @@ use ratatui::{ widgets::{Clear, Paragraph}, Frame, }; +use ratatui_textarea::{CursorMove, Input, Key, Scrolling, TextArea}; use std::cell::Cell; use std::cell::OnceCell; -use tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea}; /// #[derive(PartialEq, Eq)] @@ -122,17 +122,17 @@ impl TextInputComponent { } /// screen area (last time we got drawn) - pub fn get_area(&self) -> Rect { + pub const fn get_area(&self) -> Rect { self.current_area.get() } /// embed into parent draw area - pub fn embed(&mut self) { + pub const fn embed(&mut self) { self.embed = true; } /// - pub fn enabled(&mut self, enable: bool) { + pub const fn enabled(&mut self, enable: bool) { self.selected = Some(enable); } diff --git a/src/components/utils/filetree.rs b/src/components/utils/filetree.rs index 1329b9be..2a0b37a7 100644 --- a/src/components/utils/filetree.rs +++ b/src/components/utils/filetree.rs @@ -172,7 +172,7 @@ impl FileTreeItems { } /// - pub(crate) fn len(&self) -> usize { + pub(crate) const fn len(&self) -> usize { self.items.len() } diff --git a/src/components/utils/scroll_horizontal.rs b/src/components/utils/scroll_horizontal.rs index 67825744..9ee7e5a3 100644 --- a/src/components/utils/scroll_horizontal.rs +++ b/src/components/utils/scroll_horizontal.rs @@ -18,7 +18,7 @@ impl HorizontalScroll { } } - pub fn get_right(&self) -> usize { + pub const fn get_right(&self) -> usize { self.right.get() } diff --git a/src/components/utils/scroll_vertical.rs b/src/components/utils/scroll_vertical.rs index 51cb05f8..1f7ed779 100644 --- a/src/components/utils/scroll_vertical.rs +++ b/src/components/utils/scroll_vertical.rs @@ -20,7 +20,7 @@ impl VerticalScroll { } } - pub fn get_top(&self) -> usize { + pub const fn get_top(&self) -> usize { self.top.get() } diff --git a/src/components/utils/statustree.rs b/src/components/utils/statustree.rs index 47a1e005..6147e57e 100644 --- a/src/components/utils/statustree.rs +++ b/src/components/utils/statustree.rs @@ -51,17 +51,13 @@ impl StatusTree { let last_selection = self.selected_item().map(|e| e.info.full_path); - let last_selection_index = self.selection.unwrap_or(0); self.tree = FileTreeItems::new(list, &last_collapsed)?; self.selection = last_selection.as_ref().map_or_else( || self.tree.items().first().map(|_| 0), |last_selection| { - self.find_last_selection( - last_selection, - last_selection_index, - ) - .or_else(|| self.tree.items().first().map(|_| 0)) + self.find_last_selection(last_selection) + .or_else(|| self.tree.items().first().map(|_| 0)) }, ); @@ -173,7 +169,7 @@ impl StatusTree { } /// - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.tree.items().is_empty() } @@ -196,19 +192,18 @@ impl StatusTree { fn find_last_selection( &self, last_selection: &str, - last_index: usize, ) -> Option { if self.is_empty() { return None; } - if let Ok(i) = self.tree.items().binary_search_by(|e| { + let res = self.tree.items().binary_search_by(|e| { e.info.full_path.as_str().cmp(last_selection) - }) { - return Some(i); + }); + match res { + Ok(i) => Some(i), + Err(i) => Some(cmp::min(i, self.tree.len() - 1)), } - - Some(cmp::min(last_index, self.tree.len() - 1)) } fn selection_updown( @@ -520,7 +515,7 @@ mod tests { res.update(&string_vec_to_status(&["a", "b"])).unwrap(); res.selection = Some(1); - res.update(&string_vec_to_status(&["d", "c", "a"])).unwrap(); + res.update(&string_vec_to_status(&["a", "c", "d"])).unwrap(); assert_eq!(res.selection, Some(1)); } @@ -545,6 +540,33 @@ mod tests { assert_eq!(res.selection, Some(0)); } + #[test] + fn test_next_when_dir_disappears() { + let mut tree = StatusTree::default(); + tree.update(&string_vec_to_status(&["a/b", "c", "d"])) + .unwrap(); + tree.selection = Some(1); + assert_eq!( + tree.selected_item().unwrap().info.full_path, + "a/b" + ); + + tree.update(&string_vec_to_status(&["c", "d"])).unwrap(); + assert_eq!(tree.selected_item().unwrap().info.full_path, "c"); + } + + #[test] + fn test_next_when_last_dir_disappears() { + let mut tree = StatusTree::default(); + tree.update(&string_vec_to_status(&["a", "b", "c"])) + .unwrap(); + tree.selection = Some(2); + assert_eq!(tree.selected_item().unwrap().info.full_path, "c"); + + tree.update(&string_vec_to_status(&["a", "b"])).unwrap(); + assert_eq!(tree.selected_item().unwrap().info.full_path, "b"); + } + #[test] fn test_keep_collapsed_states() { let mut res = StatusTree::default(); diff --git a/src/gitui.rs b/src/gitui.rs new file mode 100644 index 00000000..03d73b11 --- /dev/null +++ b/src/gitui.rs @@ -0,0 +1,290 @@ +use std::time::Instant; + +use anyhow::Result; +use asyncgit::{sync::utils::repo_work_dir, AsyncGitNotification}; +use crossbeam_channel::{never, tick, unbounded, Receiver}; +use scopetime::scope_time; + +#[cfg(test)] +use crossterm::event::{KeyCode, KeyModifiers}; + +use crate::{ + app::{App, QuitState}, + args::CliArgs, + draw, + input::{Input, InputEvent, InputState}, + keys::KeyConfig, + select_event, + spinner::Spinner, + ui::style::Theme, + watcher::RepoWatcher, + AsyncAppNotification, AsyncNotification, QueueEvent, Updater, + SPINNER_INTERVAL, TICK_INTERVAL, +}; + +pub struct Gitui { + app: crate::app::App, + rx_input: Receiver, + rx_git: Receiver, + rx_app: Receiver, + rx_ticker: Receiver, + rx_watcher: Receiver<()>, +} + +impl Gitui { + pub(crate) fn new( + cliargs: CliArgs, + theme: Theme, + key_config: &KeyConfig, + updater: Updater, + ) -> Result { + let (tx_git, rx_git) = unbounded(); + let (tx_app, rx_app) = unbounded(); + + let input = Input::new(); + + let (rx_ticker, rx_watcher) = match updater { + Updater::NotifyWatcher => { + let repo_watcher = RepoWatcher::new( + repo_work_dir(&cliargs.repo_path)?.as_str(), + ); + + (never(), repo_watcher.receiver()) + } + Updater::Ticker => (tick(TICK_INTERVAL), never()), + }; + + let app = App::new( + cliargs, + tx_git, + tx_app, + input.clone(), + theme, + key_config.clone(), + )?; + + Ok(Self { + app, + rx_input: input.receiver(), + rx_git, + rx_app, + rx_ticker, + rx_watcher, + }) + } + + pub(crate) fn run_main_loop( + &mut self, + terminal: &mut ratatui::Terminal, + ) -> Result + where + ::Error: + 'static + Send + Sync, + { + let spinner_ticker = tick(SPINNER_INTERVAL); + let mut spinner = Spinner::default(); + let mut first_update = true; + + self.app.update()?; + + loop { + let event = if first_update { + first_update = false; + QueueEvent::Notify + } else { + select_event( + &self.rx_input, + &self.rx_git, + &self.rx_app, + &self.rx_ticker, + &self.rx_watcher, + &spinner_ticker, + )? + }; + + { + if matches!(event, QueueEvent::SpinnerUpdate) { + spinner.update(); + spinner.draw(terminal)?; + continue; + } + + scope_time!("loop"); + + match event { + QueueEvent::InputEvent(ev) => { + if matches!( + ev, + InputEvent::State(InputState::Polling) + ) { + //Note: external ed closed, we need to re-hide cursor + terminal.hide_cursor()?; + } + self.app.event(ev)?; + } + QueueEvent::Tick | QueueEvent::Notify => { + self.app.update()?; + } + QueueEvent::AsyncEvent(ev) => { + if !matches!( + ev, + AsyncNotification::Git( + AsyncGitNotification::FinishUnchanged + ) + ) { + self.app.update_async(ev)?; + } + } + QueueEvent::SpinnerUpdate => unreachable!(), + } + + self.draw(terminal)?; + + spinner.set_state(self.app.any_work_pending()); + spinner.draw(terminal)?; + + if self.app.is_quit() { + break; + } + } + } + + Ok(self.app.quit_state()) + } + + fn draw( + &self, + terminal: &mut ratatui::Terminal, + ) -> Result<(), B::Error> { + draw(terminal, &self.app) + } + + #[cfg(test)] + fn update_async(&mut self, event: crate::AsyncNotification) { + self.app.update_async(event).unwrap(); + } + + #[cfg(test)] + fn input_event( + &mut self, + code: KeyCode, + modifiers: KeyModifiers, + ) { + let event = crossterm::event::KeyEvent::new(code, modifiers); + self.app + .event(crate::input::InputEvent::Input( + crossterm::event::Event::Key(event), + )) + .unwrap(); + } + + #[cfg(test)] + fn wait_for_async_git_notification( + &self, + expected: AsyncGitNotification, + ) { + loop { + let actual = self + .rx_git + .recv_timeout(std::time::Duration::from_millis(100)) + .unwrap(); + + if actual == expected { + break; + } + } + } + + #[cfg(test)] + fn update(&mut self) { + self.app.update().unwrap(); + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use asyncgit::{sync::RepoPath, AsyncGitNotification}; + use crossterm::event::{KeyCode, KeyModifiers}; + use git2_testing::repo_init_suffix; + use insta::assert_snapshot; + use ratatui::{backend::TestBackend, Terminal}; + + use crate::{ + args::CliArgs, gitui::Gitui, keys::KeyConfig, + ui::style::Theme, AsyncNotification, Updater, + }; + + // Macro adapted from: https://insta.rs/docs/cmd/ + macro_rules! apply_common_filters { + {} => { + let mut settings = insta::Settings::clone_current(); + // Windows and MacOS + // We don't match on the full path, but on the suffix we pass to `repo_init_suffix` below. + settings.add_filter(r" *\[…\]\S+-insta/?", "[TEMP_FILE]"); + // Linux Temp Folder + settings.add_filter(r" */tmp/\.tmp\S+-insta/", "[TEMP_FILE]"); + // Commit ids that follow a vertical bar + settings.add_filter(r"│[a-z0-9]{7} ", "│[AAAAA] "); + let _bound = settings.bind_to_scope(); + } + } + + #[test] + fn gitui_starts() { + apply_common_filters!(); + + let (temp_dir, _repo) = repo_init_suffix(Some("-insta")); + let path: RepoPath = temp_dir.path().to_str().unwrap().into(); + let cliargs = CliArgs { + theme: PathBuf::from("theme.ron"), + select_file: None, + repo_path: path, + notify_watcher: false, + key_bindings_path: None, + key_symbols_path: None, + }; + + let theme = Theme::init(&PathBuf::new()); + let key_config = KeyConfig::default(); + + let mut gitui = + Gitui::new(cliargs, theme, &key_config, Updater::Ticker) + .unwrap(); + + let mut terminal = + Terminal::new(TestBackend::new(90, 12)).unwrap(); + + gitui.draw(&mut terminal).unwrap(); + + assert_snapshot!("app_loading", terminal.backend()); + + let event = + AsyncNotification::Git(AsyncGitNotification::Status); + gitui.update_async(event); + + gitui.draw(&mut terminal).unwrap(); + + assert_snapshot!("app_loading_finished", terminal.backend()); + + gitui.input_event(KeyCode::Char('2'), KeyModifiers::empty()); + gitui.input_event( + key_config.keys.tab_log.code, + key_config.keys.tab_log.modifiers, + ); + + gitui.wait_for_async_git_notification( + AsyncGitNotification::Log, + ); + + gitui.update(); + + gitui.draw(&mut terminal).unwrap(); + + assert_snapshot!( + "app_log_tab_showing_one_commit", + terminal.backend() + ); + } +} diff --git a/src/input.rs b/src/input.rs index 701b8b5a..cfcb8ee8 100644 --- a/src/input.rs +++ b/src/input.rs @@ -12,7 +12,7 @@ use std::{ }; static FAST_POLL_DURATION: Duration = Duration::from_millis(100); -static SLOW_POLL_DURATION: Duration = Duration::from_millis(10000); +static SLOW_POLL_DURATION: Duration = Duration::from_secs(10); /// #[derive(Clone, Copy, Debug)] diff --git a/src/keys/key_config.rs b/src/keys/key_config.rs index 9cd4eb73..45a9ffdf 100644 --- a/src/keys/key_config.rs +++ b/src/keys/key_config.rs @@ -34,9 +34,21 @@ impl KeyConfig { .map_or_else(|_| Ok(symbols_file), Ok) } - pub fn init() -> Result { - let keys = KeysList::init(Self::get_config_file()?); - let symbols = KeySymbols::init(Self::get_symbols_file()?); + pub fn init( + key_bindings_path: Option<&PathBuf>, + key_symbols_path: Option<&PathBuf>, + ) -> Result { + let keys = KeysList::init( + key_bindings_path + .unwrap_or(&Self::get_config_file()?) + .clone(), + ); + let symbols = KeySymbols::init( + key_symbols_path + .unwrap_or(&Self::get_symbols_file()?) + .clone(), + ); + Ok(Self { keys, symbols }) } @@ -185,7 +197,7 @@ mod tests { // testing let result = std::panic::catch_unwind(|| { - let loaded_config = KeyConfig::init().unwrap(); + let loaded_config = KeyConfig::init(None, None).unwrap(); assert_eq!( loaded_config.keys.move_down, KeysList::default().move_down @@ -200,7 +212,7 @@ mod tests { &original_key_symbols_path, ) .unwrap(); - let loaded_config = KeyConfig::init().unwrap(); + let loaded_config = KeyConfig::init(None, None).unwrap(); assert_eq!( loaded_config.keys.move_down, KeysList::default().move_down @@ -212,7 +224,7 @@ mod tests { &original_key_list_path, ) .unwrap(); - let loaded_config = KeyConfig::init().unwrap(); + let loaded_config = KeyConfig::init(None, None).unwrap(); assert_eq!( loaded_config.keys.move_down, GituiKeyEvent::new( @@ -223,7 +235,7 @@ mod tests { assert_eq!(loaded_config.symbols.esc, "Esc"); fs::remove_file(&original_key_symbols_path).unwrap(); - let loaded_config = KeyConfig::init().unwrap(); + let loaded_config = KeyConfig::init(None, None).unwrap(); assert_eq!( loaded_config.keys.move_down, GituiKeyEvent::new( diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 0f2909a2..24a9507a 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -128,6 +128,7 @@ pub struct KeysList { pub commit_history_next: GituiKeyEvent, pub commit: GituiKeyEvent, pub newline: GituiKeyEvent, + pub goto_line: GituiKeyEvent, } #[rustfmt::skip] @@ -225,6 +226,7 @@ impl Default for KeysList { commit_history_next: GituiKeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL), commit: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), newline: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), + goto_line: GituiKeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT), } } } diff --git a/src/main.rs b/src/main.rs index 72165b08..fd662950 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,6 +65,7 @@ mod bug_report; mod clipboard; mod cmdbar; mod components; +mod gitui; mod input; mod keys; mod notify_mutex; @@ -79,15 +80,15 @@ mod tabs; mod ui; mod watcher; -use crate::{app::App, args::process_cmdline}; +use crate::{ + app::App, + args::{process_cmdline, CliArgs}, +}; use anyhow::{anyhow, bail, Result}; use app::QuitState; -use asyncgit::{ - sync::{utils::repo_work_dir, RepoPath}, - AsyncGitNotification, -}; +use asyncgit::{sync::RepoPath, AsyncGitNotification}; use backtrace::Backtrace; -use crossbeam_channel::{never, tick, unbounded, Receiver, Select}; +use crossbeam_channel::{Receiver, Select}; use crossterm::{ terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, @@ -95,21 +96,18 @@ use crossterm::{ }, ExecutableCommand, }; -use input::{Input, InputEvent, InputState}; +use gitui::Gitui; +use input::InputEvent; use keys::KeyConfig; use ratatui::backend::CrosstermBackend; use scopeguard::defer; -use scopetime::scope_time; -use spinner::Spinner; use std::{ - cell::RefCell, io::{self, Stdout}, panic, path::Path, time::{Duration, Instant}, }; use ui::style::Theme; -use watcher::RepoWatcher; type Terminal = ratatui::Terminal>; @@ -168,9 +166,12 @@ fn main() -> Result<()> { asyncgit::register_tracing_logging(); ensure_valid_path(&cliargs.repo_path)?; - let key_config = KeyConfig::init() - .map_err(|e| log_eprintln!("KeyConfig loading error: {e}")) - .unwrap_or_default(); + let key_config = KeyConfig::init( + cliargs.key_bindings_path.as_ref(), + cliargs.key_symbols_path.as_ref(), + ) + .map_err(|e| log_eprintln!("KeyConfig loading error: {e}")) + .unwrap_or_default(); let theme = Theme::init(&cliargs.theme); setup_terminal()?; @@ -180,9 +181,8 @@ fn main() -> Result<()> { set_panic_handler()?; - let mut repo_path = cliargs.repo_path; - let mut terminal = start_terminal(io::stdout(), &repo_path)?; - let input = Input::new(); + let mut terminal = + start_terminal(io::stdout(), &cliargs.repo_path)?; let updater = if cliargs.notify_watcher { Updater::NotifyWatcher @@ -190,20 +190,28 @@ fn main() -> Result<()> { Updater::Ticker }; + let mut args = cliargs; + loop { let quit_state = run_app( app_start, - repo_path.clone(), + args.clone(), theme.clone(), - key_config.clone(), - &input, + &key_config, updater, &mut terminal, )?; match quit_state { QuitState::OpenSubmodule(p) => { - repo_path = p; + args = CliArgs { + repo_path: p, + select_file: None, + theme: args.theme, + notify_watcher: args.notify_watcher, + key_bindings_path: args.key_bindings_path, + key_symbols_path: args.key_symbols_path, + } } _ => break, } @@ -214,107 +222,17 @@ fn main() -> Result<()> { fn run_app( app_start: Instant, - repo: RepoPath, + cliargs: CliArgs, theme: Theme, - key_config: KeyConfig, - input: &Input, + key_config: &KeyConfig, updater: Updater, terminal: &mut Terminal, ) -> Result { - let (tx_git, rx_git) = unbounded(); - let (tx_app, rx_app) = unbounded(); - - let rx_input = input.receiver(); - - let (rx_ticker, rx_watcher) = match updater { - Updater::NotifyWatcher => { - let repo_watcher = - RepoWatcher::new(repo_work_dir(&repo)?.as_str()); - - (never(), repo_watcher.receiver()) - } - Updater::Ticker => (tick(TICK_INTERVAL), never()), - }; - - let spinner_ticker = tick(SPINNER_INTERVAL); - - let mut app = App::new( - RefCell::new(repo), - tx_git, - tx_app, - input.clone(), - theme, - key_config, - )?; - - let mut spinner = Spinner::default(); - let mut first_update = true; + let mut gitui = Gitui::new(cliargs, theme, key_config, updater)?; log::trace!("app start: {} ms", app_start.elapsed().as_millis()); - loop { - let event = if first_update { - first_update = false; - QueueEvent::Notify - } else { - select_event( - &rx_input, - &rx_git, - &rx_app, - &rx_ticker, - &rx_watcher, - &spinner_ticker, - )? - }; - - { - if matches!(event, QueueEvent::SpinnerUpdate) { - spinner.update(); - spinner.draw(terminal)?; - continue; - } - - scope_time!("loop"); - - match event { - QueueEvent::InputEvent(ev) => { - if matches!( - ev, - InputEvent::State(InputState::Polling) - ) { - //Note: external ed closed, we need to re-hide cursor - terminal.hide_cursor()?; - } - app.event(ev)?; - } - QueueEvent::Tick | QueueEvent::Notify => { - app.update()?; - } - QueueEvent::AsyncEvent(ev) => { - if !matches!( - ev, - AsyncNotification::Git( - AsyncGitNotification::FinishUnchanged - ) - ) { - app.update_async(ev)?; - } - } - QueueEvent::SpinnerUpdate => unreachable!(), - } - - draw(terminal, &app)?; - - spinner.set_state(app.any_work_pending()); - spinner.draw(terminal)?; - - if app.is_quit() { - break; - } - } - } - - Ok(app.quit_state()) + gitui.run_main_loop(terminal) } fn setup_terminal() -> Result<()> { @@ -338,7 +256,10 @@ fn shutdown_terminal() { } } -fn draw(terminal: &mut Terminal, app: &App) -> io::Result<()> { +fn draw( + terminal: &mut ratatui::Terminal, + app: &App, +) -> Result<(), B::Error> { if app.requires_redraw() { terminal.clear()?; } diff --git a/src/options.rs b/src/options.rs index 84063e69..a80e5cb8 100644 --- a/src/options.rs +++ b/src/options.rs @@ -116,7 +116,7 @@ impl Options { self.save(); } - pub fn has_commit_msg_history(&self) -> bool { + pub const fn has_commit_msg_history(&self) -> bool { !self.data.commit_msgs.is_empty() } diff --git a/src/popups/blame_file.rs b/src/popups/blame_file.rs index f7316806..b7d1304b 100644 --- a/src/popups/blame_file.rs +++ b/src/popups/blame_file.rs @@ -234,6 +234,16 @@ impl Component for BlameFilePopup { ) .order(1), ); + out.push( + CommandInfo::new( + strings::commands::open_line_number_popup( + &self.key_config, + ), + true, + has_result, + ) + .order(1), + ); } visibility_blocking(self) @@ -307,6 +317,22 @@ impl Component for BlameFilePopup { ), )); } + } else if key_match( + key, + self.key_config.keys.goto_line, + ) { + let maybe_blame_result = &self + .blame + .as_ref() + .and_then(|blame| blame.result()); + if let Some(blame_result) = maybe_blame_result { + let max_line = blame_result.lines().len() - 1; + self.queue.push( + InternalEvent::OpenGotoLinePopup( + max_line, + ), + ); + } } return Ok(EventState::Consumed); @@ -742,6 +768,14 @@ impl BlameFilePopup { }) } + pub fn goto_line(&mut self, line: usize) { + self.visible = true; + let mut table_state = self.table_state.take(); + table_state + .select(Some(line.clamp(0, self.get_max_line_number()))); + self.table_state.set(table_state); + } + fn selected_commit(&self) -> Option { self.blame .as_ref() diff --git a/src/popups/branchlist.rs b/src/popups/branchlist.rs index 1478cdd9..fa66ffff 100644 --- a/src/popups/branchlist.rs +++ b/src/popups/branchlist.rs @@ -20,8 +20,9 @@ use asyncgit::{ checkout_remote_branch, BranchDetails, LocalBranch, RemoteBranch, }, - checkout_branch, get_branches_info, BranchInfo, BranchType, - CommitId, RepoPathRef, RepoState, + checkout_branch, get_branches_info, + status::StatusType, + BranchInfo, BranchType, CommitId, RepoPathRef, RepoState, }, AsyncGitNotification, }; @@ -306,8 +307,7 @@ impl BranchListPopup { if self.visible { self.has_remotes = get_branches_info(&self.repo.borrow(), false) - .map(|branches| !branches.is_empty()) - .unwrap_or(false); + .is_ok_and(|branches| !branches.is_empty()); } } @@ -341,7 +341,7 @@ impl BranchListPopup { Ok(()) } - fn valid_selection(&self) -> bool { + const fn valid_selection(&self) -> bool { !self.branches.is_empty() } @@ -582,22 +582,35 @@ impl BranchListPopup { anyhow::bail!("no valid branch selected"); } - if self.local { - checkout_branch( - &self.repo.borrow(), - &self.branches[self.selection as usize].name, - )?; - self.hide(); - } else { - checkout_remote_branch( - &self.repo.borrow(), - &self.branches[self.selection as usize], - )?; - self.local = true; - self.update_branches()?; - } + let status = sync::status::get_status( + &self.repo.borrow(), + StatusType::WorkingDir, + None, + ) + .expect("Could not get status"); - self.queue.push(InternalEvent::Update(NeedsUpdate::ALL)); + let selected_branch = &self.branches[self.selection as usize]; + if status.is_empty() { + if self.local { + checkout_branch( + &self.repo.borrow(), + &selected_branch.name, + )?; + self.hide(); + } else { + checkout_remote_branch( + &self.repo.borrow(), + selected_branch, + )?; + self.local = true; + self.update_branches()?; + } + self.queue.push(InternalEvent::Update(NeedsUpdate::ALL)); + } else { + self.queue.push(InternalEvent::CheckoutOption( + selected_branch.clone(), + )); + } Ok(()) } diff --git a/src/popups/checkout_option.rs b/src/popups/checkout_option.rs new file mode 100644 index 00000000..c2abdd0a --- /dev/null +++ b/src/popups/checkout_option.rs @@ -0,0 +1,239 @@ +use crate::components::{ + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, +}; +use crate::queue::{InternalEvent, NeedsUpdate}; +use crate::strings::CheckoutOptions; +use crate::try_or_popup; +use crate::{ + app::Environment, + keys::{key_match, SharedKeyConfig}, + queue::Queue, + strings, + ui::{self, style::SharedTheme}, +}; +use anyhow::{Ok, Result}; +use asyncgit::sync::branch::checkout_remote_branch; +use asyncgit::sync::status::discard_status; +use asyncgit::sync::{checkout_branch, BranchInfo, RepoPath}; +use crossterm::event::Event; +use ratatui::{ + layout::{Alignment, Rect}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, Paragraph}, + Frame, +}; + +pub struct CheckoutOptionPopup { + queue: Queue, + repo: RepoPath, + branch: Option, + option: CheckoutOptions, + visible: bool, + key_config: SharedKeyConfig, + theme: SharedTheme, +} + +impl CheckoutOptionPopup { + /// + pub fn new(env: &Environment) -> Self { + Self { + queue: env.queue.clone(), + repo: env.repo.borrow().clone(), + branch: None, + option: CheckoutOptions::KeepLocalChanges, + visible: false, + key_config: env.key_config.clone(), + theme: env.theme.clone(), + } + } + + fn get_text(&self, _width: u16) -> Vec> { + let mut txt: Vec = Vec::with_capacity(10); + + txt.push(Line::from(vec![ + Span::styled( + String::from("Switch to: "), + self.theme.text(true, false), + ), + Span::styled( + self.branch.as_ref().expect("No branch").name.clone(), + self.theme.commit_hash(false), + ), + ])); + + let (kind_name, kind_desc) = self.option.to_string_pair(); + + txt.push(Line::from(vec![ + Span::styled( + String::from("How: "), + self.theme.text(true, false), + ), + Span::styled(kind_name, self.theme.text(true, true)), + Span::styled(kind_desc, self.theme.text(true, false)), + ])); + + txt + } + + /// + pub fn open(&mut self, branch: BranchInfo) -> Result<()> { + self.show()?; + + self.branch = Some(branch); + + Ok(()) + } + + fn checkout(&self) -> Result<()> { + if let Some(branch) = &self.branch { + if branch.is_local() { + checkout_branch(&self.repo, &branch.name)?; + } else { + checkout_remote_branch(&self.repo, branch)?; + } + } + + Ok(()) + } + + fn handle_event(&mut self) -> Result<()> { + match self.option { + CheckoutOptions::KeepLocalChanges => { + self.checkout()?; + } + CheckoutOptions::DiscardAllLocalChagnes => { + discard_status(&self.repo)?; + self.checkout()?; + } + } + + self.queue.push(InternalEvent::Update(NeedsUpdate::ALL)); + self.queue.push(InternalEvent::SelectBranch); + self.hide(); + + Ok(()) + } + + const fn change_kind(&mut self, incr: bool) { + self.option = if incr { + self.option.next() + } else { + self.option.previous() + }; + } +} + +impl DrawableComponent for CheckoutOptionPopup { + fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> { + if self.is_visible() { + const SIZE: (u16, u16) = (55, 4); + let area = + ui::centered_rect_absolute(SIZE.0, SIZE.1, area); + + let width = area.width; + + f.render_widget(Clear, area); + f.render_widget( + Paragraph::new(self.get_text(width)) + .block( + Block::default() + .borders(Borders::ALL) + .title(Span::styled( + "Checkout options", + self.theme.title(true), + )) + .border_style(self.theme.block(true)), + ) + .alignment(Alignment::Left), + area, + ); + } + + Ok(()) + } +} + +impl Component for CheckoutOptionPopup { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + out.push( + CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + true, + ) + .order(1), + ); + + out.push( + CommandInfo::new( + strings::commands::reset_commit(&self.key_config), + true, + true, + ) + .order(1), + ); + + out.push( + CommandInfo::new( + strings::commands::reset_type(&self.key_config), + true, + true, + ) + .order(1), + ); + } + + visibility_blocking(self) + } + + fn event( + &mut self, + event: &crossterm::event::Event, + ) -> Result { + if self.is_visible() { + if let Event::Key(key) = &event { + if key_match(key, self.key_config.keys.exit_popup) { + self.hide(); + } else if key_match( + key, + self.key_config.keys.move_down, + ) { + self.change_kind(true); + } else if key_match(key, self.key_config.keys.move_up) + { + self.change_kind(false); + } else if key_match(key, self.key_config.keys.enter) { + try_or_popup!( + self, + "checkout error:", + self.handle_event() + ); + } + } + + return Ok(EventState::Consumed); + } + + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} diff --git a/src/popups/commit.rs b/src/popups/commit.rs index 146286f8..b5dff767 100644 --- a/src/popups/commit.rs +++ b/src/popups/commit.rs @@ -336,6 +336,7 @@ impl CommitPopup { Ok(()) } + fn signoff_commit(&mut self) { let msg = self.input.get_text(); let signed_msg = self.add_sign_off(msg); @@ -343,7 +344,8 @@ impl CommitPopup { self.input.set_text(signed_msg); } } - fn toggle_verify(&mut self) { + + const fn toggle_verify(&mut self) { self.verify = !self.verify; } @@ -446,7 +448,7 @@ impl CommitPopup { msg_source, &mut msg, )? { - log::error!("prepare-commit-msg hook rejection: {e}",); + log::error!("prepare-commit-msg hook rejection: {e}"); } self.input.set_text(msg); diff --git a/src/popups/create_branch.rs b/src/popups/create_branch.rs index bc830310..9c03970a 100644 --- a/src/popups/create_branch.rs +++ b/src/popups/create_branch.rs @@ -131,7 +131,7 @@ impl CreateBranchPopup { Err(e) => { log::error!("create branch: {e}"); self.queue.push(InternalEvent::ShowErrorMsg( - format!("create branch error:\n{e}",), + format!("create branch error:\n{e}"), )); } } diff --git a/src/popups/create_remote.rs b/src/popups/create_remote.rs index e6a51a92..af1c37eb 100644 --- a/src/popups/create_remote.rs +++ b/src/popups/create_remote.rs @@ -202,7 +202,7 @@ impl CreateRemotePopup { Err(e) => { log::error!("create remote: {e}"); self.queue.push(InternalEvent::ShowErrorMsg( - format!("create remote error:\n{e}",), + format!("create remote error:\n{e}"), )); } } diff --git a/src/popups/goto_line.rs b/src/popups/goto_line.rs new file mode 100644 index 00000000..193e0e77 --- /dev/null +++ b/src/popups/goto_line.rs @@ -0,0 +1,167 @@ +use crate::{ + app::Environment, + components::{ + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, + }, + keys::{key_match, SharedKeyConfig}, + queue::{InternalEvent, Queue}, + strings, + ui::{self, style::SharedTheme}, +}; + +use ratatui::{ + layout::Rect, + style::{Color, Style}, + widgets::{Block, Clear, Paragraph}, + Frame, +}; + +use anyhow::Result; + +use crossterm::event::{Event, KeyCode}; + +pub struct GotoLinePopup { + visible: bool, + input: String, + line_number: usize, + key_config: SharedKeyConfig, + queue: Queue, + theme: SharedTheme, + invalid_input: bool, + max_line: usize, +} + +impl GotoLinePopup { + pub fn new(env: &Environment) -> Self { + Self { + visible: false, + input: String::new(), + key_config: env.key_config.clone(), + queue: env.queue.clone(), + theme: env.theme.clone(), + invalid_input: false, + max_line: 0, + line_number: 0, + } + } + + pub const fn open(&mut self, max_line: usize) { + self.visible = true; + self.max_line = max_line; + } +} + +impl Component for GotoLinePopup { + /// + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + out.push( + CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + true, + ) + .order(1), + ); + out.push( + CommandInfo::new( + strings::commands::goto_line(&self.key_config), + true, + true, + ) + .order(1), + ); + } + + visibility_blocking(self) + } + + fn is_visible(&self) -> bool { + self.visible + } + + /// + fn event(&mut self, event: &Event) -> Result { + if self.is_visible() { + if let Event::Key(key) = event { + if key_match(key, self.key_config.keys.exit_popup) { + self.visible = false; + self.input.clear(); + } else if let KeyCode::Char(c) = key.code { + if c.is_ascii_digit() || c == '-' { + self.input.push(c); + } + } else if key.code == KeyCode::Backspace { + self.input.pop(); + } else if key_match(key, self.key_config.keys.enter) { + self.visible = false; + if self.invalid_input { + self.queue.push(InternalEvent::ShowErrorMsg( + format!("Invalid input: only numbers between -{} and {} (included) are allowed (-1 denotes the last line, -2 denotes the second to last line, and so on)",self.max_line + 1, self.max_line)) + , + ); + } else if !self.input.is_empty() { + self.queue.push(InternalEvent::GotoLine( + self.line_number, + )); + } + self.input.clear(); + self.invalid_input = false; + } + } + match self.input.parse::() { + Ok(input) => { + let mut max_value_allowed_abs = self.max_line; + // negative indices are 1 based + if input < 0 { + max_value_allowed_abs += 1; + } + let input_abs = input.unsigned_abs(); + if input_abs > max_value_allowed_abs { + self.invalid_input = true; + } else { + self.invalid_input = false; + self.line_number = if input >= 0 { + input_abs + } else { + max_value_allowed_abs - input_abs + } + } + } + Err(_) => { + if !self.input.is_empty() { + self.invalid_input = true; + } + } + } + return Ok(EventState::Consumed); + } + Ok(EventState::NotConsumed) + } +} + +impl DrawableComponent for GotoLinePopup { + fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> { + if self.is_visible() { + let style = if self.invalid_input { + Style::default().fg(Color::Red) + } else { + self.theme.text(true, false) + }; + let input = Paragraph::new(self.input.as_str()) + .style(style) + .block(Block::bordered().title("Go to")); + + let input_area = ui::centered_rect_absolute(15, 3, area); + f.render_widget(Clear, input_area); + f.render_widget(input, input_area); + } + + Ok(()) + } +} diff --git a/src/popups/log_search.rs b/src/popups/log_search.rs index 3cd6ad86..6f4602cd 100644 --- a/src/popups/log_search.rs +++ b/src/popups/log_search.rs @@ -225,7 +225,7 @@ impl LogSearchPopupPopup { ), )]), Line::from(vec![Span::styled( - format!("[{x_summary}] summary",), + format!("[{x_summary}] summary"), self.theme.text( matches!( self.selection, @@ -235,7 +235,7 @@ impl LogSearchPopupPopup { ), )]), Line::from(vec![Span::styled( - format!("[{x_body}] message body",), + format!("[{x_body}] message body"), self.theme.text( matches!( self.selection, @@ -245,7 +245,7 @@ impl LogSearchPopupPopup { ), )]), Line::from(vec![Span::styled( - format!("[{x_files}] committed files",), + format!("[{x_files}] committed files"), self.theme.text( matches!( self.selection, @@ -255,7 +255,7 @@ impl LogSearchPopupPopup { ), )]), Line::from(vec![Span::styled( - format!("[{x_authors}] authors",), + format!("[{x_authors}] authors"), self.theme.text( matches!( self.selection, @@ -315,7 +315,7 @@ impl LogSearchPopupPopup { } } - fn move_selection(&mut self, arg: bool) { + const fn move_selection(&mut self, arg: bool) { if arg { //up self.selection = match self.selection { diff --git a/src/popups/mod.rs b/src/popups/mod.rs index cb3ae1af..ebb9e148 100644 --- a/src/popups/mod.rs +++ b/src/popups/mod.rs @@ -1,5 +1,6 @@ mod blame_file; mod branchlist; +mod checkout_option; mod commit; mod compare_commits; mod confirm; @@ -9,6 +10,7 @@ mod externaleditor; mod fetch; mod file_revlog; mod fuzzy_find; +mod goto_line; mod help; mod inspect_commit; mod log_search; @@ -30,6 +32,7 @@ mod update_remote_url; pub use blame_file::{BlameFileOpen, BlameFilePopup}; pub use branchlist::BranchListPopup; +pub use checkout_option::CheckoutOptionPopup; pub use commit::CommitPopup; pub use compare_commits::CompareCommitsPopup; pub use confirm::ConfirmPopup; @@ -39,6 +42,7 @@ pub use externaleditor::ExternalEditorPopup; pub use fetch::FetchPopup; pub use file_revlog::{FileRevOpen, FileRevlogPopup}; pub use fuzzy_find::FuzzyFindPopup; +pub use goto_line::GotoLinePopup; pub use help::HelpPopup; pub use inspect_commit::{InspectCommitOpen, InspectCommitPopup}; pub use log_search::LogSearchPopupPopup; diff --git a/src/popups/options.rs b/src/popups/options.rs index 4e194cb5..b365c648 100644 --- a/src/popups/options.rs +++ b/src/popups/options.rs @@ -134,7 +134,7 @@ impl OptionsPopup { ])); } - fn move_selection(&mut self, up: bool) { + const fn move_selection(&mut self, up: bool) { if up { self.selection = match self.selection { AppOption::StatusShowUntracked => { diff --git a/src/popups/push.rs b/src/popups/push.rs index f900ce86..c0afb0ef 100644 --- a/src/popups/push.rs +++ b/src/popups/push.rs @@ -145,9 +145,16 @@ impl PushPopup { }; // run pre push hook - can reject push - if let HookResult::NotOk(e) = - hooks_pre_push(&self.repo.borrow())? - { + let repo = self.repo.borrow(); + if let HookResult::NotOk(e) = hooks_pre_push( + &repo, + &remote, + &asyncgit::sync::PrePushTarget::Branch { + branch: &self.branch, + delete: self.modifier.delete(), + }, + cred.clone(), + )? { log::error!("pre-push hook failed: {e}"); self.queue.push(InternalEvent::ShowErrorMsg(format!( "pre-push hook failed:\n{e}" diff --git a/src/popups/push_tags.rs b/src/popups/push_tags.rs index 30df245b..aa911388 100644 --- a/src/popups/push_tags.rs +++ b/src/popups/push_tags.rs @@ -84,10 +84,15 @@ impl PushTagsPopup { &mut self, cred: Option, ) -> Result<()> { - // run pre push hook - can reject push - if let HookResult::NotOk(e) = - hooks_pre_push(&self.repo.borrow())? - { + let remote = get_default_remote(&self.repo.borrow())?; + + let repo = self.repo.borrow(); + if let HookResult::NotOk(e) = hooks_pre_push( + &repo, + &remote, + &asyncgit::sync::PrePushTarget::Tags, + cred.clone(), + )? { log::error!("pre-push hook failed: {e}"); self.queue.push(InternalEvent::ShowErrorMsg(format!( "pre-push hook failed:\n{e}" @@ -100,7 +105,7 @@ impl PushTagsPopup { self.pending = true; self.progress = None; self.git_push.request(PushTagsRequest { - remote: get_default_remote(&self.repo.borrow())?, + remote, basic_credential: cred, })?; Ok(()) diff --git a/src/popups/remotelist.rs b/src/popups/remotelist.rs index 0630e35f..c09986ae 100644 --- a/src/popups/remotelist.rs +++ b/src/popups/remotelist.rs @@ -146,12 +146,14 @@ impl Component for RemoteListPopup { } else if key_match( e, self.key_config.keys.update_remote_name, - ) { + ) && self.valid_selection() + { self.rename_remote(); } else if key_match( e, self.key_config.keys.update_remote_url, - ) { + ) && self.valid_selection() + { self.update_remote_url(); } } @@ -411,7 +413,7 @@ impl RemoteListPopup { Ok(true) } - fn valid_selection(&self) -> bool { + const fn valid_selection(&self) -> bool { !self.remote_names.is_empty() && self.remote_names.len() >= self.selection as usize } diff --git a/src/popups/rename_branch.rs b/src/popups/rename_branch.rs index 05b4ea93..85190673 100644 --- a/src/popups/rename_branch.rs +++ b/src/popups/rename_branch.rs @@ -140,7 +140,7 @@ impl RenameBranchPopup { Err(e) => { log::error!("create branch: {e}"); self.queue.push(InternalEvent::ShowErrorMsg( - format!("rename branch error:\n{e}",), + format!("rename branch error:\n{e}"), )); } } diff --git a/src/popups/rename_remote.rs b/src/popups/rename_remote.rs index a5dc914f..52dafa3c 100644 --- a/src/popups/rename_remote.rs +++ b/src/popups/rename_remote.rs @@ -163,7 +163,7 @@ impl RenameRemotePopup { Err(e) => { log::error!("rename remote: {e}"); self.queue.push(InternalEvent::ShowErrorMsg( - format!("rename remote error:\n{e}",), + format!("rename remote error:\n{e}"), )); } } diff --git a/src/popups/reset.rs b/src/popups/reset.rs index e3c9865d..d40d2a6f 100644 --- a/src/popups/reset.rs +++ b/src/popups/reset.rs @@ -120,7 +120,7 @@ impl ResetPopup { /// #[allow(clippy::unnecessary_wraps)] pub fn update(&mut self) -> Result<()> { - self.git_branch_name.lookup().map(Some).unwrap_or(None); + self.git_branch_name.lookup().ok(); Ok(()) } @@ -137,7 +137,7 @@ impl ResetPopup { self.hide(); } - fn change_kind(&mut self, incr: bool) { + const fn change_kind(&mut self, incr: bool) { self.kind = if incr { match self.kind { ResetType::Soft => ResetType::Mixed, diff --git a/src/popups/revision_files.rs b/src/popups/revision_files.rs index 9fbe9e25..51ee94cf 100644 --- a/src/popups/revision_files.rs +++ b/src/popups/revision_files.rs @@ -38,7 +38,7 @@ impl RevisionFilesPopup { /// pub fn new(env: &Environment) -> Self { Self { - files: RevisionFilesComponent::new(env), + files: RevisionFilesComponent::new(env, None), visible: false, key_config: env.key_config.clone(), open_request: None, diff --git a/src/popups/stashmsg.rs b/src/popups/stashmsg.rs index 8c7a7a07..f7421064 100644 --- a/src/popups/stashmsg.rs +++ b/src/popups/stashmsg.rs @@ -138,7 +138,7 @@ impl StashMsgPopup { } /// - pub fn options(&mut self, options: StashingOptions) { + pub const fn options(&mut self, options: StashingOptions) { self.options = options; } } diff --git a/src/popups/submodules.rs b/src/popups/submodules.rs index 8fe3c23e..f40fe0ba 100644 --- a/src/popups/submodules.rs +++ b/src/popups/submodules.rs @@ -317,8 +317,8 @@ impl SubmodulesListPopup { } fn set_selection(&mut self, selection: u16) -> Result<()> { - let num_entriess: u16 = self.submodules.len().try_into()?; - let num_entries = num_entriess.saturating_sub(1); + let num_entries: u16 = self.submodules.len().try_into()?; + let num_entries = num_entries.saturating_sub(1); let selection = if selection > num_entries { num_entries diff --git a/src/popups/tag_commit.rs b/src/popups/tag_commit.rs index b7343d92..b39ffa8a 100644 --- a/src/popups/tag_commit.rs +++ b/src/popups/tag_commit.rs @@ -196,7 +196,7 @@ impl TagCommitPopup { log::error!("e: {e}"); self.queue.push(InternalEvent::ShowErrorMsg( - format!("tag error:\n{e}",), + format!("tag error:\n{e}"), )); } } diff --git a/src/popups/taglist.rs b/src/popups/taglist.rs index 8b7ed2c5..959251b2 100644 --- a/src/popups/taglist.rs +++ b/src/popups/taglist.rs @@ -312,8 +312,7 @@ impl TagListPopup { self.has_remotes = sync::get_branches_info(&self.repo.borrow(), false) - .map(|branches| !branches.is_empty()) - .unwrap_or(false); + .is_ok_and(|branches| !branches.is_empty()); let basic_credential = if self.has_remotes { if need_username_password(&self.repo.borrow())? { diff --git a/src/popups/update_remote_url.rs b/src/popups/update_remote_url.rs index 5a7f2aa8..c33c052e 100644 --- a/src/popups/update_remote_url.rs +++ b/src/popups/update_remote_url.rs @@ -140,7 +140,7 @@ impl UpdateRemoteUrlPopup { Err(e) => { log::error!("update remote url: {e}"); self.queue.push(InternalEvent::ShowErrorMsg( - format!("update remote url error:\n{e}",), + format!("update remote url error:\n{e}"), )); } } diff --git a/src/queue.rs b/src/queue.rs index 635fbc9e..5cdfe3ce 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -8,7 +8,8 @@ use crate::{ }; use asyncgit::{ sync::{ - diff::DiffLinePosition, CommitId, LogFilterSearchOptions, + diff::DiffLinePosition, BranchInfo, CommitId, + LogFilterSearchOptions, }, PushType, }; @@ -157,6 +158,12 @@ pub enum InternalEvent { RewordCommit(CommitId), /// CommitSearch(LogFilterSearchOptions), + /// + OpenGotoLinePopup(usize), + /// + GotoLine(usize), + /// + CheckoutOption(BranchInfo), } /// single threaded simple queue for components to communicate with each other diff --git a/src/snapshots/gitui__gitui__tests__app_loading.snap b/src/snapshots/gitui__gitui__tests__app_loading.snap new file mode 100644 index 00000000..6a8025c3 --- /dev/null +++ b/src/snapshots/gitui__gitui__tests__app_loading.snap @@ -0,0 +1,17 @@ +--- +source: src/gitui.rs +expression: terminal.backend() +snapshot_kind: text +--- +" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " +" ──────────────────────────────────────────────────────────────────────────────────────── " +"┌Unstaged Changes───────────────────────────┐┌Diff: ─────────────────────────────────────┐" +"│Loading ... ││ │" +"│ ││ │" +"│ ││ │" +"└───────────────────────────────────{master}┘│ │" +"┌Staged Changes─────────────────────────────┐│ │" +"│Loading ... ││ │" +"│ ││ │" +"└───────────────────────────────────────────┘└───────────────────────────────────────────┘" +" " diff --git a/src/snapshots/gitui__gitui__tests__app_loading_finished.snap b/src/snapshots/gitui__gitui__tests__app_loading_finished.snap new file mode 100644 index 00000000..97229001 --- /dev/null +++ b/src/snapshots/gitui__gitui__tests__app_loading_finished.snap @@ -0,0 +1,17 @@ +--- +source: src/gitui.rs +expression: terminal.backend() +snapshot_kind: text +--- +" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " +" ──────────────────────────────────────────────────────────────────────────────────────── " +"┌Unstaged Changes───────────────────────────┐┌Diff: ─────────────────────────────────────┐" +"│ ││ │" +"│ ││ │" +"│ ││ │" +"└───────────────────────────────────{master}┘│ │" +"┌Staged Changes─────────────────────────────┐│ │" +"│ ││ │" +"│ ││ │" +"└───────────────────────────────────────────┘└───────────────────────────────────────────┘" +"Branches [b] Push [p] Fetch [⇧F] Pull [f] Undo Commit [⇧U] Submodules [⇧S] more [.]" diff --git a/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap b/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap new file mode 100644 index 00000000..bbdd5be8 --- /dev/null +++ b/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap @@ -0,0 +1,17 @@ +--- +source: src/gitui.rs +expression: terminal.backend() +snapshot_kind: text +--- +" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " +" ──────────────────────────────────────────────────────────────────────────────────────── " +"┌Commit 1/1──────────────────────────────────────────────────────────────────────────────┐" +"│[AAAAA] <1m ago name initial █" +"│ ║" +"│ ║" +"│ ║" +"│ ║" +"│ ║" +"│ ║" +"└────────────────────────────────────────────────────────────────────────────────────────┘" +"Scroll [↑↓] Mark [˽] Details [⏎] Branches [b] Compare [⇧C] Copy Hash [y] Tag [t] more [.]" diff --git a/src/spinner.rs b/src/spinner.rs index 2fc6b3a2..56f0c135 100644 --- a/src/spinner.rs +++ b/src/spinner.rs @@ -1,8 +1,5 @@ -use ratatui::{ - backend::{Backend, CrosstermBackend}, - Terminal, -}; -use std::{cell::Cell, char, io}; +use ratatui::{backend::Backend, Terminal}; +use std::{cell::Cell, char}; // static SPINNER_CHARS: &[char] = &['◢', '◣', '◤', '◥']; // static SPINNER_CHARS: &[char] = &['⢹', '⢺', '⢼', '⣸', '⣇', '⡧', '⡗', '⡏']; @@ -34,15 +31,15 @@ impl Spinner { } /// - pub fn set_state(&mut self, active: bool) { + pub const fn set_state(&mut self, active: bool) { self.active = active; } /// draws or removes spinner char depending on `pending` state - pub fn draw( + pub fn draw( &self, - terminal: &mut Terminal>, - ) -> io::Result<()> { + terminal: &mut Terminal, + ) -> Result<(), B::Error> { let idx = self.idx; let char_to_draw = diff --git a/src/strings.rs b/src/strings.rs index 0b2d25ef..f66a9e93 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -438,6 +438,46 @@ pub fn ellipsis_trim_start(s: &str, width: usize) -> Cow<'_, str> { } } +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum CheckoutOptions { + KeepLocalChanges, + DiscardAllLocalChagnes, +} + +impl CheckoutOptions { + pub const fn previous(self) -> Self { + match self { + Self::KeepLocalChanges => Self::DiscardAllLocalChagnes, + Self::DiscardAllLocalChagnes => Self::KeepLocalChanges, + } + } + + pub const fn next(self) -> Self { + match self { + Self::KeepLocalChanges => Self::DiscardAllLocalChagnes, + Self::DiscardAllLocalChagnes => Self::KeepLocalChanges, + } + } + + pub const fn to_string_pair( + self, + ) -> (&'static str, &'static str) { + const CHECKOUT_OPTION_UNCHANGE: &str = + " 🟡 Keep local changes"; + const CHECKOUT_OPTION_DISCARD: &str = + " 🔴 Discard all local changes"; + + match self { + Self::KeepLocalChanges => { + ("Don't change", CHECKOUT_OPTION_UNCHANGE) + } + Self::DiscardAllLocalChagnes => { + ("Discard", CHECKOUT_OPTION_DISCARD) + } + } + } +} + pub mod commit { use crate::keys::SharedKeyConfig; @@ -1449,6 +1489,18 @@ pub mod commands { CMD_GROUP_LOG, ) } + pub fn open_line_number_popup( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Go to [{}]", + key_config.get_hint(key_config.keys.goto_line), + ), + "go to a given line number in the blame view", + CMD_GROUP_GENERAL, + ) + } pub fn log_tag_commit( key_config: &SharedKeyConfig, ) -> CommandText { @@ -1870,4 +1922,15 @@ pub mod commands { CMD_GROUP_LOG, ) } + + pub fn goto_line(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Go To [{}]", + key_config.get_hint(key_config.keys.enter), + ), + "Go to the given line", + CMD_GROUP_GENERAL, + ) + } } diff --git a/src/tabs/files.rs b/src/tabs/files.rs index 79a071f2..7b5fc8d1 100644 --- a/src/tabs/files.rs +++ b/src/tabs/files.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::{ app::Environment, @@ -19,10 +19,13 @@ pub struct FilesTab { impl FilesTab { /// - pub fn new(env: &Environment) -> Self { + pub fn new( + env: &Environment, + select_file: Option, + ) -> Self { Self { visible: false, - files: RevisionFilesComponent::new(env), + files: RevisionFilesComponent::new(env, select_file), repo: env.repo.clone(), } } diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs index 1a97a0ac..6b78a076 100644 --- a/src/tabs/stashlist.rs +++ b/src/tabs/stashlist.rs @@ -55,7 +55,7 @@ impl StashList { } Err(e) => { self.queue.push(InternalEvent::ShowErrorMsg( - format!("stash apply error:\n{e}",), + format!("stash apply error:\n{e}"), )); } } diff --git a/src/tabs/status.rs b/src/tabs/status.rs index d5e1db8c..135cf18e 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -382,7 +382,7 @@ impl Status { /// pub fn update(&mut self) -> Result<()> { - self.git_branch_name.lookup().map(Some).unwrap_or(None); + let _ = self.git_branch_name.lookup().ok(); if self.is_visible() { let config = diff --git a/src/ui/reflow.rs b/src/ui/reflow.rs index 1de2a5c3..2238c445 100644 --- a/src/ui/reflow.rs +++ b/src/ui/reflow.rs @@ -161,7 +161,10 @@ impl<'a, 'b> LineTruncator<'a, 'b> { } } - pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) { + pub const fn set_horizontal_offset( + &mut self, + horizontal_offset: u16, + ) { self.horizontal_offset = horizontal_offset; } } diff --git a/src/ui/stateful_paragraph.rs b/src/ui/stateful_paragraph.rs index ca7d09fb..fc8ec076 100644 --- a/src/ui/stateful_paragraph.rs +++ b/src/ui/stateful_paragraph.rs @@ -70,7 +70,7 @@ impl ParagraphState { self.scroll } - pub fn set_scroll(&mut self, scroll: ScrollPos) { + pub const fn set_scroll(&mut self, scroll: ScrollPos) { self.scroll = scroll; } }