mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Merge branch 'master' into feature/more-space-view-diff
This commit is contained in:
commit
766f160f84
90 changed files with 3965 additions and 1210 deletions
|
|
@ -1,2 +1,2 @@
|
|||
msrv = "1.82.0"
|
||||
msrv = "1.88.0"
|
||||
cognitive-complexity-threshold = 18
|
||||
|
|
|
|||
208
.github/workflows/cd.yml
vendored
208
.github/workflows/cd.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
188
.github/workflows/nightly.yml
vendored
188
.github/workflows/nightly.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
63
CHANGELOG.md
63
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**
|
||||

|
||||
|
||||
**go to line in blame**
|
||||

|
||||
|
||||
### 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**
|
||||
|
||||
|
|
|
|||
1614
Cargo.lock
generated
1614
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
154
Cargo.toml
154
Cargo.toml
|
|
@ -1,28 +1,92 @@
|
|||
[package]
|
||||
name = "gitui"
|
||||
version = "0.27.0"
|
||||
version = "0.28.1"
|
||||
authors = ["extrawurst <mail@rusticorn.com>"]
|
||||
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
|
||||
|
|
|
|||
8
Makefile
8
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
|
||||
|
|
|
|||
10
README.md
10
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. <a name="limitations"></a> Known Limitations <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
- 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/)
|
||||
|
|
|
|||
BIN
assets/blame-goto-line.png
Normal file
BIN
assets/blame-goto-line.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
BIN
assets/discard-changes-on-checkout.png
Normal file
BIN
assets/discard-changes-on-checkout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
1
assets/logo.svg
Normal file
1
assets/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600" fill="none"><g clip-path="url(#a)"><mask id="c" width="438" height="484" x="38" y="57" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#D9D9D9" d="m340 541 76-39 60-52-76-43-35-8-13 42H231l-13-38-46-7-24-106 20-29v-44l85-47 101-6 48 26 67-40-60-67-150-25-189 78-31 201 116 182 185 22Z"/></mask><g stroke="#000" filter="url(#b)" mask="url(#c)"><path stroke-width="38.4" d="M290 483a183 183 0 1 0 0-366 183 183 0 0 0 0 366Z"/><path fill="#000" stroke-linejoin="round" stroke-width="12.8" d="m480 351 23-9-18-16-5 25Zm-14 36 25-4-15-20-10 24Zm-20 33 25 1-11-23-14 22Zm-26 28 24 6-6-24-18 18Zm-32 22 23 11-1-25-22 14Zm-35 16 20 15 4-25-24 10Zm-37 9 16 18 9-23-25 5Zm-39 1 13 21 13-21h-26Zm-38-6 9 23 16-18-25-5Zm-36-14 4 25 20-15-24-10Zm-33-20-1 25 23-11-22-14Zm-28-26-6 24 24-6-18-18Zm-22-32-11 23 25-1-14-22Zm-16-35-15 20 25 4-10-24Zm-9-37-18 16 23 9-5-25Zm-1-39-21 13 21 13v-26Zm6-38-23 9 18 16 5-25Zm14-36-25 4 15 20 10-24Zm20-33-25-1 11 23 14-22Zm26-28-24-6 6 24 18-18Zm32-22-23-11 1 25 22-14Zm35-16-20-15-4 25 24-10Zm37-9-16-18-9 23 25-5Zm39-1-13-21-13 21h26Zm38 6-9-23-16 18 25 5Zm36 14-4-25-20 15 24 10Zm33 20 1-25-23 11 22 14Zm28 26 6-24-24 6 18 18Zm22 32 11-23-25 1 14 22Zm16 36 15-20-25-4 10 24Zm9 36 18-16-23-9 5 25Z"/><path fill="#000" stroke-linejoin="round" stroke-width="25.6" d="m260 121 30 30 30-30h-60Zm191 95-19 38 38 19-19-57Zm-32 211-41-6-7 41 48-35Zm-210 35-7-41-41 6 48 35Zm-99-189 38-19-19-38-19 57Z"/></g></g><g filter="url(#d)"><path fill="#000" d="M305 307h81v7h-18v71l-35-6a98 98 0 0 1-92-5c-24-16-36-41-36-75 0-26 8-47 23-63 16-16 37-24 63-24 15 0 28 3 41 8l27-5 1 60h-9c-7-36-24-54-51-54h-7c-29 4-44 30-44 80 0 16 1 30 4 42 7 24 22 36 45 36 12 0 22-4 31-10v-55h-24v-7Zm157 71h3c2 0 4 3 4 7h-70v-7l6-1c7-1 10-6 10-14v-92h-16v-8h52v115h11Zm-8-152c0 4-1 7-3 10-4 7-10 11-18 11-4 0-7-1-10-3-8-4-11-10-11-18l2-10c4-7 10-11 19-11 3 0 7 1 10 3 7 4 11 10 11 18Zm24 46v-7h20v-27l36-6v33h37v7h-37v86l1 7c1 10 5 15 12 15l4-1c7-1 13-10 16-25l8 1c-1 8-4 14-6 19-6 9-16 14-30 14h-7c-23-3-34-18-34-45v-71h-20Zm101-49v-8h85v8h-22v97c0 11 0 20 2 28 5 21 16 31 36 31 30 0 45-19 45-59v-97h-22v-8h55v8h-22v96c0 13-2 25-5 34-8 23-27 35-57 35-48 0-72-22-72-65V223h-23Zm271 162h-85v-8h23V223h-23v-8h85v8h-22v154h15c4 1 6 3 7 8Z"/></g><defs><filter id="b" width="455.7" height="455.7" x="62" y="72" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation="2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_2"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_2" result="shape"/></filter><filter id="d" width="653.4" height="191.2" x="201" y="201" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation="2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_2"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_2" result="shape"/></filter><clipPath id="a"><path fill="#fff" d="M64 74h452v452H64z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "asyncgit"
|
||||
version = "0.27.0"
|
||||
version = "0.28.1"
|
||||
authors = ["extrawurst <mail@rusticorn.com>"]
|
||||
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"]
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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<StatusItem>,
|
||||
|
|
@ -31,19 +23,17 @@ pub struct Status {
|
|||
///
|
||||
#[derive(Default, Hash, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct StatusParams {
|
||||
tick: u128,
|
||||
status_type: StatusType,
|
||||
config: Option<ShowUntrackedFilesConfig>,
|
||||
}
|
||||
|
||||
impl StatusParams {
|
||||
///
|
||||
pub fn new(
|
||||
pub const fn new(
|
||||
status_type: StatusType,
|
||||
config: Option<ShowUntrackedFilesConfig>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tick: current_tick(),
|
||||
status_type,
|
||||
config,
|
||||
}
|
||||
|
|
@ -59,6 +49,8 @@ pub struct AsyncStatus {
|
|||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
repo: RepoPath,
|
||||
/// Counter that increments after each completed fetch.
|
||||
generation: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ impl BranchInfo {
|
|||
|
||||
None
|
||||
}
|
||||
|
||||
/// returns whether branch is local
|
||||
pub const fn is_local(&self) -> bool {
|
||||
matches!(self.details, BranchDetails::Local(_))
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<git2_hooks::HookResult> 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<crate::sync::cred::BasicAuthCredential>,
|
||||
) -> Result<HashMap<String, Oid>> {
|
||||
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<String> {
|
||||
// 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<HookResult> {
|
||||
pub fn hooks_pre_push(
|
||||
repo_path: &RepoPath,
|
||||
remote: &str,
|
||||
push: &PrePushTarget<'_>,
|
||||
basic_credential: Option<crate::sync::cred::BasicAuthCredential>,
|
||||
) -> Result<HookResult> {
|
||||
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<String, Oid>,
|
||||
) -> Result<PrePushRef> {
|
||||
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<String, Oid>,
|
||||
) -> Result<Vec<PrePushRef>> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -69,10 +69,14 @@ pub fn repo(repo_path: &RepoPath) -> Result<Repository> {
|
|||
}
|
||||
|
||||
pub fn gix_repo(repo_path: &RepoPath) -> Result<gix::Repository> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl NewFromOldContent {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn skip_old_line(&mut self) {
|
||||
const fn skip_old_line(&mut self) {
|
||||
self.old_index += 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<bool> {
|
||||
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
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Tags> {
|
|||
}
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
6
build.rs
6
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}'");
|
||||
|
|
|
|||
102
deny.toml
102
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" },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "filetreelist"
|
||||
version = "0.5.2"
|
||||
version = "0.6.0"
|
||||
authors = ["extrawurst <mail@rusticorn.com>"]
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "git2-hooks"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0"
|
||||
authors = ["extrawurst <mail@rusticorn.com>"]
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ pub enum HooksError {
|
|||
|
||||
#[error("shellexpand error:{0}")]
|
||||
ShellExpand(#[from] shellexpand::LookupError<std::env::VarError>),
|
||||
|
||||
#[error("hook process terminated by signal without exit code")]
|
||||
NoExitCode,
|
||||
}
|
||||
|
||||
/// crate specific `Result` type
|
||||
|
|
|
|||
|
|
@ -141,6 +141,20 @@ impl HookPaths {
|
|||
/// this function calls hook scripts based on conventions documented here
|
||||
/// see <https://git-scm.com/docs/githooks>
|
||||
pub fn run_hook_os_str<I, S>(&self, args: I) -> Result<HookResult>
|
||||
where
|
||||
I: IntoIterator<Item = S> + Copy,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
self.run_hook_os_str_with_stdin(args, None)
|
||||
}
|
||||
|
||||
/// this function calls hook scripts with stdin input based on conventions documented here
|
||||
/// see <https://git-scm.com/docs/githooks>
|
||||
pub fn run_hook_os_str_with_stdin<I, S>(
|
||||
&self,
|
||||
args: I,
|
||||
stdin: Option<&[u8]>,
|
||||
) -> Result<HookResult>
|
||||
where
|
||||
I: IntoIterator<Item = S> + Copy,
|
||||
S: AsRef<OsStr>,
|
||||
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<bool> {
|
||||
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<Oid>,
|
||||
pub remote_ref: String,
|
||||
pub remote_oid: Option<Oid>,
|
||||
}
|
||||
|
||||
impl PrePushRef {
|
||||
pub fn new(
|
||||
local_ref: impl Into<String>,
|
||||
local_oid: Option<Oid>,
|
||||
remote_ref: impl Into<String>,
|
||||
remote_oid: Option<Oid>,
|
||||
) -> Self {
|
||||
Self {
|
||||
local_ref: local_ref.into(),
|
||||
local_oid,
|
||||
remote_ref: remote_ref.into(),
|
||||
remote_oid,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_oid(oid: Option<Oid>) -> String {
|
||||
// "If the foreign ref does not yet exist the <remote-object-name> 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<i32>,
|
||||
/// 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 <https://git-scm.com/docs/githooks#_pre_push>
|
||||
///
|
||||
/// 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:
|
||||
/// `<local-ref> SP <local-object-name> SP <remote-ref> SP <remote-object-name> 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<HookResult> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T: AsRef<std::ffi::OsStr>>(
|
||||
suffix: Option<T>,
|
||||
) -> (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()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
max_width=70
|
||||
hard_tabs=true
|
||||
newline_style="Unix"
|
||||
max_width = 70
|
||||
hard_tabs = true
|
||||
newline_style = "Unix"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
64
src/app.rs
64
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<CommandBar>,
|
||||
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<bool>,
|
||||
|
|
@ -150,13 +153,14 @@ impl App {
|
|||
///
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn new(
|
||||
repo: RepoPathRef,
|
||||
cliargs: CliArgs,
|
||||
sender_git: Sender<AsyncGitNotification>,
|
||||
sender_app: Sender<AsyncAppNotification>,
|
||||
input: Input,
|
||||
theme: Theme,
|
||||
key_config: KeyConfig,
|
||||
) -> Result<Self> {
|
||||
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<PathBuf> = 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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
src/args.rs
45
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<PathBuf>,
|
||||
pub repo_path: RepoPath,
|
||||
pub notify_watcher: bool,
|
||||
pub key_bindings_path: Option<PathBuf>,
|
||||
pub key_symbols_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn process_cmdline() -> Result<CliArgs> {
|
||||
|
|
@ -51,6 +58,10 @@ pub fn process_cmdline() -> Result<CliArgs> {
|
|||
PathBuf::from,
|
||||
);
|
||||
|
||||
let select_file = arg_matches
|
||||
.get_one::<String>(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<CliArgs> {
|
|||
let notify_watcher: bool =
|
||||
*arg_matches.get_one(WATCHER_FLAG_ID).unwrap_or(&false);
|
||||
|
||||
let key_bindings_path = arg_matches
|
||||
.get_one::<String>(KEY_BINDINGS_FLAG_ID)
|
||||
.map(PathBuf::from);
|
||||
|
||||
let key_symbols_path = arg_matches
|
||||
.get_one::<String>(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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CommitInfo>,
|
||||
focus: Focus,
|
||||
key_config: SharedKeyConfig,
|
||||
select_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl RevisionFilesComponent {
|
||||
///
|
||||
pub fn new(env: &Environment) -> Self {
|
||||
pub fn new(
|
||||
env: &Environment,
|
||||
select_file: Option<PathBuf>,
|
||||
) -> 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);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ impl StatusTreeComponent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_commit(&mut self, revision: Option<CommitId>) {
|
||||
pub const fn set_commit(&mut self, revision: Option<CommitId>) {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ impl FileTreeItems {
|
|||
}
|
||||
|
||||
///
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
pub(crate) const fn len(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl HorizontalScroll {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_right(&self) -> usize {
|
||||
pub const fn get_right(&self) -> usize {
|
||||
self.right.get()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ impl VerticalScroll {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_top(&self) -> usize {
|
||||
pub const fn get_top(&self) -> usize {
|
||||
self.top.get()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<usize> {
|
||||
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();
|
||||
|
|
|
|||
290
src/gitui.rs
Normal file
290
src/gitui.rs
Normal file
|
|
@ -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<InputEvent>,
|
||||
rx_git: Receiver<AsyncGitNotification>,
|
||||
rx_app: Receiver<AsyncAppNotification>,
|
||||
rx_ticker: Receiver<Instant>,
|
||||
rx_watcher: Receiver<()>,
|
||||
}
|
||||
|
||||
impl Gitui {
|
||||
pub(crate) fn new(
|
||||
cliargs: CliArgs,
|
||||
theme: Theme,
|
||||
key_config: &KeyConfig,
|
||||
updater: Updater,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
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<B: ratatui::backend::Backend>(
|
||||
&mut self,
|
||||
terminal: &mut ratatui::Terminal<B>,
|
||||
) -> Result<QuitState, anyhow::Error>
|
||||
where
|
||||
<B as ratatui::backend::Backend>::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<B: ratatui::backend::Backend>(
|
||||
&self,
|
||||
terminal: &mut ratatui::Terminal<B>,
|
||||
) -> 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -34,9 +34,21 @@ impl KeyConfig {
|
|||
.map_or_else(|_| Ok(symbols_file), Ok)
|
||||
}
|
||||
|
||||
pub fn init() -> Result<Self> {
|
||||
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<Self> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
153
src/main.rs
153
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<CrosstermBackend<io::Stdout>>;
|
||||
|
||||
|
|
@ -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<QuitState, anyhow::Error> {
|
||||
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<B: ratatui::backend::Backend>(
|
||||
terminal: &mut ratatui::Terminal<B>,
|
||||
app: &App,
|
||||
) -> Result<(), B::Error> {
|
||||
if app.requires_redraw() {
|
||||
terminal.clear()?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CommitId> {
|
||||
self.blame
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
239
src/popups/checkout_option.rs
Normal file
239
src/popups/checkout_option.rs
Normal file
|
|
@ -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<BranchInfo>,
|
||||
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<Line<'_>> {
|
||||
let mut txt: Vec<Line> = 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<CommandInfo>,
|
||||
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<EventState> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
167
src/popups/goto_line.rs
Normal file
167
src/popups/goto_line.rs
Normal file
|
|
@ -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<CommandInfo>,
|
||||
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<EventState> {
|
||||
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::<isize>() {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -84,10 +84,15 @@ impl PushTagsPopup {
|
|||
&mut self,
|
||||
cred: Option<BasicAuthCredential>,
|
||||
) -> 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(())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ impl StashMsgPopup {
|
|||
}
|
||||
|
||||
///
|
||||
pub fn options(&mut self, options: StashingOptions) {
|
||||
pub const fn options(&mut self, options: StashingOptions) {
|
||||
self.options = options;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())? {
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
17
src/snapshots/gitui__gitui__tests__app_loading.snap
Normal file
17
src/snapshots/gitui__gitui__tests__app_loading.snap
Normal file
|
|
@ -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 ... ││ │"
|
||||
"│ ││ │"
|
||||
"└───────────────────────────────────────────┘└───────────────────────────────────────────┘"
|
||||
" "
|
||||
17
src/snapshots/gitui__gitui__tests__app_loading_finished.snap
Normal file
17
src/snapshots/gitui__gitui__tests__app_loading_finished.snap
Normal file
|
|
@ -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 [.]"
|
||||
|
|
@ -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 [.]"
|
||||
|
|
@ -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<B: ratatui::backend::Backend>(
|
||||
&self,
|
||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||
) -> io::Result<()> {
|
||||
terminal: &mut Terminal<B>,
|
||||
) -> Result<(), B::Error> {
|
||||
let idx = self.idx;
|
||||
|
||||
let char_to_draw =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
visible: false,
|
||||
files: RevisionFilesComponent::new(env),
|
||||
files: RevisionFilesComponent::new(env, select_file),
|
||||
repo: env.repo.clone(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue