Compare commits

..

No commits in common. "master" and "v0.16.2" have entirely different histories.

260 changed files with 25938 additions and 50358 deletions

View file

@ -1,8 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.arm-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

View file

@ -1,2 +1,2 @@
msrv = "1.88.0"
cognitive-complexity-threshold = 18
msrv = "1.50.0"
cognitive-complexity-threshold = 18

View file

@ -1,3 +0,0 @@
root = true
[*.rs]
indent_style = tab

2
.github/FUNDING.yml vendored
View file

@ -1 +1 @@
github: extrawurst
github: extrawurst

View file

@ -2,7 +2,7 @@
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
labels: ''
assignees: ''
---

View file

@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature-request'
labels: ''
assignees: ''
---

View file

@ -1,16 +0,0 @@
<!---
Thank you for contributing to GitUI! Please fill out the template below, and remove or add any
information as you feel necessary.
--->
This Pull Request fixes/closes #{issue_num}.
It changes the following:
-
-
I followed the checklist:
- [ ] I added unittests
- [ ] I ran `make check` without errors
- [ ] I tested the overall application
- [ ] I added an appropriate item to the changelog

View file

@ -5,12 +5,7 @@ updates:
schedule:
interval: daily
open-pull-requests-limit: 10
groups:
cargo-minor:
patterns: ["*"]
update-types:
- 'minor'
cargo-patch:
patterns: ["*"]
update-types:
- 'patch'
ignore:
- dependency-name: pprof
versions:
- 0.4.1

8
.github/stale.yml vendored
View file

@ -1,18 +1,18 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 180
daysUntilStale: 90
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 14
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- nostale
# Label to use when marking an issue as stale
staleLabel: dormant
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
any activity half a year. It will be closed in 14 days if no further activity occurs. Thank you
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View file

@ -1,23 +0,0 @@
name: brew update
on:
# only manually
workflow_dispatch:
inputs:
tag-name:
required: true
description: 'release tag'
jobs:
update_brew:
runs-on: ubuntu-latest
steps:
- name: Bump homebrew-core formula
uses: mislav/bump-homebrew-formula-action@v3
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
tag-name: ${{ github.event.inputs.tag-name }}

View file

@ -2,133 +2,98 @@ name: CD
on:
push:
tags:
- "*"
workflow_dispatch:
permissions:
contents: write
tags:
- '*'
jobs:
release:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Get version
id: get_version
run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
- name: Get version
id: get_version
run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
- 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: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: clippy
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: New Resolver
run: |
cargo install cargo-modify --force
cargo modify new-resolver
- uses: taiki-e/install-action@nextest
- name: Build
run: cargo build
- name: Run tests
run: make test
- name: Run clippy
run: |
cargo clean
make 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
- 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: 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 Release Mac
if: matrix.os == 'macos-latest'
run: make release-mac
- 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: 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: Set SHA
if: matrix.os == 'macos-latest'
id: shasum
run: |
echo ::set-output name=sha::"$(shasum -a 256 ./release/gitui-mac.tar.gz | awk '{printf $1}')"
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: Extract release notes
if: matrix.os == 'ubuntu-latest'
id: release_notes
uses: ffurrer2/extract-release-notes@v1
- name: Release
uses: softprops/action-gh-release@v1
with:
body: ${{ steps.release_notes.outputs.release_notes }}
prerelease: ${{ contains(github.ref, '-') }}
files: |
./release/*.tar.gz
./release/*.zip
./release/*.msi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
tar xf $GITHUB_WORKSPACE/arm.tar.xz
# - name: Bump personal tap formula
# uses: mislav/bump-homebrew-formula-action@v1
# if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases
# env:
# COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
# with:
# formula-name: gitui
# homebrew-tap: extrawurst/tap
# download-url: https://github.com/extrawurst/gitui/releases/download/${{ steps.get_version.outputs.version }}/gitui-mac.tar.gz
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: 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: 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: 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
- name: Bump homebrew-core formula
uses: mislav/bump-homebrew-formula-action@v1
if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases
env:
COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
with:
formula-name: gitui

View file

@ -2,11 +2,11 @@ name: CI
on:
schedule:
- cron: "0 2 * * *"
- cron: '0 2 * * *' # run at 2 AM UTC
push:
branches: ["*"]
branches: [ '*' ]
pull_request:
branches: [master]
branches: [ master ]
env:
CARGO_TERM_COLOR: always
@ -17,309 +17,151 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [nightly, stable, "1.88"]
rust: [nightly, stable, '1.50']
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
shared-key: ${{ matrix.os }}-${{ env.cache-name }}-${{ matrix.rust }}
- name: Restore cargo cache
uses: actions/cache@v2
env:
cache-name: ci
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
target
key: ${{ matrix.os }}-${{ env.cache-name }}-${{ matrix.rust }}-${{ hashFiles('Cargo.lock') }}
- name: MacOS Workaround
if: matrix.os == 'macos-latest'
run: cargo clean -p serde_derive -p thiserror
- name: MacOS Workaround
if: matrix.os == 'macos-latest'
run: cargo clean -p serde_derive -p thiserror
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
components: clippy
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
default: true
profile: minimal
components: clippy
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: New Resolver
if: matrix.rust != '1.50'
run: |
cargo install cargo-modify --force
cargo modify new-resolver
- name: Rustup Show
run: rustup show
- name: Build Debug
run: |
cargo build
- uses: taiki-e/install-action@nextest
- name: Run tests
run: make test
- name: Build Debug
run: |
cargo build
- name: Run clippy
run: |
make clippy
- name: Run tests
run: make test
- name: Build Release
run: make build-release
- name: Run clippy
run: |
make clippy
- name: Binary Size (unix)
if: matrix.os != 'windows-latest'
run: |
ls -l ./target/release/gitui
- name: Binary Size (win)
if: matrix.os == 'windows-latest'
run: |
ls -l ./target/release/gitui.exe
- name: Build Release
run: make build-release
- name: Binary dependencies (mac)
if: matrix.os == 'macos-latest'
run: |
otool -L ./target/release/gitui
- name: Test Install
run: cargo install --path "." --force --locked
- name: Binary Size (unix)
if: matrix.os != 'windows-latest'
run: |
ls -l ./target/release/gitui
- name: Binary Size (win)
if: matrix.os == 'windows-latest'
run: |
ls -l ./target/release/gitui.exe
- name: Binary dependencies (mac)
if: matrix.os == 'macos-latest'
run: |
otool -L ./target/release/gitui
- name: Build MSI (windows)
if: matrix.os == 'windows-latest'
run: |
cargo install cargo-wix --version 0.3.3 --locked
cargo wix --version
cargo wix -p gitui --no-build --nocapture --output ./target/wix/gitui-win.msi
ls -l ./target/wix/gitui-win.msi
- name: Build MSI (windows)
if: matrix.os == 'windows-latest'
run: |
cargo install cargo-wix
cargo wix --no-build --nocapture --output ./target/wix/gitui.msi
ls -l ./target/wix/gitui.msi
build-linux-musl:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: [nightly, stable, "1.88"]
rust: [nightly, stable, '1.50']
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@master
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
profile: minimal
default: true
target: x86_64-unknown-linux-musl
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}
# TODO: remove once we depend on 1.51 as a msrv and resolver is supported by default
- name: New Resolver
if: matrix.rust != '1.50'
run: |
cargo install cargo-modify --force
cargo modify new-resolver
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
targets: x86_64-unknown-linux-musl
- name: Setup MUSL
run: |
sudo apt-get -qq install musl-tools
- name: Build Debug
run: |
make build-linux-musl-debug
./target/x86_64-unknown-linux-musl/debug/gitui --version
- name: Build Release
run: |
make build-linux-musl-release
./target/x86_64-unknown-linux-musl/release/gitui --version
ls -l ./target/x86_64-unknown-linux-musl/release/gitui
- name: Test
run: |
make test-linux-musl
# The build would fail without manually installing the target.
# https://github.com/dtolnay/rust-toolchain/issues/83
- name: Manually install target
run: rustup target add x86_64-unknown-linux-musl
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: Rustup Show
run: rustup show
- uses: taiki-e/install-action@nextest
- name: Setup MUSL
run: |
sudo apt-get -qq install musl-tools
- name: Build Debug
run: |
make build-linux-musl-debug
./target/x86_64-unknown-linux-musl/debug/gitui --version
- name: Build Release
run: |
make build-linux-musl-release
./target/x86_64-unknown-linux-musl/release/gitui --version
ls -l ./target/x86_64-unknown-linux-musl/release/gitui
- name: Test
run: |
make test-linux-musl
- name: Test Install
run: cargo install --path "." --force --locked
build-linux-arm:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: [nightly, stable, "1.88"]
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: Setup ARM toolchain
run: |
rustup target add aarch64-unknown-linux-gnu
rustup target add armv7-unknown-linux-gnueabihf
rustup target add arm-unknown-linux-gnueabihf
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
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
tar xf $GITHUB_WORKSPACE/arm.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
- name: Rustup Show
run: rustup show
- name: Build Debug
run: |
make build-linux-arm-debug
- name: Build Release
run: |
make build-linux-arm-release
ls -l ./target/aarch64-unknown-linux-gnu/release/gitui || ls -l ./target/armv7-unknown-linux-gnueabihf/release/gitui || ls -l ./target/arm-unknown-linux-gnueabihf/release/gitui
build-apple-x86:
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
rust: [nightly, stable, "1.88"]
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: apple-x86-${{ env.cache-name }}-${{ matrix.rust }}
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: Setup target
run: rustup target add x86_64-apple-darwin
- name: Rustup Show
run: rustup show
- name: Build Debug
run: |
make build-apple-x86-debug
- name: Build Release
run: |
make build-apple-x86-release
ls -l ./target/x86_64-apple-darwin/release/gitui
linting:
name: Lints
rustfmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@master
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: rustfmt
- run: cargo fmt -- --check
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-stable
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt -- --check
- name: tombi install
uses: tombi-toml/setup-tombi@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
version: '0.9.0'
- name: tombi check
run: |
tombi format --check
- name: cargo-deny install
run: |
cargo install --locked cargo-deny
- name: cargo-deny checks
run: |
cargo deny check
udeps:
name: udeps
sec:
name: Security audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-nightly
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
- name: build cargo-udeps
run: cargo install --git https://github.com/est31/cargo-udeps --locked
- name: run cargo-udeps
run: cargo +nightly udeps --all-targets
- uses: actions/checkout@v2
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
log-test:
name: Changelog Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract release notes
id: extract_release_notes
uses: ffurrer2/extract-release-notes@v2
with:
release_notes_file: ./release-notes.txt
- uses: actions/upload-artifact@v4
with:
name: release-notes.txt
path: ./release-notes.txt
test-homebrew:
name: Test Homebrew Formula (macOS)
runs-on: macos-latest
steps:
- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Install stable Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Let Homebrew build gitui from source
run: brew install --build-from-source gitui
- uses: actions/checkout@master
- name: Extract release notes
id: extract_release_notes
uses: ffurrer2/extract-release-notes@v1
with:
release_notes_file: ./release-notes.txt
- uses: actions/upload-artifact@v1
with:
name: release-notes.txt
path: ./release-notes.txt

View file

@ -1,125 +0,0 @@
name: Build Nightly Releases
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
AWS_BUCKET_NAME: s3://gitui/nightly/
jobs:
release:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]
runs-on: ${{ matrix.os }}
steps:
- 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: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: taiki-e/install-action@nextest
# 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 MUSL
if: matrix.os == 'ubuntu-latest'
run: |
rustup target add x86_64-unknown-linux-musl
sudo apt-get -qq install musl-tools
- 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
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
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
tar xf $GITHUB_WORKSPACE/arm.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
- 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 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: 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: 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

View file

@ -1,4 +1,5 @@
{
"editor.formatOnSave": true,
"workbench.settings.enableNaturalLanguageSearch": false,
"telemetry.enableTelemetry": false,
}

File diff suppressed because it is too large Load diff

View file

@ -1,26 +0,0 @@
# Contributing
Were glad you found this document that is intended to make contributing to
GitUI as easy as possible!
## Building GitUI
In order to build GitUI on your machine, follow the instructions in the
[“Build” section](./README.md#build).
## Getting help
Theres a [Discord server][discord-server] you can join if you get stuck or
dont know where to start. People are happy to answer any questions you might
have!
## Getting started
If you are looking for something to work on, but dont yet know what might be a
good first issue, you can take a look at [issues labelled with
`good-first-issue`][good-first-issues]. They have been selected to not require
too much context so that people not familiar with the codebase yet can still
make a contribution.
[discord-server]: https://discord.gg/rZv4uxSQx3
[good-first-issues]: https://github.com/gitui-org/gitui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22

4768
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,110 +1,87 @@
[package]
name = "gitui"
version = "0.28.1"
authors = ["extrawurst <mail@rusticorn.com>"]
version = "0.16.2"
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"]
description = "blazing fast terminal-ui for git"
edition = "2021"
rust-version = "1.88"
exclude = [".github/*", ".vscode/*", "assets/*"]
homepage = "https://github.com/gitui-org/gitui"
repository = "https://github.com/gitui-org/gitui"
edition = "2018"
exclude = [".github/*",".vscode/*","assets/*"]
homepage = "https://github.com/extrawurst/gitui"
repository = "https://github.com/extrawurst/gitui"
readme = "README.md"
license = "MIT"
categories = ["command-line-utilities"]
keywords = ["cli", "git", "gui", "terminal", "ui"]
build = "build.rs"
[workspace]
members = [
"asyncgit",
"filetreelist",
"git2-hooks",
"git2-testing",
"scopetime",
keywords = [
"git",
"gui",
"cli",
"terminal",
"ui",
]
[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" }
asyncgit = { path = "./asyncgit", version = "0.16" }
filetreelist = { path = "./filetreelist", version = "0.2" }
crossterm = { version = "0.19", features = [ "serde" ] }
clap = { version = "2.33", default-features = false }
tui = { version = "0.15", default-features = false, features = ['crossterm', 'serde'] }
bytesize = { version = "1.0", default-features = false}
itertools = "0.10"
rayon-core = "1.9"
log = "0.4"
simplelog = { version = "0.10", default-features = false }
dirs-next = "2.0"
crossbeam-channel = "0.5"
scopeguard = "1.1"
bitflags = "1.2"
chrono = "0.4"
backtrace = "0.3"
ron = "0.6"
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"
anyhow = "1.0"
unicode-width = "0.1"
textwrap = "0.14"
unicode-truncate = "0.2"
unicode-segmentation = "1.7"
easy-cast = "0.4"
bugreport = "0.4"
lazy_static = "1.4"
syntect = { version = "4.5", default-features = false, features = ["metadata", "default-fancy"]}
[target.'cfg(all(target_family="unix",not(target_os="macos")))'.dependencies]
which = "4.1"
# pprof is not available on windows
[target.'cfg(not(windows))'.dependencies]
pprof = { version = "0.4", features = ["flamegraph"], optional = true }
[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"] }
pretty_assertions = "0.7"
[badges]
maintenance = { status = "actively-developed" }
[features]
default = ["ghemoji", "regex-fancy", "trace-libgit", "vendor-openssl"]
ghemoji = ["gh-emoji"]
# regex-* features are mutually exclusive.
regex-fancy = ["syntect/regex-fancy", "two-face/syntect-fancy"]
regex-onig = ["syntect/regex-onig", "two-face/syntect-onig"]
timing = ["scopetime/enabled"]
trace-libgit = ["asyncgit/trace-libgit"]
vendor-openssl = ["asyncgit/vendor-openssl"]
default=[]
timing=["scopetime/enabled"]
# 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
[workspace]
members=[
"asyncgit",
"scopetime",
"filetreelist",
]
[profile.release]
opt-level = "z" # Optimize for size.
strip = "debuginfo"
lto = true
opt-level = 'z' # Optimize for size.
codegen-units = 1
# make debug build as fast as release
# usage of utf8 encoding inside tui
# makes their debug profile slow
[profile.dev.package."tui"]
opt-level = 3
[profile.dev]
split-debuginfo = "unpacked"

26
FAQ.md
View file

@ -1,26 +0,0 @@
## <a name="table-of-contents"></a> Table of Contents
1. ["Bad Credentials" Error](#credentials)
2. [Custom key bindings](#keybindings)
2. [Watcher](#watcher)
## 1. <a name="credentials"></a> "Bad Credentials" Error <small><sup>[Top ▲](#table-of-contents)</sup></small>
Some users have trouble pushing/pulling from remotes and adding their ssh-key to their ssh-agent solved the issue. The error they get is:
![](./assets/bad-credentials.png)
See Github's excellent documentation for [Adding your SSH Key to the ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#adding-your-ssh-key-to-the-ssh-agent)
Note that in some cases adding the line `ssh-add -K ~/.ssh/id_ed25519`(or whatever your key is called) to your bash init script is necessary too to survive restarts.
## 2. <a name="keybindings"></a> Custom key bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
If you want to use `vi`-style keys or customize your key bindings in any other fashion see the specific docs on that: [key config](./KEY_CONFIG.md)
## 3. <a name="watcher"></a> Watching for changes <small><sup>[Top ▲](#table-of-contents)</sup></small>
By default, `gitui` polls for changes in the working directory every 5 seconds. If you supply `--watcher` as an argument, it uses a `notify`-based approach instead. This is usually faster and was for some time the default update strategy. It turned out, however, that `notify`-based updates can cause issues on some platforms, so tick-based updates seemed like a safer default.
See #1444 for details.

View file

@ -4,48 +4,14 @@ The default keys are based on arrow keys to navigate.
However popular demand lead to fully customizability of the key bindings.
Create a `key_bindings.ron` file like this:
```
(
move_left: Some(( code: Char('h'), modifiers: "")),
move_right: Some(( code: Char('l'), modifiers: "")),
move_up: Some(( code: Char('k'), modifiers: "")),
move_down: Some(( code: Char('j'), modifiers: "")),
stash_open: Some(( code: Char('l'), modifiers: "")),
open_help: Some(( code: F(1), modifiers: "")),
status_reset_item: Some(( code: Char('U'), modifiers: "SHIFT")),
)
```
On first start `gitui` will create `key_config.ron` file automatically based on the defaults.
This file allows changing every key binding.
The config file format based on the [Ron file format](https://github.com/ron-rs/ron).
The location of the file depends on your OS:
* `$HOME/.config/gitui/key_bindings.ron` (mac)
* `$XDG_CONFIG_HOME/gitui/key_bindings.ron` (linux using XDG)
* `$HOME/.config/gitui/key_bindings.ron` (linux)
* `%APPDATA%/gitui/key_bindings.ron` (Windows)
* `$HOME/.config/gitui/key_config.ron` (mac)
* `$XDG_CONFIG_HOME/gitui/key_config.ron` (linux using XDG)
* `$HOME/.config/gitui/key_config.ron` (linux)
* `%APPDATA%/gitui/key_config.ron` (Windows)
See all possible keys to overwrite in gitui: [here](https://github.com/gitui-org/gitui/blob/master/src/keys/key_list.rs#L83)
Possible values for:
* `code` are defined by the type `KeyCode` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/enum.KeyCode.html)
* `modifiers` are defined by the type `KeyModifiers` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/struct.KeyModifiers.html)
Here is a [vim style key config](vim_style_key_config.ron) with `h`, `j`, `k`, `l` to navigate. Use it to copy the content into `key_bindings.ron` to get vim style key bindings.
# Key Symbols
Similar to the above GitUI allows you to change the way the UI visualizes key combos containing special keys like `enter`(default: `⏎`) and `shift`(default: `⇧`).
If we can find a file `key_symbols.ron` in the above folders we apply the overwrites in it.
Example content of this file looks like:
```
(
enter: Some("enter"),
shift: Some("shift-")
)
```
This example will only overwrite two symbols. Find all possible symbols to overwrite in `symbols.rs` in the type `KeySymbolsFile` ([src/keys/symbols.rs](https://github.com/gitui-org/gitui/blob/master/src/keys/symbols.rs))
Here is a [vim style key config](vim_style_key_config.ron) with `h`, `j`, `k`, `l` to navigate. Use it to copy the content into `key_config.ron` to get vim style key bindings.

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 gitui-org
Copyright (c) 2021 Stephan Dilly
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,120 +1,66 @@
.PHONY: debug build-release release-linux-musl test clippy clippy-pedantic install install-debug sort
.PHONY: debug build-release release-linux-musl test clippy clippy-pedantic install install-debug
ARGS=-l
# ARGS=-l -d ~/code/extern/kubernetes
# ARGS=-l -d ~/code/extern/linux
# ARGS=-l -d ~/code/git-bare-test.git -w ~/code/git-bare-test
# ARGS=-l -d <some_path>
profile:
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --features timing -- ${ARGS}
run-timing:
cargo run --features=timing --release -- ${ARGS}
cargo run --features=timing,pprof -- ${ARGS}
debug:
RUST_BACKTRACE=true cargo run --features=timing -- ${ARGS}
build-release:
cargo build --release --locked
cargo build --release
release-mac: build-release
strip target/release/gitui
otool -L target/release/gitui
ls -lisah target/release/gitui
mkdir -p release
tar -C ./target/release/ -czvf ./release/gitui-mac.tar.gz ./gitui
ls -lisah ./release/gitui-mac.tar.gz
release-mac-x86: build-apple-x86-release
strip target/x86_64-apple-darwin/release/gitui
otool -L target/x86_64-apple-darwin/release/gitui
ls -lisah target/x86_64-apple-darwin/release/gitui
mkdir -p release
tar -C ./target/x86_64-apple-darwin/release/ -czvf ./release/gitui-mac-x86.tar.gz ./gitui
ls -lisah ./release/gitui-mac-x86.tar.gz
release-win: build-release
mkdir -p release
tar -C ./target/release/ -czvf ./release/gitui-win.tar.gz ./gitui.exe
cargo install cargo-wix --version 0.3.3 --locked
cargo wix -p gitui --no-build --nocapture --output ./release/gitui-win.msi
ls -l ./release/gitui-win.msi
cargo install cargo-wix
cargo wix --no-build --nocapture --output ./release/gitui.msi
ls -l ./release/gitui.msi
release-linux-musl: build-linux-musl-release
strip target/x86_64-unknown-linux-musl/release/gitui
mkdir -p release
tar -C ./target/x86_64-unknown-linux-musl/release/ -czvf ./release/gitui-linux-x86_64.tar.gz ./gitui
build-apple-x86-debug:
cargo build --target=x86_64-apple-darwin
build-apple-x86-release:
cargo build --release --target=x86_64-apple-darwin --locked
tar -C ./target/x86_64-unknown-linux-musl/release/ -czvf ./release/gitui-linux-musl.tar.gz ./gitui
build-linux-musl-debug:
cargo build --target=x86_64-unknown-linux-musl
build-linux-musl-release:
cargo build --release --target=x86_64-unknown-linux-musl --locked
cargo build --release --target=x86_64-unknown-linux-musl
test-linux-musl:
cargo nextest run --workspace --target=x86_64-unknown-linux-musl
release-linux-arm: build-linux-arm-release
mkdir -p release
aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/gitui
arm-linux-gnueabihf-strip target/armv7-unknown-linux-gnueabihf/release/gitui
arm-linux-gnueabihf-strip target/arm-unknown-linux-gnueabihf/release/gitui
tar -C ./target/aarch64-unknown-linux-gnu/release/ -czvf ./release/gitui-linux-aarch64.tar.gz ./gitui
tar -C ./target/armv7-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-armv7.tar.gz ./gitui
tar -C ./target/arm-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-arm.tar.gz ./gitui
build-linux-arm-debug:
cargo build --target=aarch64-unknown-linux-gnu
cargo build --target=armv7-unknown-linux-gnueabihf
cargo build --target=arm-unknown-linux-gnueabihf
build-linux-arm-release:
cargo build --release --target=aarch64-unknown-linux-gnu --locked
cargo build --release --target=armv7-unknown-linux-gnueabihf --locked
cargo build --release --target=arm-unknown-linux-gnueabihf --locked
cargo test --workspace --target=x86_64-unknown-linux-musl
test:
cargo nextest run --workspace
cargo test --workspace
fmt:
cargo fmt -- --check
clippy:
touch src/main.rs
cargo clean -p gitui -p asyncgit -p scopetime -p filetreelist
cargo clippy --workspace --all-features
clippy-nightly:
touch src/main.rs
cargo clean -p gitui -p asyncgit -p scopetime -p filetreelist
cargo +nightly clippy --workspace --all-features
check: fmt clippy test sort deny
check-nightly:
cargo +nightly c
cargo +nightly clippy --workspace --all-features
cargo +nightly t
deny:
cargo deny check
sort:
tombi format --check
check: fmt clippy test
install:
cargo install --path "." --offline --locked
cargo install --path "." --offline
install-timing:
cargo install --features=timing --path "." --offline --locked
licenses:
cargo bundle-licenses --format toml --output THIRDPARTY.toml
clean:
cargo clean
cargo install --features=timing --path "." --offline

View file

@ -1,14 +0,0 @@
# Nightlies
**Use with caution as these binaries are build nightly and might be broken**
When you find problems please report them and always mention the version that you see in the `help popup` or when running `gitui -V`
* [gitui-linux-aarch64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-aarch64.tar.gz)
* [gitui-linux-arm.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-arm.tar.gz)
* [gitui-linux-armv7.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-armv7.tar.gz)
* [gitui-linux-x86_64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-x86_64.tar.gz)
* [gitui-mac.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac.tar.gz)
* [gitui-mac-x86.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac-x86.tar.gz)
* [gitui-win.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.tar.gz)
* [gitui-win.msi](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.msi)

195
README.md
View file

@ -1,23 +1,23 @@
<h1 align="center">
<img width="300px" src="assets/logo.png" />
[![CI][s0]][l0] [![crates][s1]][l1] ![MIT][s2] [![UNSAFE][s3]][l3] [![TWEET][s6]][l6] [![dep_status][s7]][l7] [![discord][s8]][l8]
[![CI][s0]][l0] [![crates][s1]][l1] ![MIT][s2] [![UNSAFE][s3]][l3] [![ITCH][s4]][l4] [![DISC][s5]][l5] [![TWEET][s6]][l6]
</h1>
[s0]: https://github.com/gitui-org/gitui/workflows/CI/badge.svg
[l0]: https://github.com/gitui-org/gitui/actions
[s0]: https://github.com/extrawurst/gitui/workflows/CI/badge.svg
[l0]: https://github.com/extrawurst/gitui/actions
[s1]: https://img.shields.io/crates/v/gitui.svg
[l1]: https://crates.io/crates/gitui
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
[s3]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[l3]: https://github.com/rust-secure-code/safety-dance/
[s4]: https://img.shields.io/badge/itch.io-ok-green
[l4]: https://extrawurst.itch.io/gitui
[s5]: https://img.shields.io/discord/723083834811220028.svg?logo=chat
[l5]: https://discord.gg/7TGFfuq
[s6]: https://img.shields.io/twitter/follow/extrawurst?label=follow&style=social
[l6]: https://twitter.com/intent/follow?screen_name=extrawurst
[s7]: https://deps.rs/repo/github/gitui-org/gitui/status.svg
[l7]: https://deps.rs/repo/github/gitui-org/gitui
[s8]: https://img.shields.io/discord/1176858176897953872
[l8]: https://discord.gg/rQNeEnMhus
<h5 align="center">GitUI provides you with the comfort of a git GUI but right in your terminal</h1>
@ -32,35 +32,30 @@
5. [Limitations](#limitations)
6. [Installation](#installation)
7. [Build](#build)
8. [FAQs](#faqs)
9. [Diagnostics](#diagnostics)
10. [Color Theme](#theme)
11. [Key Bindings](#bindings)
12. [Sponsoring](#sponsoring)
13. [Inspiration](#inspiration)
14. [Contributing](#contributing)
15. [Contributors](#contributors)
8. [Diagnostics](#diagnostics)
9. [Color Theme](#theme)
10. [Key Bindings](#bindings)
11. [Sponsoring](#sponsoring)
12. [Inspiration](#inspiration)
## 1. <a name="features"></a> Features <small><sup>[Top ▲](#table-of-contents)</sup></small>
- Fast and intuitive **keyboard only** control
- Context based help (**no need to memorize** tons of hot-keys)
- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*,*prepare-commit-msg*)
- Inspect, commit, and amend changes (incl. hooks: _commit-msg_/_post-commit_)
- Stage, unstage, revert and reset files, hunks and lines
- Stashing (save, pop, apply, drop, and inspect)
- Push / Fetch to / from remote
- Push/Fetch to/from remote
- Branch List (create, rename, delete, checkout, remotes)
- Browse / **Search** commit log, diff committed changes
- Responsive terminal UI
- Browse commit log, diff committed changes
- Scalable terminal UI layout
- Async git API for fluid control
- Submodule support
- gpg commit signing with shortcomings (see [#97](https://github.com/gitui-org/gitui/issues/97)))
## 2. <a name="motivation"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small>
I do most of my git work in a terminal but I frequently found myself using git GUIs for some use-cases like: index, commit, diff, stash, blame and log.
Unfortunately popular git GUIs all fail on giant repositories or become unresponsive and unusable.
Unfortunately popular git GUIs all fail on giant repositories or become unresponsive and unusable.
GitUI provides you with the user experience and comfort of a git GUI but right in your terminal while being portable, fast, free and opensource.
@ -70,22 +65,25 @@ For a [RustBerlin meetup presentation](https://youtu.be/rpilJV-eIVw?t=5334) ([sl
| | Time | Memory (GB) | Binary (MB) | Freezes | Crashes |
| --------- | ---------- | ----------- | ----------- | --------- | --------- |
| `gitui` | **24 s** ✅ | **0.17** ✅ | 10 | **No** ✅ | **No** ✅ |
| `lazygit` | 57 s | 2.6 | 25 | Yes | Sometimes |
| `gitui` | **24 s** ✅ | **0.17** ✅ | 1.4 | **No** ✅ | **No** ✅ |
| `lazygit` | 57 s | 2.6 | 16 | Yes | Sometimes |
| `tig` | 4 m 20 s | 1.3 | **0.6** ✅ | Sometimes | **No** ✅ |
## 4. <a name="roadmap"></a> Road(map) to 1.0 <small><sup>[Top ▲](#table-of-contents)</sup></small>
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))
* log search (commit, author, sha) ([#449](https://github.com/extrawurst/gitui/issues/449),[#429](https://github.com/extrawurst/gitui/issues/429))
* file history log ([#381](https://github.com/extrawurst/gitui/issues/381))
* visualize branching structure in log tab ([#81](https://github.com/extrawurst/gitui/issues/81))
* interactive rebase ([#32](https://github.com/extrawurst/gitui/issues/32))
* notify-based change detection ([#1](https://github.com/extrawurst/gitui/issues/1))
## 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))
- *credential.helper* for https needs to be **explicitly** configured (see [#800](https://github.com/gitui-org/gitui/issues/800))
- no support for [bare repositories](https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server) (see [#100](https://github.com/extrawurst/gitui/issues/100))
- no support for [core.hooksPath](https://git-scm.com/docs/githooks) config
- no support for GPG signing (see [#97](https://github.com/extrawurst/gitui/issues/97))
Currently, this tool does not fully substitute the _git shell_, however both tools work well in tandem.
@ -95,30 +93,22 @@ All support is welcomed! Sponsors as well! ❤️
## 6. <a name="installation"></a> Installation <small><sup>[Top ▲](#table-of-contents)</sup></small>
GitUI is in beta and may contain bugs and missing features. However, for personal use it is reasonably stable and is being used while developing itself.
For the time being this product is in alpha and is not considered production ready. However, for personal use it is reasonably stable and is being used while developing itself.
<a href="https://repology.org/project/gitui/versions">
<img src="https://repology.org/badge/vertical-allrepos/gitui.svg" alt="Packaging status" align="right">
</a>
### Various Package Managers
<details>
<summary>Install Instructions</summary>
##### [Arch Linux](https://archlinux.org/packages/extra/x86_64/gitui/)
### [Arch Linux](https://archlinux.org/packages/community/x86_64/gitui/)
```sh
pacman -S gitui
```
##### Fedora
### Fedora
```sh
sudo dnf install gitui
```
##### Gentoo
### Gentoo
Available in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)
```sh
@ -127,49 +117,25 @@ sudo emerge --sync dm9pZCAq
sudo emerge dev-vcs/gitui::dm9pZCAq
```
##### [openSUSE](https://software.opensuse.org/package/gitui)
```sh
sudo zypper install gitui
```
##### Homebrew (macOS)
### Homebrew (macOS)
```sh
brew install gitui
```
##### [MacPorts (macOS)](https://ports.macports.org/port/gitui/details/)
```sh
port install gitui
```
##### [Winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/StephanDilly/gitui) (Windows)
```
winget install gitui
```
##### [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/gitui.json) (Windows)
### [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/gitui.json) (Windows)
```
scoop install gitui
```
##### [Chocolatey](https://chocolatey.org/packages/gitui) (Windows)
### [Chocolatey](https://chocolatey.org/packages/gitui) (Windows)
```
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)
### [Nix](https://search.nixos.org/packages?channel=unstable&show=gitui&from=0&size=50&sort=relevance&query=gitui) (Nix/NixOS)
Nixpkg
```
@ -180,78 +146,28 @@ NixOS
nix-env -iA nixos.gitui
```
##### [Termux](https://github.com/termux/termux-packages/tree/master/packages/gitui) (Android)
## Release Binaries
```
pkg install gitui
```
##### [Anaconda](https://anaconda.org/conda-forge/gitui)
```
conda install -c conda-forge gitui
```
</details>
### Release Binaries
[Available for download in releases](https://github.com/gitui-org/gitui/releases)
[Available for download in releases](https://github.com/extrawurst/gitui/releases)
Binaries available for:
#### Linux
- gitui-linux-x86_64.tar.gz (linux musl statically linked)
- gitui-linux-aarch64.tar.gz (linux on 64 bit arm)
- gitui-linux-arm.tar.gz
- gitui-linux-armv7.tar.gz
All contain a single binary file
#### macOS
- gitui-mac.tar.gz (arm64)
- gitui-mac-x86.tar.gz (intel x86)
#### Windows
- gitui-win.tar.gz (single 64bit binary)
- gitui-win.msi (64bit Installer package)
### Nightly Builds
see [NIGHTLIES.md](./NIGHTLIES.md)
- Linux
- macOS
- Windows
## 7. <a name="build"></a> Build <small><sup>[Top ▲](#table-of-contents)</sup></small>
### Requirements
- Minimum supported `rust`/`cargo` version: `1.88`
- Latest `rust` and `cargo`
- See [Install Rust](https://www.rust-lang.org/tools/install)
- To build openssl dependency (see https://docs.rs/openssl/latest/openssl/)
- perl >= 5.12 (strawberry perl works for windows https://strawberryperl.com/)
- a c compiler (msvc, gcc or clang, cargo will find it)
- To run the complete test suite python is required (and it must be invocable as `python`)
### Cargo Install
The simplest way to start playing around with `gitui` is to have `cargo` build and install it with `cargo install gitui --locked`. If you are not familiar with rust and cargo: [Getting Started with Rust](https://doc.rust-lang.org/book/ch01-00-getting-started.html)
The simplest way to start playing around with `gitui` is to have `cargo` build and install it with `cargo install gitui`
### Cargo Features
#### trace-libgit
enable `libgit2` tracing
works if `libgit2` built with `-DENABLE_TRACE=ON`
this feature enabled by default, to disable: `cargo install --no-default-features`
## 8. <a name="faqs"></a> FAQs <small><sup>[Top ▲](#table-of-contents)</sup></small>
see [FAQs page](./FAQ.md)
## 9. <a name="diagnostics"></a> Diagnostics <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 8. <a name="diagnostics"></a> Diagnostics <small><sup>[Top ▲](#table-of-contents)</sup></small>
To run with logging enabled run `gitui -l`.
@ -262,7 +178,7 @@ This will log to:
- Linux: `$HOME/.cache/gitui/gitui.log`
- Windows: `%LOCALAPPDATA%/gitui/gitui.log`
## 10. <a name="theme"></a> Color Theme <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 9. <a name="theme"></a> Color Theme <small><sup>[Top ▲](#table-of-contents)</sup></small>
![](assets/light-theme.png)
@ -270,32 +186,19 @@ This will log to:
However, you can customize everything to your liking: See [Themes](THEMES.md).
## 11. <a name="bindings"></a> Key Bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 10. <a name="bindings"></a> Key Bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
The key bindings can be customized: See [Key Config](KEY_CONFIG.md) on how to set them to `vim`-like bindings.
## 12. <a name="sponsoring"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 11. <a name="sponsoring"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>
[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/extrawurst)
[![buy-me-a-coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000)](https://www.buymeacoffee.com/extrawurst)
## 13. <a name="inspiration"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 12. <a name="inspiration"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>
- [lazygit](https://github.com/jesseduffield/lazygit)
- [tig](https://github.com/jonas/tig)
- [GitUp](https://github.com/git-up/GitUp)
- It would be nice to come up with a way to have the map view available in a terminal tool
- [git-brunch](https://github.com/andys8/git-brunch)
## 14. <a name="contributing"></a> Contributing <small><sup>[Top ▲](#table-of-contents)</sup></small>
See [CONTRIBUTING.md](CONTRIBUTING.md).
## 15. <a name="contributors"></a> Contributors <small><sup>[Top ▲](#table-of-contents)</sup></small>
Thanks goes to all the contributors that help make GitUI amazing! ❤️
Wanna become a co-maintainer? We are looking for [you](https://github.com/gitui-org/gitui/issues/2084)!
<a href="https://github.com/gitui-org/gitui/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gitui-org/gitui" />
</a>

View file

@ -3,88 +3,14 @@
default on light terminal:
![](assets/light-theme.png)
## Configuration
to change the colors of the default theme you have to modify `theme.ron` file
[Ron format](https://github.com/ron-rs/ron) located at config path. The path differs depending on the operating system:
To change the colors of the default theme you need to add a `theme.ron` file that contains the colors you want to override. Note that you dont have to specify the full theme anymore (as of 0.23). Instead, it is sufficient to override just the values that you want to differ from their default values.
The file uses the [Ron format](https://github.com/ron-rs/ron) and is located at one of the following paths, depending on your operating system:
* `$HOME/.config/gitui/theme.ron` (mac)
* `$HOME/Library/Application Support/gitui/theme.ron` (mac)
* `$XDG_CONFIG_HOME/gitui/theme.ron` (linux using XDG)
* `$HOME/.config/gitui/theme.ron` (linux)
* `%APPDATA%/gitui/theme.ron` (Windows)
Alternatively, you can create a theme in the same directory mentioned above and use it with the `-t` flag followed by the name of the file in the directory. E.g. If you are on linux calling `gitui -t arc.ron`, this will load the theme in `$XDG_CONFIG_HOME/gitui/arc.ron` or `$HOME/.config/gitui/arc.ron`.
Alternatively you may make a theme in the same directory mentioned above with and select with the `-t` flag followed by the name of the file in the directory. E.g. If you are on linux calling `gitui -t arc.ron` wil use `$XDG_CONFIG_HOME/gitui/arc.ron` or `$HOME/.config/gitui/arc.ron`
Example theme override:
```ron
(
selection_bg: Some("Blue"),
selection_fg: Some("#ffffff"),
)
```
Note that you need to wrap values in `Some` due to the way the overrides work (as of 0.23).
Notes:
* rgb colors might not be supported in every terminal.
* using a color like `yellow` might appear in whatever your terminal/theme defines for `yellow`
* valid colors can be found in ratatui's [Color](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) struct.
* all customizable theme elements can be found in [`style.rs` in the `impl Default for Theme` block](https://github.com/gitui-org/gitui/blob/master/src/ui/style.rs#L305)
## Preset Themes
You can find preset themes by Catppuccin [here](https://github.com/catppuccin/gitui.git).
## Syntax Highlighting
The syntax highlighting theme can be defined using the element `syntax`. Both [default themes of the syntect library](https://github.com/trishume/syntect/blob/7fe13c0fd53cdfa0f9fea1aa14c5ba37f81d8b71/src/dumps.rs#L215) and custom themes are supported.
Example syntax theme:
```ron
(
syntax: Some("InspiredGitHub"),
)
```
Custom themes are located in the [configuration directory](#configuration), are using TextMate's theme format and must have a `.tmTheme` file extension. To load a custom theme, `syntax` must be set to the file name without the file extension. For example, to load [`Blackboard.tmTheme`](https://raw.githubusercontent.com/filmgirl/TextMate-Themes/refs/heads/master/Blackboard.tmTheme), place the file next to `theme.ron` and set:
```ron
(
syntax: Some("Blackboard"),
)
```
[filmgirl/TextMate-Themes](https://github.com/filmgirl/TextMate-Themes) offers many [beautiful](https://inkdeep.github.io/TextMate-Themes) TextMate themes to choose from.
## Customizing line breaks
If you want to change how the line break is displayed in the diff, you can also specify `line_break` in your `theme.ron`:
```ron
(
line_break: Some("¶"),
)
```
Note that if you want to turn it off, you should use a blank string:
```ron
(
line_break: Some(""),
)
```
## Customizing selection
By default the `selection_fg` color is used to color the text of the selected line.
Diff line, filename, commit hashes, time and author are re-colored with `selection_fg` color.
This can be changed by specifying the `use_selection_fg` boolean in your `theme.ron`:
```
(
use_selection_fg: Some(false),
)
```
By default, `use_selection_fg` is set to `true`.
Valid colors can be found in tui-rs' [Color](https://docs.rs/tui/0.12.0/tui/style/enum.Color.html) struct. note that rgb colors might not be supported in every terminal.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 900 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

View file

@ -1 +0,0 @@
<mxfile host="app.diagrams.net" modified="2021-08-17T21:58:53.216Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" etag="DR-vNI6rA-1d9_EpYLQC" version="14.9.7" type="device"><diagram id="cIq82w5ce00BbVejkL92" name="Page-1">5Zldc6IwFIZ/DZfrEJCvS0Xb7exHZ9bd6WxvdlIIkGlMmBir3V+/iQQVg+12Bqm7eqHwkkB43nMCJ1puPF9fc1gWX1iKiOXY6dpyJ5bjABCG8kcpz5Xi+0El5BynutFOmOHfSIu2Vpc4RYtGQ8EYEbhsigmjFCWioUHO2arZLGOkedUS5sgQZgkkpnqHU1FUaugEO/0jwnlRXxn4UXVkDuvG+k4WBUzZak9yp5Ybc8ZEtTVfx4goeDWXqt/VkaPbgXFExd90KLNx+X10/wl4v+6/5fx28jMVH/RZniBZ6hvWgxXPNQHOljRF6iS25Y5XBRZoVsJEHV1Jz6VWiDmRe0Bu6tMhLtD66DjB9u5l2CA2R4I/yyZ1h0B30REDvGDgVcpq54AbaazFHn0n1CLUrufbs+/AyA3N5g2cnPPj5Hh+g5Njm5S2wbdPCYTeiSi5BqVZIUPesX9QwWHyKOkcUpP3L4c1hgTnVCoEZWpXgcEyCUdanuM0VT3GTcgZJiRmhPHNudxs8+mIrn9A17UNutE2MBtBeKoYHBp0JyiDSyKOQd2Lt4Xg7BHVrCij6ACflmofEokJ8ZecaAvwpjsduDC03RrxCz74bVPBqVzwDBfuFInFhoR8DhWQ5vKB1b0lr6VGL4Y4fjBwDhwJTUcA6Dc1fMOUm5wyjv7nzBgOgteN6DU1AsOFmNENccf+jKnKCh/OFQ76sCi3HC4iS1zbNCfs05zQMGd4UdnRZkCv2REZBmDFqljSRymTKj8uJx+clvepPu2oC8w9P+wT8D/jhGhxoNeEAGbxORNQLM8zDTL5KJvpIQ07cKTtTQq8e40BzEJ3gmVNdamO+O/viFlU35YCM3r+aaL2CXxAZMx4ivjBNbtwzIsOZzUQvb9jZoloWPWmxaIDdzoB11zdcD2TWuva0fBk0MwSLt6U0moAkKby/SiXm0xORfZyt5pky2BW7032DZVf9ePDtrzxNRYJoxmWvWJ1aLMQpba+oWTJF/gJWd7kWAp1uxzVZm8nJgaDenl962PLGiDwt9PavpXuyaxsqQMJWxwtxs9u7Q8EzewYAq9tBbrXOcUs32YrGeFFFezW1LGiqTUC1vTKGodWeKWUUWyF3k7pK9xPMD+1OxBEfTpg1m9f4VOFfzD4p+DWL94vh3c3cOXu7i+uzbG9Pwrd6R8=</diagram></mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 KiB

View file

@ -1,52 +1,33 @@
[package]
name = "asyncgit"
version = "0.28.1"
authors = ["extrawurst <mail@rusticorn.com>"]
edition = "2021"
version = "0.16.3"
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"]
edition = "2018"
description = "allow using git2 in a asynchronous context"
homepage = "https://github.com/gitui-org/gitui"
repository = "https://github.com/gitui-org/gitui"
homepage = "https://github.com/extrawurst/gitui"
repository = "https://github.com/extrawurst/gitui"
readme = "README.md"
license = "MIT"
categories = ["asynchronous", "concurrency"]
categories = ["concurrency","asynchronous"]
keywords = ["git"]
[dependencies]
bitflags = "2"
crossbeam-channel = "0.5"
dirs = "6.0"
easy-cast = "0.5"
fuzzy-matcher = "0.3"
git2 = "0.20"
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 }
rayon = "1.11"
rayon-core = "1.13"
scopetime = { path = "../scopetime", version = "0.1" }
serde = { version = "1.0", features = ["derive"] }
ssh-key = { version = "0.6.7", features = ["crypto", "encryption"] }
thiserror = "2.0"
unicode-truncate = "2.0"
url = "2.5"
git2 = "0.13"
# pinning to vendored openssl, using the git2 feature this gets lost with new resolver
openssl-sys = { version = '0.9', features= ["vendored"] }
# git2 = { path = "../../github/git2-rs", features = ["vendored-openssl"]}
# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="513a8c9", features = ["vendored-openssl"]}
rayon-core = "1.9"
crossbeam-channel = "0.5"
log = "0.4"
thiserror = "1.0"
url = "2.2"
unicode-truncate = "0.2.0"
easy-cast = "0.4"
[dev-dependencies]
env_logger = "0.11"
tempfile = "3.2"
invalidstring = { path = "../invalidstring", version = "0.1" }
pretty_assertions = "1.4"
serial_test = "3.3"
tempfile = "3"
[features]
default = ["trace-libgit"]
trace-libgit = []
vendor-openssl = ["openssl-sys"]
serial_test = "0.5.1"
pretty_assertions = "0.7"

View file

@ -4,299 +4,206 @@
use crate::error::Result;
use crossbeam_channel::Sender;
use std::sync::{Arc, Mutex, RwLock};
/// Passed to `AsyncJob::run` allowing sending intermediate progress notifications
pub struct RunParams<
T: Copy + Send,
P: Clone + Send + Sync + PartialEq,
> {
sender: Sender<T>,
progress: Arc<RwLock<P>>,
}
impl<T: Copy + Send, P: Clone + Send + Sync + PartialEq>
RunParams<T, P>
{
/// send an intermediate update notification.
/// do not confuse this with the return value of `run`.
/// `send` should only be used about progress notifications
/// and not for the final notification indicating the end of the async job.
/// see `run` for more info
pub fn send(&self, notification: T) -> Result<()> {
self.sender.send(notification)?;
Ok(())
}
/// set the current progress
pub fn set_progress(&self, p: P) -> Result<bool> {
Ok(if *self.progress.read()? == p {
false
} else {
*(self.progress.write()?) = p;
true
})
}
}
use std::sync::{Arc, Mutex};
/// trait that defines an async task we can run on a threadpool
pub trait AsyncJob: Send + Sync + Clone {
/// defines what notification type is used to communicate outside
type Notification: Copy + Send;
/// type of progress
type Progress: Clone + Default + Send + Sync + PartialEq;
/// can run a synchronous time intensive task.
/// the returned notification is used to tell interested parties
/// that the job finished and the job can be access via `take_last`.
/// prior to this final notification it is not safe to assume `take_last`
/// will already return the correct job
fn run(
&mut self,
params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification>;
/// allows observers to get intermediate progress status if the job customizes it
/// by default this will be returning `Self::Progress::default()`
fn get_progress(&self) -> Self::Progress {
Self::Progress::default()
}
/// can run a synchronous time intensive task
fn run(&mut self);
}
/// Abstraction for a FIFO task queue that will only queue up **one** `next` job.
/// It keeps overwriting the next job until it is actually taken to be processed
#[derive(Debug, Clone)]
pub struct AsyncSingleJob<J: AsyncJob> {
next: Arc<Mutex<Option<J>>>,
last: Arc<Mutex<Option<J>>>,
progress: Arc<RwLock<J::Progress>>,
sender: Sender<J::Notification>,
pending: Arc<Mutex<()>>,
pub struct AsyncSingleJob<J: AsyncJob, T: Copy + Send + 'static> {
next: Arc<Mutex<Option<J>>>,
last: Arc<Mutex<Option<J>>>,
sender: Sender<T>,
pending: Arc<Mutex<()>>,
notification: T,
}
impl<J: 'static + AsyncJob> AsyncSingleJob<J> {
///
pub fn new(sender: Sender<J::Notification>) -> Self {
Self {
next: Arc::new(Mutex::new(None)),
last: Arc::new(Mutex::new(None)),
pending: Arc::new(Mutex::new(())),
progress: Arc::new(RwLock::new(J::Progress::default())),
sender,
}
}
impl<J: 'static + AsyncJob, T: Copy + Send + 'static>
AsyncSingleJob<J, T>
{
///
pub fn new(sender: Sender<T>, value: T) -> Self {
Self {
next: Arc::new(Mutex::new(None)),
last: Arc::new(Mutex::new(None)),
pending: Arc::new(Mutex::new(())),
notification: value,
sender,
}
}
///
pub fn is_pending(&self) -> bool {
self.pending.try_lock().is_err()
}
///
pub fn is_pending(&self) -> bool {
self.pending.try_lock().is_err()
}
/// makes sure `next` is cleared and returns `true` if it actually canceled something
pub fn cancel(&self) -> bool {
if let Ok(mut next) = self.next.lock() {
if next.is_some() {
*next = None;
return true;
}
}
/// makes sure `next` is cleared and returns `true` if it actually canceled something
pub fn cancel(&mut self) -> bool {
if let Ok(mut next) = self.next.lock() {
if next.is_some() {
*next = None;
return true;
}
}
false
}
false
}
/// take out last finished job
pub fn take_last(&self) -> Option<J> {
self.last.lock().map_or(None, |mut last| last.take())
}
/// take out last finished job
pub fn take_last(&self) -> Option<J> {
if let Ok(mut last) = self.last.lock() {
last.take()
} else {
None
}
}
/// spawns `task` if nothing is running currently,
/// otherwise schedules as `next` overwriting if `next` was set before.
/// return `true` if the new task gets started right away.
pub fn spawn(&self, task: J) -> bool {
self.schedule_next(task);
self.check_for_job()
}
/// spawns `task` if nothing is running currently, otherwise schedules as `next` overwriting if `next` was set before
pub fn spawn(&mut self, task: J) -> bool {
self.schedule_next(task);
self.check_for_job()
}
///
pub fn progress(&self) -> Option<J::Progress> {
self.progress.read().ok().map(|d| (*d).clone())
}
fn check_for_job(&self) -> bool {
if self.is_pending() {
return false;
}
fn check_for_job(&self) -> bool {
if self.is_pending() {
return false;
}
if let Some(task) = self.take_next() {
let self_arc = self.clone();
if let Some(task) = self.take_next() {
let self_clone = (*self).clone();
rayon_core::spawn(move || {
if let Err(e) = self_clone.run_job(task) {
log::error!("async job error: {e}");
}
});
rayon_core::spawn(move || {
if let Err(e) = self_arc.run_job(task) {
log::error!("async job error: {}", e);
}
});
return true;
}
return true;
}
false
}
false
}
fn run_job(&self, mut task: J) -> Result<()> {
//limit the pending scope
{
let _pending = self.pending.lock()?;
fn run_job(&self, mut task: J) -> Result<()> {
//limit the pending scope
{
let _pending = self.pending.lock()?;
let notification = task.run(RunParams {
progress: self.progress.clone(),
sender: self.sender.clone(),
})?;
task.run();
if let Ok(mut last) = self.last.lock() {
*last = Some(task);
}
if let Ok(mut last) = self.last.lock() {
*last = Some(task);
}
self.sender.send(notification)?;
}
self.sender.send(self.notification)?;
}
self.check_for_job();
self.check_for_job();
Ok(())
}
Ok(())
}
fn schedule_next(&self, task: J) {
if let Ok(mut next) = self.next.lock() {
*next = Some(task);
}
}
fn schedule_next(&mut self, task: J) {
if let Ok(mut next) = self.next.lock() {
*next = Some(task);
}
}
fn take_next(&self) -> Option<J> {
self.next.lock().map_or(None, |mut next| next.take())
}
fn take_next(&self) -> Option<J> {
if let Ok(mut next) = self.next.lock() {
next.take()
} else {
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crossbeam_channel::unbounded;
use pretty_assertions::assert_eq;
use std::{
sync::atomic::{AtomicBool, AtomicU32, Ordering},
thread,
time::Duration,
};
use super::*;
use crossbeam_channel::unbounded;
use pretty_assertions::assert_eq;
use std::{
sync::atomic::AtomicU32, thread::sleep, time::Duration,
};
#[derive(Clone)]
struct TestJob {
v: Arc<AtomicU32>,
finish: Arc<AtomicBool>,
value_to_add: u32,
}
#[derive(Clone)]
struct TestJob {
v: Arc<AtomicU32>,
value_to_add: u32,
}
type TestNotification = ();
impl AsyncJob for TestJob {
fn run(&mut self) {
sleep(Duration::from_millis(100));
impl AsyncJob for TestJob {
type Notification = TestNotification;
type Progress = ();
self.v.fetch_add(
self.value_to_add,
std::sync::atomic::Ordering::Relaxed,
);
}
}
fn run(
&mut self,
_params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
println!("[job] wait");
type Notificaton = ();
while !self.finish.load(Ordering::SeqCst) {
std::thread::yield_now();
}
#[test]
fn test_overwrite() {
let (sender, receiver) = unbounded();
println!("[job] sleep");
let mut job: AsyncSingleJob<TestJob, Notificaton> =
AsyncSingleJob::new(sender, ());
thread::sleep(Duration::from_millis(100));
let task = TestJob {
v: Arc::new(AtomicU32::new(1)),
value_to_add: 1,
};
println!("[job] done sleeping");
assert!(job.spawn(task.clone()));
sleep(Duration::from_millis(1));
for _ in 0..5 {
assert!(!job.spawn(task.clone()));
}
let res =
self.v.fetch_add(self.value_to_add, Ordering::SeqCst);
let _foo = receiver.recv().unwrap();
let _foo = receiver.recv().unwrap();
assert!(receiver.is_empty());
println!("[job] value: {res}");
assert_eq!(
task.v.load(std::sync::atomic::Ordering::Relaxed),
3
);
}
Ok(())
}
}
#[test]
fn test_cancel() {
let (sender, receiver) = unbounded();
#[test]
fn test_overwrite() {
let (sender, receiver) = unbounded();
let mut job: AsyncSingleJob<TestJob, Notificaton> =
AsyncSingleJob::new(sender, ());
let job: AsyncSingleJob<TestJob> =
AsyncSingleJob::new(sender);
let task = TestJob {
v: Arc::new(AtomicU32::new(1)),
value_to_add: 1,
};
let task = TestJob {
v: Arc::new(AtomicU32::new(1)),
finish: Arc::new(AtomicBool::new(false)),
value_to_add: 1,
};
assert!(job.spawn(task.clone()));
sleep(Duration::from_millis(1));
assert!(job.spawn(task.clone()));
task.finish.store(true, Ordering::SeqCst);
thread::sleep(Duration::from_millis(10));
for _ in 0..5 {
assert!(!job.spawn(task.clone()));
}
assert!(job.cancel());
for _ in 0..5 {
println!("spawn");
assert!(!job.spawn(task.clone()));
}
let _foo = receiver.recv().unwrap();
println!("recv");
receiver.recv().unwrap();
receiver.recv().unwrap();
assert!(receiver.is_empty());
assert_eq!(
task.v.load(std::sync::atomic::Ordering::SeqCst),
3
);
}
fn wait_for_job(job: &AsyncSingleJob<TestJob>) {
while job.is_pending() {
thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn test_cancel() {
let (sender, receiver) = unbounded();
let job: AsyncSingleJob<TestJob> =
AsyncSingleJob::new(sender);
let task = TestJob {
v: Arc::new(AtomicU32::new(1)),
finish: Arc::new(AtomicBool::new(false)),
value_to_add: 1,
};
assert!(job.spawn(task.clone()));
task.finish.store(true, Ordering::SeqCst);
thread::sleep(Duration::from_millis(10));
for _ in 0..5 {
println!("spawn");
assert!(!job.spawn(task.clone()));
}
println!("cancel");
assert!(job.cancel());
task.finish.store(true, Ordering::SeqCst);
wait_for_job(&job);
println!("recv");
receiver.recv().unwrap();
println!("received");
assert_eq!(
task.v.load(std::sync::atomic::Ordering::SeqCst),
2
);
}
assert_eq!(
task.v.load(std::sync::atomic::Ordering::Relaxed),
2
);
}
}

View file

@ -1,188 +1,179 @@
use crate::{
error::Result,
hash,
sync::{self, CommitId, FileBlame, RepoPath},
AsyncGitNotification,
error::Result,
hash,
sync::{self, FileBlame},
AsyncGitNotification, CWD,
};
use crossbeam_channel::Sender;
use std::{
hash::Hash,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
hash::Hash,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
};
///
#[derive(Hash, Clone, PartialEq, Eq)]
#[derive(Hash, Clone, PartialEq)]
pub struct BlameParams {
/// path to the file to blame
pub file_path: String,
/// blame at a specific revision
pub commit_id: Option<CommitId>,
/// path to the file to blame
pub file_path: String,
}
struct Request<R, A>(R, Option<A>);
#[derive(Default, Clone)]
struct LastResult<P, R> {
params: P,
result: R,
params: P,
hash: u64,
result: R,
}
///
pub struct AsyncBlame {
current: Arc<Mutex<Request<u64, FileBlame>>>,
last: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
repo: RepoPath,
current: Arc<Mutex<Request<u64, FileBlame>>>,
last: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
}
impl AsyncBlame {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
Self {
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn last(&self) -> Result<Option<(BlameParams, FileBlame)>> {
let last = self.last.lock()?;
///
pub fn last(
&mut self,
) -> Result<Option<(BlameParams, FileBlame)>> {
let last = self.last.lock()?;
Ok(last.clone().map(|last_result| {
(last_result.params, last_result.result)
}))
}
Ok(last.clone().map(|last_result| {
(last_result.params, last_result.result)
}))
}
///
pub fn refresh(&self) -> Result<()> {
if let Ok(Some(param)) = self.get_last_param() {
self.clear_current()?;
self.request(param)?;
}
Ok(())
}
///
pub fn refresh(&mut self) -> Result<()> {
if let Ok(Some(param)) = self.get_last_param() {
self.clear_current()?;
self.request(param)?;
}
Ok(())
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn request(
&self,
params: BlameParams,
) -> Result<Option<FileBlame>> {
log::trace!("request");
///
pub fn request(
&mut self,
params: BlameParams,
) -> Result<Option<FileBlame>> {
log::trace!("request");
let hash = hash(&params);
let hash = hash(&params);
{
let mut current = self.current.lock()?;
{
let mut current = self.current.lock()?;
if current.0 == hash {
return Ok(current.1.clone());
}
if current.0 == hash {
return Ok(current.1.clone());
}
current.0 = hash;
current.1 = None;
}
current.0 = hash;
current.1 = None;
}
let arc_current = Arc::clone(&self.current);
let arc_last = Arc::clone(&self.last);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let repo = self.repo.clone();
let arc_current = Arc::clone(&self.current);
let arc_last = Arc::clone(&self.last);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
self.pending.fetch_add(1, Ordering::Relaxed);
self.pending.fetch_add(1, Ordering::Relaxed);
rayon_core::spawn(move || {
let notify = Self::get_blame_helper(
&repo,
params,
&arc_last,
&arc_current,
hash,
);
rayon_core::spawn(move || {
let notify = Self::get_blame_helper(
params,
&arc_last,
&arc_current,
hash,
);
let notify = match notify {
Err(err) => {
log::error!("get_blame_helper error: {err}");
true
}
Ok(notify) => notify,
};
let notify = match notify {
Err(err) => {
log::error!("get_blame_helper error: {}", err);
true
}
Ok(notify) => notify,
};
arc_pending.fetch_sub(1, Ordering::Relaxed);
arc_pending.fetch_sub(1, Ordering::Relaxed);
sender
.send(if notify {
AsyncGitNotification::Blame
} else {
AsyncGitNotification::FinishUnchanged
})
.expect("error sending blame");
});
sender
.send(if notify {
AsyncGitNotification::Blame
} else {
AsyncGitNotification::FinishUnchanged
})
.expect("error sending blame");
});
Ok(None)
}
Ok(None)
}
fn get_blame_helper(
repo_path: &RepoPath,
params: BlameParams,
arc_last: &Arc<
Mutex<Option<LastResult<BlameParams, FileBlame>>>,
>,
arc_current: &Arc<Mutex<Request<u64, FileBlame>>>,
hash: u64,
) -> Result<bool> {
let file_blame = sync::blame::blame_file(
repo_path,
&params.file_path,
params.commit_id,
)?;
fn get_blame_helper(
params: BlameParams,
arc_last: &Arc<
Mutex<Option<LastResult<BlameParams, FileBlame>>>,
>,
arc_current: &Arc<Mutex<Request<u64, FileBlame>>>,
hash: u64,
) -> Result<bool> {
let file_blame =
sync::blame::blame_file(CWD, &params.file_path)?;
let mut notify = false;
{
let mut current = arc_current.lock()?;
if current.0 == hash {
current.1 = Some(file_blame.clone());
notify = true;
}
}
let mut notify = false;
{
let mut current = arc_current.lock()?;
if current.0 == hash {
current.1 = Some(file_blame.clone());
notify = true;
}
}
{
let mut last = arc_last.lock()?;
*last = Some(LastResult {
result: file_blame,
params,
});
}
{
let mut last = arc_last.lock()?;
*last = Some(LastResult {
result: file_blame,
hash,
params,
});
}
Ok(notify)
}
Ok(notify)
}
fn get_last_param(&self) -> Result<Option<BlameParams>> {
Ok(self
.last
.lock()?
.clone()
.map(|last_result| last_result.params))
}
fn get_last_param(&self) -> Result<Option<BlameParams>> {
Ok(self
.last
.lock()?
.clone()
.map(|last_result| last_result.params))
}
fn clear_current(&self) -> Result<()> {
let mut current = self.current.lock()?;
current.0 = 0;
current.1 = None;
Ok(())
}
fn clear_current(&mut self) -> Result<()> {
let mut current = self.current.lock()?;
current.0 = 0;
current.1 = None;
Ok(())
}
}

View file

@ -1,77 +0,0 @@
use crate::{
asyncjob::{AsyncJob, RunParams},
error::Result,
sync::{branch::get_branches_info, BranchInfo, RepoPath},
AsyncGitNotification,
};
use std::sync::{Arc, Mutex};
enum JobState {
Request {
local_branches: bool,
repo: RepoPath,
},
Response(Result<Vec<BranchInfo>>),
}
///
#[derive(Clone, Default)]
pub struct AsyncBranchesJob {
state: Arc<Mutex<Option<JobState>>>,
}
///
impl AsyncBranchesJob {
///
pub fn new(repo: RepoPath, local_branches: bool) -> Self {
Self {
state: Arc::new(Mutex::new(Some(JobState::Request {
repo,
local_branches,
}))),
}
}
///
pub fn result(&self) -> Option<Result<Vec<BranchInfo>>> {
if let Ok(mut state) = self.state.lock() {
if let Some(state) = state.take() {
return match state {
JobState::Request { .. } => None,
JobState::Response(result) => Some(result),
};
}
}
None
}
}
impl AsyncJob for AsyncBranchesJob {
type Notification = AsyncGitNotification;
type Progress = ();
fn run(
&mut self,
_params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
if let Ok(mut state) = self.state.lock() {
*state = state.take().map(|state| match state {
JobState::Request {
local_branches,
repo,
} => {
let branches =
get_branches_info(&repo, local_branches);
JobState::Response(branches)
}
JobState::Response(result) => {
JobState::Response(result)
}
});
}
Ok(AsyncGitNotification::Branches)
}
}

View file

@ -1,47 +1,48 @@
use crate::{
error::Result,
sync::{self, branch::get_branch_name, RepoPathRef},
error::Result,
sync::{self, branch::get_branch_name},
};
use sync::Head;
///
pub struct BranchName {
last_result: Option<(Head, String)>,
repo: RepoPathRef,
last_result: Option<(Head, String)>,
repo_path: String,
}
impl BranchName {
///
pub const fn new(repo: RepoPathRef) -> Self {
Self {
repo,
last_result: None,
}
}
///
pub fn new(path: &str) -> Self {
Self {
repo_path: path.to_string(),
last_result: None,
}
}
///
pub fn lookup(&mut self) -> Result<String> {
let current_head = sync::get_head_tuple(&self.repo.borrow())?;
///
pub fn lookup(&mut self) -> Result<String> {
let current_head =
sync::get_head_tuple(self.repo_path.as_str())?;
if let Some((last_head, branch_name)) =
self.last_result.as_ref()
{
if *last_head == current_head {
return Ok(branch_name.clone());
}
}
if let Some((last_head, branch_name)) =
self.last_result.as_ref()
{
if *last_head == current_head {
return Ok(branch_name.clone());
}
}
self.fetch(current_head)
}
self.fetch(current_head)
}
///
pub fn last(&self) -> Option<String> {
self.last_result.as_ref().map(|last| last.1.clone())
}
///
pub fn last(&self) -> Option<String> {
self.last_result.as_ref().map(|last| last.1.clone())
}
fn fetch(&mut self, head: Head) -> Result<String> {
let name = get_branch_name(&self.repo.borrow())?;
self.last_result = Some((head, name.clone()));
Ok(name)
}
fn fetch(&mut self, head: Head) -> Result<String> {
let name = get_branch_name(self.repo_path.as_str())?;
self.last_result = Some((head, name.clone()));
Ok(name)
}
}

View file

@ -1,146 +1,105 @@
use crate::{
error::Result,
sync::{self, commit_files::OldNew, CommitId, RepoPath},
AsyncGitNotification, StatusItem,
error::Result,
sync::{self, CommitId},
AsyncGitNotification, StatusItem, CWD,
};
use crossbeam_channel::Sender;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
};
type ResultType = Vec<StatusItem>;
struct Request<R, A>(R, A);
///
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct CommitFilesParams {
///
pub id: CommitId,
///
pub other: Option<CommitId>,
}
impl From<CommitId> for CommitFilesParams {
fn from(id: CommitId) -> Self {
Self { id, other: None }
}
}
impl From<(CommitId, CommitId)> for CommitFilesParams {
fn from((id, other): (CommitId, CommitId)) -> Self {
Self {
id,
other: Some(other),
}
}
}
impl From<OldNew<CommitId>> for CommitFilesParams {
fn from(old_new: OldNew<CommitId>) -> Self {
Self {
id: old_new.new,
other: Some(old_new.old),
}
}
}
///
pub struct AsyncCommitFiles {
current:
Arc<Mutex<Option<Request<CommitFilesParams, ResultType>>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
repo: RepoPath,
current: Arc<Mutex<Option<Request<CommitId, ResultType>>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
}
impl AsyncCommitFiles {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
current: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
Self {
current: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn current(
&self,
) -> Result<Option<(CommitFilesParams, ResultType)>> {
let c = self.current.lock()?;
///
pub fn current(
&mut self,
) -> Result<Option<(CommitId, ResultType)>> {
let c = self.current.lock()?;
c.as_ref()
.map_or(Ok(None), |c| Ok(Some((c.0, c.1.clone()))))
}
c.as_ref()
.map_or(Ok(None), |c| Ok(Some((c.0, c.1.clone()))))
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn fetch(&self, params: CommitFilesParams) -> Result<()> {
if self.is_pending() {
return Ok(());
}
///
pub fn fetch(&mut self, id: CommitId) -> Result<()> {
if self.is_pending() {
return Ok(());
}
log::trace!("request: {params:?}");
log::trace!("request: {}", id.to_string());
{
let current = self.current.lock()?;
if let Some(c) = &*current {
if c.0 == params {
return Ok(());
}
}
}
{
let current = self.current.lock()?;
if let Some(c) = &*current {
if c.0 == id {
return Ok(());
}
}
}
let arc_current = Arc::clone(&self.current);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let repo = self.repo.clone();
let arc_current = Arc::clone(&self.current);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
self.pending.fetch_add(1, Ordering::Relaxed);
self.pending.fetch_add(1, Ordering::Relaxed);
rayon_core::spawn(move || {
Self::fetch_helper(&repo, params, &arc_current)
.expect("failed to fetch");
rayon_core::spawn(move || {
Self::fetch_helper(id, &arc_current)
.expect("failed to fetch");
arc_pending.fetch_sub(1, Ordering::Relaxed);
arc_pending.fetch_sub(1, Ordering::Relaxed);
sender
.send(AsyncGitNotification::CommitFiles)
.expect("error sending");
});
sender
.send(AsyncGitNotification::CommitFiles)
.expect("error sending");
});
Ok(())
}
Ok(())
}
fn fetch_helper(
repo_path: &RepoPath,
params: CommitFilesParams,
arc_current: &Arc<
Mutex<Option<Request<CommitFilesParams, ResultType>>>,
>,
) -> Result<()> {
let res = sync::get_commit_files(
repo_path,
params.id,
params.other,
)?;
fn fetch_helper(
id: CommitId,
arc_current: &Arc<
Mutex<Option<Request<CommitId, ResultType>>>,
>,
) -> Result<()> {
let res = sync::get_commit_files(CWD, id)?;
log::trace!("get_commit_files: {:?} ({})", params, res.len());
log::trace!(
"get_commit_files: {} ({})",
id.to_string(),
res.len()
);
{
let mut current = arc_current.lock()?;
*current = Some(Request(params, res));
}
{
let mut current = arc_current.lock()?;
*current = Some(Request(id, res));
}
Ok(())
}
Ok(())
}
}

View file

@ -1,221 +1,195 @@
use crate::{
error::Result,
hash,
sync::{
self, commit_files::OldNew, diff::DiffOptions, CommitId,
RepoPath,
},
AsyncGitNotification, FileDiff,
error::Result,
hash,
sync::{self, CommitId},
AsyncGitNotification, FileDiff, CWD,
};
use crossbeam_channel::Sender;
use std::{
hash::Hash,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
hash::Hash,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
};
///
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
#[derive(Hash, Clone, PartialEq)]
pub enum DiffType {
/// diff two commits
Commits(OldNew<CommitId>),
/// diff in a given commit
Commit(CommitId),
/// diff against staged file
Stage,
/// diff against file in workdir
WorkDir,
/// diff in a given commit
Commit(CommitId),
/// diff against staged file
Stage,
/// diff against file in workdir
WorkDir,
}
///
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
#[derive(Hash, Clone, PartialEq)]
pub struct DiffParams {
/// path to the file to diff
pub path: String,
/// what kind of diff
pub diff_type: DiffType,
/// diff options
pub options: DiffOptions,
/// path to the file to diff
pub path: String,
/// what kind of diff
pub diff_type: DiffType,
}
struct Request<R, A>(R, Option<A>);
#[derive(Default, Clone)]
struct LastResult<P, R> {
params: P,
result: R,
params: P,
hash: u64,
result: R,
}
///
pub struct AsyncDiff {
current: Arc<Mutex<Request<u64, FileDiff>>>,
last: Arc<Mutex<Option<LastResult<DiffParams, FileDiff>>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
repo: RepoPath,
current: Arc<Mutex<Request<u64, FileDiff>>>,
last: Arc<Mutex<Option<LastResult<DiffParams, FileDiff>>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
}
impl AsyncDiff {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
Self {
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn last(&self) -> Result<Option<(DiffParams, FileDiff)>> {
let last = self.last.lock()?;
///
pub fn last(&mut self) -> Result<Option<(DiffParams, FileDiff)>> {
let last = self.last.lock()?;
Ok(last.clone().map(|res| (res.params, res.result)))
}
Ok(last.clone().map(|res| (res.params, res.result)))
}
///
pub fn refresh(&self) -> Result<()> {
if let Ok(Some(param)) = self.get_last_param() {
self.clear_current()?;
self.request(param)?;
}
Ok(())
}
///
pub fn refresh(&mut self) -> Result<()> {
if let Ok(Some(param)) = self.get_last_param() {
self.clear_current()?;
self.request(param)?;
}
Ok(())
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn request(
&self,
params: DiffParams,
) -> Result<Option<FileDiff>> {
log::trace!("request {params:?}");
///
pub fn request(
&mut self,
params: DiffParams,
) -> Result<Option<FileDiff>> {
log::trace!("request");
let hash = hash(&params);
let hash = hash(&params);
{
let mut current = self.current.lock()?;
{
let mut current = self.current.lock()?;
if current.0 == hash {
return Ok(current.1.clone());
}
if current.0 == hash {
return Ok(current.1.clone());
}
current.0 = hash;
current.1 = None;
}
current.0 = hash;
current.1 = None;
}
let arc_current = Arc::clone(&self.current);
let arc_last = Arc::clone(&self.last);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let repo = self.repo.clone();
let arc_current = Arc::clone(&self.current);
let arc_last = Arc::clone(&self.last);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
self.pending.fetch_add(1, Ordering::Relaxed);
self.pending.fetch_add(1, Ordering::Relaxed);
rayon_core::spawn(move || {
let notify = Self::get_diff_helper(
&repo,
params,
&arc_last,
&arc_current,
hash,
);
rayon_core::spawn(move || {
let notify = Self::get_diff_helper(
params,
&arc_last,
&arc_current,
hash,
);
let notify = match notify {
Err(e) => {
log::error!("get_diff_helper error: {e}");
true
}
Ok(notify) => notify,
};
let notify = match notify {
Err(err) => {
log::error!("get_diff_helper error: {}", err);
true
}
Ok(notify) => notify,
};
arc_pending.fetch_sub(1, Ordering::Relaxed);
arc_pending.fetch_sub(1, Ordering::Relaxed);
sender
.send(if notify {
AsyncGitNotification::Diff
} else {
AsyncGitNotification::FinishUnchanged
})
.expect("error sending diff");
});
sender
.send(if notify {
AsyncGitNotification::Diff
} else {
AsyncGitNotification::FinishUnchanged
})
.expect("error sending diff");
});
Ok(None)
}
Ok(None)
}
fn get_diff_helper(
repo_path: &RepoPath,
params: DiffParams,
arc_last: &Arc<
Mutex<Option<LastResult<DiffParams, FileDiff>>>,
>,
arc_current: &Arc<Mutex<Request<u64, FileDiff>>>,
hash: u64,
) -> Result<bool> {
let res = match params.diff_type {
DiffType::Stage => sync::diff::get_diff(
repo_path,
&params.path,
true,
Some(params.options),
)?,
DiffType::WorkDir => sync::diff::get_diff(
repo_path,
&params.path,
false,
Some(params.options),
)?,
DiffType::Commit(id) => sync::diff::get_diff_commit(
repo_path,
id,
params.path.clone(),
Some(params.options),
)?,
DiffType::Commits(ids) => sync::diff::get_diff_commits(
repo_path,
ids,
params.path.clone(),
Some(params.options),
)?,
};
fn get_diff_helper(
params: DiffParams,
arc_last: &Arc<
Mutex<Option<LastResult<DiffParams, FileDiff>>>,
>,
arc_current: &Arc<Mutex<Request<u64, FileDiff>>>,
hash: u64,
) -> Result<bool> {
let res = match params.diff_type {
DiffType::Stage => {
sync::diff::get_diff(CWD, &params.path, true)?
}
DiffType::WorkDir => {
sync::diff::get_diff(CWD, &params.path, false)?
}
DiffType::Commit(id) => sync::diff::get_diff_commit(
CWD,
id,
params.path.clone(),
)?,
};
let mut notify = false;
{
let mut current = arc_current.lock()?;
if current.0 == hash {
current.1 = Some(res.clone());
notify = true;
}
}
let mut notify = false;
{
let mut current = arc_current.lock()?;
if current.0 == hash {
current.1 = Some(res.clone());
notify = true;
}
}
{
let mut last = arc_last.lock()?;
*last = Some(LastResult {
result: res,
params,
});
}
{
let mut last = arc_last.lock()?;
*last = Some(LastResult {
result: res,
hash,
params,
});
}
Ok(notify)
}
Ok(notify)
}
fn get_last_param(&self) -> Result<Option<DiffParams>> {
Ok(self.last.lock()?.clone().map(|e| e.params))
}
fn get_last_param(&self) -> Result<Option<DiffParams>> {
Ok(self.last.lock()?.clone().map(|e| e.params))
}
fn clear_current(&self) -> Result<()> {
let mut current = self.current.lock()?;
current.0 = 0;
current.1 = None;
Ok(())
}
fn clear_current(&mut self) -> Result<()> {
let mut current = self.current.lock()?;
current.0 = 0;
current.1 = None;
Ok(())
}
}

View file

@ -1,350 +1,60 @@
use std::{
num::TryFromIntError, path::StripPrefixError,
string::FromUtf8Error,
};
#![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)]
use std::{num::TryFromIntError, string::FromUtf8Error};
use thiserror::Error;
///
#[derive(Error, Debug)]
pub enum GixError {
///
#[error("gix::discover error: {0}")]
Discover(#[from] Box<gix::discover::Error>),
///
#[error("gix::head::peel::to_commit error: {0}")]
HeadPeelToCommit(#[from] gix::head::peel::to_commit::Error),
///
#[error("gix::object::find::existing::with_conversion::Error error: {0}")]
ObjectFindExistingWithConversion(
#[from] gix::object::find::existing::with_conversion::Error,
),
///
#[error("gix::objs::decode::Error error: {0}")]
ObjsDecode(#[from] gix::objs::decode::Error),
///
#[error("gix::pathspec::init::Error error: {0}")]
PathspecInit(#[from] Box<gix::pathspec::init::Error>),
///
#[error("gix::reference::find::existing error: {0}")]
ReferenceFindExisting(
#[from] gix::reference::find::existing::Error,
),
///
#[error("gix::reference::head_tree_id::Error error: {0}")]
ReferenceHeadTreeId(#[from] gix::reference::head_tree_id::Error),
///
#[error("gix::reference::iter::Error error: {0}")]
ReferenceIter(#[from] gix::reference::iter::Error),
///
#[error("gix::reference::iter::init::Error error: {0}")]
ReferenceIterInit(#[from] gix::reference::iter::init::Error),
///
#[error("gix::revision::walk error: {0}")]
RevisionWalk(#[from] gix::revision::walk::Error),
///
#[error("gix::status::Error error: {0}")]
Status(#[from] Box<gix::status::Error>),
///
#[error("gix::status::index_worktree::Error error: {0}")]
StatusIndexWorktree(
#[from] Box<gix::status::index_worktree::Error>,
),
///
#[error("gix::status::into_iter::Error error: {0}")]
StatusIntoIter(#[from] Box<gix::status::into_iter::Error>),
///
#[error("gix::status::iter::Error error: {0}")]
StatusIter(#[from] Box<gix::status::iter::Error>),
///
#[error("gix::status::tree_index::Error error: {0}")]
StatusTreeIndex(#[from] Box<gix::status::tree_index::Error>),
///
#[error("gix::worktree::open_index::Error error: {0}")]
WorktreeOpenIndex(#[from] Box<gix::worktree::open_index::Error>),
}
///
#[derive(Error, Debug)]
pub enum Error {
///
#[error("`{0}`")]
Generic(String),
#[error("`{0}`")]
Generic(String),
///
#[error("git: no head found")]
NoHead,
#[error("git: no head found")]
NoHead,
///
#[error("git: conflict during rebase")]
RebaseConflict,
#[error("git: remote url not found")]
UnknownRemote,
///
#[error("git: remote url not found")]
UnknownRemote,
#[error("git: inconclusive remotes")]
NoDefaultRemoteFound,
///
#[error("git: inconclusive remotes")]
NoDefaultRemoteFound,
#[error("git: work dir error")]
NoWorkDir,
///
#[error("git: work dir error")]
NoWorkDir,
#[error("git: uncommitted changes")]
UncommittedChanges,
///
#[error("git: uncommitted changes")]
UncommittedChanges,
#[error("git: can\u{2019}t run blame on a binary file")]
NoBlameOnBinaryFile,
///
#[error("git: can\u{2019}t run blame on a binary file")]
NoBlameOnBinaryFile,
#[error("binary file")]
BinaryFile,
///
#[error("binary file")]
BinaryFile,
#[error("io error:{0}")]
Io(#[from] std::io::Error),
///
#[error("io error:{0}")]
Io(#[from] std::io::Error),
#[error("git error:{0}")]
Git(#[from] git2::Error),
///
#[error("git error:{0}")]
Git(#[from] git2::Error),
#[error("utf8 error:{0}")]
Utf8Conversion(#[from] FromUtf8Error),
///
#[error("git config error: {0}")]
GitConfig(String),
#[error("TryFromInt error:{0}")]
IntConversion(#[from] TryFromIntError),
///
#[error("strip prefix error: {0}")]
StripPrefix(#[from] StripPrefixError),
///
#[error("utf8 error:{0}")]
Utf8Conversion(#[from] FromUtf8Error),
///
#[error("TryFromInt error:{0}")]
IntConversion(#[from] TryFromIntError),
///
#[error("EasyCast error:{0}")]
EasyCast(#[from] easy_cast::Error),
///
#[error("no parent of commit found")]
NoParent,
///
#[error("not on a branch")]
NoBranch,
///
#[error("rayon error: {0}")]
ThreadPool(#[from] rayon_core::ThreadPoolBuildError),
///
#[error("git hook error: {0}")]
Hooks(#[from] git2_hooks::HooksError),
///
#[error("sign builder error: {0}")]
SignBuilder(#[from] crate::sync::sign::SignBuilderError),
///
#[error("sign error: {0}")]
Sign(#[from] crate::sync::sign::SignError),
///
#[error("gix error:{0}")]
Gix(#[from] GixError),
///
#[error("amend error: config commit.gpgsign=true detected.\ngpg signing is not supported for amending non-last commits")]
SignAmendNonLastCommit,
///
#[error("reword error: config commit.gpgsign=true detected.\ngpg signing is not supported for rewording non-last commits")]
SignRewordNonLastCommit,
///
#[error("reword error: config commit.gpgsign=true detected.\ngpg signing is not supported for rewording commits with staged changes\ntry unstaging or stashing your changes")]
SignRewordLastCommitStaged,
#[error("EasyCast error:{0}")]
EasyCast(#[from] easy_cast::Error),
}
///
pub type Result<T> = std::result::Result<T, Error>;
impl<T> From<std::sync::PoisonError<T>> for Error {
fn from(error: std::sync::PoisonError<T>) -> Self {
Self::Generic(format!("poison error: {error}"))
}
fn from(error: std::sync::PoisonError<T>) -> Self {
Self::Generic(format!("poison error: {}", error))
}
}
impl<T> From<crossbeam_channel::SendError<T>> for Error {
fn from(error: crossbeam_channel::SendError<T>) -> Self {
Self::Generic(format!("send error: {error}"))
}
}
impl From<gix::discover::Error> for GixError {
fn from(error: gix::discover::Error) -> Self {
Self::Discover(Box::new(error))
}
}
impl From<gix::discover::Error> for Error {
fn from(error: gix::discover::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::head::peel::to_commit::Error> for Error {
fn from(error: gix::head::peel::to_commit::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::object::find::existing::with_conversion::Error>
for Error
{
fn from(
error: gix::object::find::existing::with_conversion::Error,
) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::objs::decode::Error> for Error {
fn from(error: gix::objs::decode::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::pathspec::init::Error> for GixError {
fn from(error: gix::pathspec::init::Error) -> Self {
Self::PathspecInit(Box::new(error))
}
}
impl From<gix::pathspec::init::Error> for Error {
fn from(error: gix::pathspec::init::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::reference::find::existing::Error> for Error {
fn from(error: gix::reference::find::existing::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::reference::head_tree_id::Error> for Error {
fn from(error: gix::reference::head_tree_id::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::reference::iter::Error> for Error {
fn from(error: gix::reference::iter::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::reference::iter::init::Error> for Error {
fn from(error: gix::reference::iter::init::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::revision::walk::Error> for Error {
fn from(error: gix::revision::walk::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::status::Error> for GixError {
fn from(error: gix::status::Error) -> Self {
Self::Status(Box::new(error))
}
}
impl From<gix::status::Error> for Error {
fn from(error: gix::status::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::status::iter::Error> for GixError {
fn from(error: gix::status::iter::Error) -> Self {
Self::StatusIter(Box::new(error))
}
}
impl From<gix::status::iter::Error> for Error {
fn from(error: gix::status::iter::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::status::into_iter::Error> for GixError {
fn from(error: gix::status::into_iter::Error) -> Self {
Self::StatusIntoIter(Box::new(error))
}
}
impl From<gix::status::into_iter::Error> for Error {
fn from(error: gix::status::into_iter::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::status::index_worktree::Error> for GixError {
fn from(error: gix::status::index_worktree::Error) -> Self {
Self::StatusIndexWorktree(Box::new(error))
}
}
impl From<gix::status::index_worktree::Error> for Error {
fn from(error: gix::status::index_worktree::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::status::tree_index::Error> for GixError {
fn from(error: gix::status::tree_index::Error) -> Self {
Self::StatusTreeIndex(Box::new(error))
}
}
impl From<gix::status::tree_index::Error> for Error {
fn from(error: gix::status::tree_index::Error) -> Self {
Self::Gix(GixError::from(error))
}
}
impl From<gix::worktree::open_index::Error> for GixError {
fn from(error: gix::worktree::open_index::Error) -> Self {
Self::WorktreeOpenIndex(Box::new(error))
}
}
impl From<gix::worktree::open_index::Error> for Error {
fn from(error: gix::worktree::open_index::Error) -> Self {
Self::Gix(GixError::from(error))
}
fn from(error: crossbeam_channel::SendError<T>) -> Self {
Self::Generic(format!("send error: {}", error))
}
}

159
asyncgit/src/fetch.rs Normal file
View file

@ -0,0 +1,159 @@
use crate::{
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::{fetch, push::ProgressNotification},
},
AsyncGitNotification, RemoteProgress, CWD,
};
use crossbeam_channel::{unbounded, Sender};
use std::{
sync::{Arc, Mutex},
thread,
};
///
#[derive(Default, Clone, Debug)]
pub struct FetchRequest {
///
pub remote: String,
///
pub branch: String,
///
pub basic_credential: Option<BasicAuthCredential>,
}
#[derive(Default, Clone, Debug)]
struct FetchState {
request: FetchRequest,
}
///
pub struct AsyncFetch {
state: Arc<Mutex<Option<FetchState>>>,
last_result: Arc<Mutex<Option<(usize, String)>>>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
sender: Sender<AsyncGitNotification>,
}
impl AsyncFetch {
///
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
Self {
state: Arc::new(Mutex::new(None)),
last_result: Arc::new(Mutex::new(None)),
progress: Arc::new(Mutex::new(None)),
sender: sender.clone(),
}
}
///
pub fn is_pending(&self) -> Result<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn last_result(&self) -> Result<Option<(usize, String)>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
let res = self.progress.lock()?;
Ok(res.as_ref().map(|progress| progress.clone().into()))
}
///
pub fn request(&mut self, params: FetchRequest) -> Result<()> {
log::trace!("request");
if self.is_pending()? {
return Ok(());
}
self.set_request(&params)?;
RemoteProgress::set_progress(&self.progress, None)?;
let arc_state = Arc::clone(&self.state);
let arc_res = Arc::clone(&self.last_result);
let arc_progress = Arc::clone(&self.progress);
let sender = self.sender.clone();
thread::spawn(move || {
let (progress_sender, receiver) = unbounded();
let handle = RemoteProgress::spawn_receiver_thread(
AsyncGitNotification::Fetch,
sender.clone(),
receiver,
arc_progress,
);
let res = fetch(
CWD,
&params.branch,
params.basic_credential,
Some(progress_sender.clone()),
);
progress_sender
.send(ProgressNotification::Done)
.expect("closing send failed");
handle.join().expect("joining thread failed");
Self::set_result(&arc_res, res).expect("result error");
Self::clear_request(&arc_state).expect("clear error");
sender
.send(AsyncGitNotification::Fetch)
.expect("AsyncNotification error");
});
Ok(())
}
fn set_request(&self, params: &FetchRequest) -> Result<()> {
let mut state = self.state.lock()?;
if state.is_some() {
return Err(Error::Generic("pending request".into()));
}
*state = Some(FetchState {
request: params.clone(),
});
Ok(())
}
fn clear_request(
state: &Arc<Mutex<Option<FetchState>>>,
) -> Result<()> {
let mut state = state.lock()?;
*state = None;
Ok(())
}
fn set_result(
arc_result: &Arc<Mutex<Option<(usize, String)>>>,
res: Result<usize>,
) -> Result<()> {
let mut last_res = arc_result.lock()?;
*last_res = match res {
Ok(bytes) => Some((bytes, String::new())),
Err(e) => {
log::error!("fetch error: {}", e);
Some((0, e.to_string()))
}
};
Ok(())
}
}

View file

@ -1,69 +0,0 @@
//!
use crate::{
asyncjob::{AsyncJob, RunParams},
error::Result,
sync::remotes::fetch_all,
sync::{cred::BasicAuthCredential, RepoPath},
AsyncGitNotification, ProgressPercent,
};
use std::sync::{Arc, Mutex};
enum JobState {
Request(Option<BasicAuthCredential>),
Response(Result<()>),
}
///
#[derive(Clone)]
pub struct AsyncFetchJob {
state: Arc<Mutex<Option<JobState>>>,
repo: RepoPath,
}
///
impl AsyncFetchJob {
///
pub fn new(
repo: RepoPath,
basic_credential: Option<BasicAuthCredential>,
) -> Self {
Self {
repo,
state: Arc::new(Mutex::new(Some(JobState::Request(
basic_credential,
)))),
}
}
}
impl AsyncJob for AsyncFetchJob {
type Notification = AsyncGitNotification;
type Progress = ProgressPercent;
fn run(
&mut self,
_params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
if let Ok(mut state) = self.state.lock() {
*state = state.take().map(|state| match state {
JobState::Request(basic_credentials) => {
//TODO: support progress
let result = fetch_all(
&self.repo,
&basic_credentials,
&None,
);
JobState::Response(result)
}
JobState::Response(result) => {
JobState::Response(result)
}
});
}
Ok(AsyncGitNotification::Fetch)
}
}

View file

@ -1,200 +0,0 @@
use rayon::{
prelude::ParallelIterator,
slice::{ParallelSlice, ParallelSliceMut},
};
use crate::{
asyncjob::{AsyncJob, RunParams},
error::Result,
sync::{self, CommitId, RepoPath, SharedCommitFilterFn},
AsyncGitNotification, ProgressPercent,
};
use std::{
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex,
},
time::{Duration, Instant},
};
///
pub struct CommitFilterResult {
///
pub result: Vec<CommitId>,
///
pub duration: Duration,
}
enum JobState {
Request {
commits: Vec<CommitId>,
repo_path: RepoPath,
},
Response(Result<CommitFilterResult>),
}
///
#[derive(Clone)]
pub struct AsyncCommitFilterJob {
state: Arc<Mutex<Option<JobState>>>,
filter: SharedCommitFilterFn,
cancellation_flag: Arc<AtomicBool>,
}
///
impl AsyncCommitFilterJob {
///
pub fn new(
repo_path: RepoPath,
commits: Vec<CommitId>,
filter: SharedCommitFilterFn,
cancellation_flag: Arc<AtomicBool>,
) -> Self {
Self {
state: Arc::new(Mutex::new(Some(JobState::Request {
repo_path,
commits,
}))),
filter,
cancellation_flag,
}
}
///
pub fn result(&self) -> Option<Result<CommitFilterResult>> {
if let Ok(mut state) = self.state.lock() {
if let Some(state) = state.take() {
return match state {
JobState::Request { .. } => None,
JobState::Response(result) => Some(result),
};
}
}
None
}
fn run_request(
&self,
repo_path: &RepoPath,
commits: Vec<CommitId>,
params: &RunParams<AsyncGitNotification, ProgressPercent>,
) -> JobState {
let result = self
.filter_commits(repo_path, commits, params)
.map(|(start, result)| CommitFilterResult {
result,
duration: start.elapsed(),
});
JobState::Response(result)
}
fn filter_commits(
&self,
repo_path: &RepoPath,
commits: Vec<CommitId>,
params: &RunParams<AsyncGitNotification, ProgressPercent>,
) -> Result<(Instant, Vec<CommitId>)> {
scopetime::scope_time!("filter_commits");
let total_amount = commits.len();
let start = Instant::now();
//note: for some reason >4 threads degrades search performance
let pool =
rayon::ThreadPoolBuilder::new().num_threads(4).build()?;
let idx = AtomicUsize::new(0);
let mut result = pool.install(|| {
commits
.into_iter()
.enumerate()
.collect::<Vec<(usize, CommitId)>>()
.par_chunks(1000)
.filter_map(|c| {
//TODO: error log repo open errors
sync::repo(repo_path).ok().map(|repo| {
c.iter()
.filter_map(|(e, c)| {
let idx = idx.fetch_add(
1,
std::sync::atomic::Ordering::Relaxed,
);
if self
.cancellation_flag
.load(Ordering::Relaxed)
{
return None;
}
Self::update_progress(
params,
ProgressPercent::new(
idx,
total_amount,
),
);
(*self.filter)(&repo, c)
.ok()
.and_then(|res| {
res.then_some((*e, *c))
})
})
.collect::<Vec<_>>()
})
})
.flatten()
.collect::<Vec<_>>()
});
result.par_sort_by(|a, b| a.0.cmp(&b.0));
let result = result.into_iter().map(|c| c.1).collect();
Ok((start, result))
}
fn update_progress(
params: &RunParams<AsyncGitNotification, ProgressPercent>,
new_progress: ProgressPercent,
) {
match params.set_progress(new_progress) {
Err(e) => log::error!("progress error: {e}"),
Ok(result) if result => {
if let Err(e) =
params.send(AsyncGitNotification::CommitFilter)
{
log::error!("send error: {e}");
}
}
_ => (),
}
}
}
impl AsyncJob for AsyncCommitFilterJob {
type Notification = AsyncGitNotification;
type Progress = ProgressPercent;
fn run(
&mut self,
params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
if let Ok(mut state) = self.state.lock() {
*state = state.take().map(|state| match state {
JobState::Request { commits, repo_path } => {
self.run_request(&repo_path, commits, &params)
}
JobState::Response(result) => {
JobState::Response(result)
}
});
}
Ok(AsyncGitNotification::CommitFilter)
}
}

View file

@ -1,59 +1,35 @@
/*!
`AsyncGit` is a library that provides non-blocking access to Git
operations, enabling `GitUI` to perform potentially slow Git operations
in the background while keeping the user interface responsive.
It also provides synchronous Git operations.
It wraps libraries like git2 and gix.
*/
//! asyncgit
#![forbid(missing_docs)]
#![deny(
mismatched_lifetime_syntaxes,
unused_imports,
unused_must_use,
dead_code,
unstable_name_collisions,
unused_assignments,
deprecated
unused_imports,
unused_must_use,
dead_code,
unstable_name_collisions,
unused_assignments
)]
#![deny(unstable_name_collisions)]
#![deny(clippy::all, clippy::perf, clippy::nursery, clippy::pedantic)]
#![deny(
clippy::filetype_is_file,
clippy::cargo,
clippy::unwrap_used,
clippy::panic,
clippy::match_like_matches_macro,
clippy::needless_update
//TODO: get this in someday since expect still leads us to crashes sometimes
// clippy::expect_used
)]
#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::missing_errors_doc,
clippy::empty_docs,
clippy::unnecessary_debug_formatting
)]
//TODO:
#![allow(
clippy::significant_drop_tightening,
clippy::missing_panics_doc,
clippy::multiple_crate_versions
)]
#![deny(clippy::filetype_is_file)]
#![deny(clippy::cargo)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
#![deny(clippy::match_like_matches_macro)]
#![deny(clippy::needless_update)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::missing_errors_doc)]
//TODO: get this in someday since expect still leads us to crashes sometimes
// #![deny(clippy::expect_used)]
pub mod asyncjob;
mod blame;
mod branches;
pub mod cached;
mod commit_files;
mod diff;
mod error;
mod fetch_job;
mod filter_commits;
mod fetch;
mod progress;
mod pull;
mod push;
mod push_tags;
pub mod remote_progress;
@ -62,92 +38,65 @@ mod revlog;
mod status;
pub mod sync;
mod tags;
mod treefiles;
pub use crate::{
blame::{AsyncBlame, BlameParams},
branches::AsyncBranchesJob,
commit_files::{AsyncCommitFiles, CommitFilesParams},
diff::{AsyncDiff, DiffParams, DiffType},
error::{Error, Result},
fetch_job::AsyncFetchJob,
filter_commits::{AsyncCommitFilterJob, CommitFilterResult},
progress::ProgressPercent,
pull::{AsyncPull, FetchRequest},
push::{AsyncPush, PushRequest},
push_tags::{AsyncPushTags, PushTagsRequest},
remote_progress::{RemoteProgress, RemoteProgressState},
revlog::{AsyncLog, FetchStatus},
status::{AsyncStatus, StatusParams},
sync::{
diff::{DiffLine, DiffLineType, FileDiff},
remotes::push::PushType,
status::{StatusItem, StatusItemType},
},
tags::AsyncTags,
treefiles::AsyncTreeFilesJob,
blame::{AsyncBlame, BlameParams},
commit_files::AsyncCommitFiles,
diff::{AsyncDiff, DiffParams, DiffType},
fetch::{AsyncFetch, FetchRequest},
push::{AsyncPush, PushRequest},
push_tags::{AsyncPushTags, PushTagsRequest},
remote_progress::{RemoteProgress, RemoteProgressState},
revlog::{AsyncLog, FetchStatus},
status::{AsyncStatus, StatusParams},
sync::{
diff::{DiffLine, DiffLineType, FileDiff},
status::{StatusItem, StatusItemType},
},
tags::AsyncTags,
};
pub use git2::message_prettify;
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
/// this type is used to communicate events back through the channel
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum AsyncGitNotification {
/// this indicates that no new state was fetched but that a async process finished
FinishUnchanged,
///
Status,
///
Diff,
///
Log,
///
FileLog,
///
CommitFiles,
///
Tags,
///
Push,
///
PushTags,
///
Pull,
///
Blame,
///
RemoteTags,
///
Fetch,
///
Branches,
///
TreeFiles,
///
CommitFilter,
/// this indicates that no new state was fetched but that a async process finished
FinishUnchanged,
///
Status,
///
Diff,
///
Log,
///
CommitFiles,
///
Tags,
///
Push,
///
PushTags,
///
Fetch,
///
Blame,
///
//TODO: this does not belong here
SyntaxHighlighting,
///
//TODO: this does not belong here
RemoteTags,
}
/// current working directory `./`
pub static CWD: &str = "./";
/// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait
pub fn hash<T: Hash + ?Sized>(v: &T) -> u64 {
let mut hasher = DefaultHasher::new();
v.hash(&mut hasher);
hasher.finish()
}
///
#[cfg(feature = "trace-libgit")]
pub fn register_tracing_logging() -> bool {
fn git_trace(level: git2::TraceLevel, msg: &[u8]) {
log::info!("[{:?}]: {}", level, String::from_utf8_lossy(msg));
}
git2::trace_set(git2::TraceLevel::Trace, git_trace).is_ok()
}
///
#[cfg(not(feature = "trace-libgit"))]
pub fn register_tracing_logging() -> bool {
true
let mut hasher = DefaultHasher::new();
v.hash(&mut hasher);
hasher.finish()
}

View file

@ -4,51 +4,45 @@ use easy_cast::{Conv, ConvFloat};
use std::cmp;
///
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
#[derive(Clone, Debug)]
pub struct ProgressPercent {
/// percent 0..100
pub progress: u8,
/// percent 0..100
pub progress: u8,
}
impl ProgressPercent {
///
pub fn new(current: usize, total: usize) -> Self {
let total = f64::conv(cmp::max(current, total));
let progress = f64::conv(current) / total * 100.0;
let progress = u8::try_conv_nearest(progress).unwrap_or(100);
Self { progress }
}
///
pub const fn empty() -> Self {
Self { progress: 0 }
}
///
pub const fn full() -> Self {
Self { progress: 100 }
}
///
pub fn new(current: usize, total: usize) -> Self {
let total = f64::conv(cmp::max(current, total));
let progress = f64::conv(current) / total * 100.0;
let progress = u8::conv_nearest(progress);
Self { progress }
}
///
pub const fn empty() -> Self {
Self { progress: 0 }
}
///
pub const fn full() -> Self {
Self { progress: 100 }
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_progress_zero_total() {
let prog = ProgressPercent::new(1, 0);
#[test]
fn test_progress_zero_total() {
let prog = ProgressPercent::new(1, 0);
assert_eq!(prog.progress, 100);
}
assert_eq!(prog.progress, 100);
}
#[test]
fn test_progress_zero_all() {
let prog = ProgressPercent::new(0, 0);
assert_eq!(prog.progress, 100);
}
#[test]
fn test_progress_rounding() {
let prog = ProgressPercent::new(2, 10);
#[test]
fn test_progress_rounding() {
let prog = ProgressPercent::new(2, 10);
assert_eq!(prog.progress, 20);
}
assert_eq!(prog.progress, 20);
}
}

View file

@ -1,163 +0,0 @@
use crate::{
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::{fetch, push::ProgressNotification},
RepoPath,
},
AsyncGitNotification, RemoteProgress,
};
use crossbeam_channel::{unbounded, Sender};
use std::{
sync::{Arc, Mutex},
thread,
};
///
#[derive(Default, Clone, Debug)]
pub struct FetchRequest {
///
pub remote: String,
///
pub branch: String,
///
pub basic_credential: Option<BasicAuthCredential>,
}
//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not
#[derive(Default, Clone, Debug)]
struct FetchState {}
///
pub struct AsyncPull {
state: Arc<Mutex<Option<FetchState>>>,
last_result: Arc<Mutex<Option<(usize, String)>>>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
sender: Sender<AsyncGitNotification>,
repo: RepoPath,
}
impl AsyncPull {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
state: Arc::new(Mutex::new(None)),
last_result: Arc::new(Mutex::new(None)),
progress: Arc::new(Mutex::new(None)),
sender: sender.clone(),
}
}
///
pub fn is_pending(&self) -> Result<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn last_result(&self) -> Result<Option<(usize, String)>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
let res = self.progress.lock()?;
Ok(res.as_ref().map(|progress| progress.clone().into()))
}
///
pub fn request(&self, params: FetchRequest) -> Result<()> {
log::trace!("request");
if self.is_pending()? {
return Ok(());
}
self.set_request(&params)?;
RemoteProgress::set_progress(&self.progress, None)?;
let arc_state = Arc::clone(&self.state);
let arc_res = Arc::clone(&self.last_result);
let arc_progress = Arc::clone(&self.progress);
let sender = self.sender.clone();
let repo = self.repo.clone();
thread::spawn(move || {
let (progress_sender, receiver) = unbounded();
let handle = RemoteProgress::spawn_receiver_thread(
AsyncGitNotification::Pull,
sender.clone(),
receiver,
arc_progress,
);
let res = fetch(
&repo,
&params.branch,
params.basic_credential,
Some(progress_sender.clone()),
);
progress_sender
.send(ProgressNotification::Done)
.expect("closing send failed");
handle.join().expect("joining thread failed");
Self::set_result(&arc_res, res).expect("result error");
Self::clear_request(&arc_state).expect("clear error");
sender
.send(AsyncGitNotification::Pull)
.expect("AsyncNotification error");
});
Ok(())
}
fn set_request(&self, _params: &FetchRequest) -> Result<()> {
let mut state = self.state.lock()?;
if state.is_some() {
return Err(Error::Generic("pending request".into()));
}
*state = Some(FetchState {});
Ok(())
}
fn clear_request(
state: &Arc<Mutex<Option<FetchState>>>,
) -> Result<()> {
let mut state = state.lock()?;
*state = None;
Ok(())
}
fn set_result(
arc_result: &Arc<Mutex<Option<(usize, String)>>>,
res: Result<usize>,
) -> Result<()> {
let mut last_res = arc_result.lock()?;
*last_res = match res {
Ok(bytes) => Some((bytes, String::new())),
Err(e) => {
log::error!("fetch error: {e}");
Some((0, e.to_string()))
}
};
Ok(())
}
}

View file

@ -1,174 +1,163 @@
use crate::{
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::push::push_raw,
remotes::push::{ProgressNotification, PushType},
RepoPath,
},
AsyncGitNotification, RemoteProgress,
error::{Error, Result},
sync::{
cred::BasicAuthCredential, remotes::push::push,
remotes::push::ProgressNotification,
},
AsyncGitNotification, RemoteProgress, CWD,
};
use crossbeam_channel::{unbounded, Sender};
use std::{
sync::{Arc, Mutex},
thread,
sync::{Arc, Mutex},
thread,
};
///
#[derive(Default, Clone, Debug)]
pub struct PushRequest {
///
pub remote: String,
///
pub branch: String,
///
pub push_type: PushType,
///
pub force: bool,
///
pub delete: bool,
///
pub basic_credential: Option<BasicAuthCredential>,
///
pub remote: String,
///
pub branch: String,
///
pub force: bool,
///
pub basic_credential: Option<BasicAuthCredential>,
}
//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not
#[derive(Default, Clone, Debug)]
struct PushState {}
struct PushState {
request: PushRequest,
}
///
pub struct AsyncPush {
state: Arc<Mutex<Option<PushState>>>,
last_result: Arc<Mutex<Option<String>>>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
sender: Sender<AsyncGitNotification>,
repo: RepoPath,
state: Arc<Mutex<Option<PushState>>>,
last_result: Arc<Mutex<Option<String>>>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
sender: Sender<AsyncGitNotification>,
}
impl AsyncPush {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
state: Arc::new(Mutex::new(None)),
last_result: Arc::new(Mutex::new(None)),
progress: Arc::new(Mutex::new(None)),
sender: sender.clone(),
}
}
///
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
Self {
state: Arc::new(Mutex::new(None)),
last_result: Arc::new(Mutex::new(None)),
progress: Arc::new(Mutex::new(None)),
sender: sender.clone(),
}
}
///
pub fn is_pending(&self) -> Result<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn is_pending(&self) -> Result<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn last_result(&self) -> Result<Option<String>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn last_result(&self) -> Result<Option<String>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
let res = self.progress.lock()?;
Ok(res.as_ref().map(|progress| progress.clone().into()))
}
///
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
let res = self.progress.lock()?;
Ok(res.as_ref().map(|progress| progress.clone().into()))
}
///
pub fn request(&self, params: PushRequest) -> Result<()> {
log::trace!("request");
///
pub fn request(&mut self, params: PushRequest) -> Result<()> {
log::trace!("request");
if self.is_pending()? {
return Ok(());
}
if self.is_pending()? {
return Ok(());
}
self.set_request(&params)?;
RemoteProgress::set_progress(&self.progress, None)?;
self.set_request(&params)?;
RemoteProgress::set_progress(&self.progress, None)?;
let arc_state = Arc::clone(&self.state);
let arc_res = Arc::clone(&self.last_result);
let arc_progress = Arc::clone(&self.progress);
let sender = self.sender.clone();
let repo = self.repo.clone();
let arc_state = Arc::clone(&self.state);
let arc_res = Arc::clone(&self.last_result);
let arc_progress = Arc::clone(&self.progress);
let sender = self.sender.clone();
thread::spawn(move || {
let (progress_sender, receiver) = unbounded();
thread::spawn(move || {
let (progress_sender, receiver) = unbounded();
let handle = RemoteProgress::spawn_receiver_thread(
AsyncGitNotification::Push,
sender.clone(),
receiver,
arc_progress,
);
let handle = RemoteProgress::spawn_receiver_thread(
AsyncGitNotification::Push,
sender.clone(),
receiver,
arc_progress,
);
let res = push_raw(
&repo,
params.remote.as_str(),
params.branch.as_str(),
params.push_type,
params.force,
params.delete,
params.basic_credential.clone(),
Some(progress_sender.clone()),
);
let res = push(
CWD,
params.remote.as_str(),
params.branch.as_str(),
params.force,
params.basic_credential.clone(),
Some(progress_sender.clone()),
);
progress_sender
.send(ProgressNotification::Done)
.expect("closing send failed");
progress_sender
.send(ProgressNotification::Done)
.expect("closing send failed");
handle.join().expect("joining thread failed");
handle.join().expect("joining thread failed");
Self::set_result(&arc_res, res).expect("result error");
Self::set_result(&arc_res, res).expect("result error");
Self::clear_request(&arc_state).expect("clear error");
Self::clear_request(&arc_state).expect("clear error");
sender
.send(AsyncGitNotification::Push)
.expect("error sending push");
});
sender
.send(AsyncGitNotification::Push)
.expect("error sending push");
});
Ok(())
}
Ok(())
}
fn set_request(&self, _params: &PushRequest) -> Result<()> {
let mut state = self.state.lock()?;
fn set_request(&self, params: &PushRequest) -> Result<()> {
let mut state = self.state.lock()?;
if state.is_some() {
return Err(Error::Generic("pending request".into()));
}
if state.is_some() {
return Err(Error::Generic("pending request".into()));
}
*state = Some(PushState {});
*state = Some(PushState {
request: params.clone(),
});
Ok(())
}
Ok(())
}
fn clear_request(
state: &Arc<Mutex<Option<PushState>>>,
) -> Result<()> {
let mut state = state.lock()?;
fn clear_request(
state: &Arc<Mutex<Option<PushState>>>,
) -> Result<()> {
let mut state = state.lock()?;
*state = None;
*state = None;
Ok(())
}
Ok(())
}
fn set_result(
arc_result: &Arc<Mutex<Option<String>>>,
res: Result<()>,
) -> Result<()> {
let mut last_res = arc_result.lock()?;
fn set_result(
arc_result: &Arc<Mutex<Option<String>>>,
res: Result<()>,
) -> Result<()> {
let mut last_res = arc_result.lock()?;
*last_res = match res {
Ok(()) => None,
Err(e) => {
log::error!("push error: {e}");
Some(e.to_string())
}
};
*last_res = match res {
Ok(_) => None,
Err(e) => {
log::error!("push error: {}", e);
Some(e.to_string())
}
};
Ok(())
}
Ok(())
}
}

View file

@ -1,157 +1,153 @@
use crate::{
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::tags::{push_tags, PushTagsProgress},
RepoPath,
},
AsyncGitNotification, RemoteProgress,
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::tags::{push_tags, PushTagsProgress},
},
AsyncGitNotification, RemoteProgress, CWD,
};
use crossbeam_channel::{unbounded, Sender};
use std::{
sync::{Arc, Mutex},
thread,
sync::{Arc, Mutex},
thread,
};
///
#[derive(Default, Clone, Debug)]
pub struct PushTagsRequest {
///
pub remote: String,
///
pub basic_credential: Option<BasicAuthCredential>,
///
pub remote: String,
///
pub basic_credential: Option<BasicAuthCredential>,
}
//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not
#[derive(Default, Clone, Debug)]
struct PushState {}
struct PushState {
request: PushTagsRequest,
}
///
pub struct AsyncPushTags {
state: Arc<Mutex<Option<PushState>>>,
last_result: Arc<Mutex<Option<String>>>,
progress: Arc<Mutex<Option<PushTagsProgress>>>,
sender: Sender<AsyncGitNotification>,
repo: RepoPath,
state: Arc<Mutex<Option<PushState>>>,
last_result: Arc<Mutex<Option<String>>>,
progress: Arc<Mutex<Option<PushTagsProgress>>>,
sender: Sender<AsyncGitNotification>,
}
impl AsyncPushTags {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
state: Arc::new(Mutex::new(None)),
last_result: Arc::new(Mutex::new(None)),
progress: Arc::new(Mutex::new(None)),
sender: sender.clone(),
}
}
///
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
Self {
state: Arc::new(Mutex::new(None)),
last_result: Arc::new(Mutex::new(None)),
progress: Arc::new(Mutex::new(None)),
sender: sender.clone(),
}
}
///
pub fn is_pending(&self) -> Result<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn is_pending(&self) -> Result<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn last_result(&self) -> Result<Option<String>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn last_result(&self) -> Result<Option<String>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn progress(&self) -> Result<Option<PushTagsProgress>> {
let res = self.progress.lock()?;
Ok(*res)
}
///
pub fn progress(&self) -> Result<Option<PushTagsProgress>> {
let res = self.progress.lock()?;
Ok(*res)
}
///
pub fn request(&self, params: PushTagsRequest) -> Result<()> {
log::trace!("request");
///
pub fn request(&mut self, params: PushTagsRequest) -> Result<()> {
log::trace!("request");
if self.is_pending()? {
return Ok(());
}
if self.is_pending()? {
return Ok(());
}
self.set_request(&params)?;
RemoteProgress::set_progress(&self.progress, None)?;
self.set_request(&params)?;
RemoteProgress::set_progress(&self.progress, None)?;
let arc_state = Arc::clone(&self.state);
let arc_res = Arc::clone(&self.last_result);
let arc_progress = Arc::clone(&self.progress);
let sender = self.sender.clone();
let repo = self.repo.clone();
let arc_state = Arc::clone(&self.state);
let arc_res = Arc::clone(&self.last_result);
let arc_progress = Arc::clone(&self.progress);
let sender = self.sender.clone();
thread::spawn(move || {
let (progress_sender, receiver) = unbounded();
thread::spawn(move || {
let (progress_sender, receiver) = unbounded();
let handle = RemoteProgress::spawn_receiver_thread(
AsyncGitNotification::PushTags,
sender.clone(),
receiver,
arc_progress,
);
let handle = RemoteProgress::spawn_receiver_thread(
AsyncGitNotification::PushTags,
sender.clone(),
receiver,
arc_progress,
);
let res = push_tags(
&repo,
params.remote.as_str(),
params.basic_credential.clone(),
Some(progress_sender),
);
let res = push_tags(
CWD,
params.remote.as_str(),
params.basic_credential.clone(),
Some(progress_sender),
);
handle.join().expect("joining thread failed");
handle.join().expect("joining thread failed");
Self::set_result(&arc_res, res).expect("result error");
Self::set_result(&arc_res, res).expect("result error");
Self::clear_request(&arc_state).expect("clear error");
Self::clear_request(&arc_state).expect("clear error");
sender
.send(AsyncGitNotification::PushTags)
.expect("error sending push");
});
sender
.send(AsyncGitNotification::PushTags)
.expect("error sending push");
});
Ok(())
}
Ok(())
}
fn set_request(&self, _params: &PushTagsRequest) -> Result<()> {
let mut state = self.state.lock()?;
fn set_request(&self, params: &PushTagsRequest) -> Result<()> {
let mut state = self.state.lock()?;
if state.is_some() {
return Err(Error::Generic("pending request".into()));
}
if state.is_some() {
return Err(Error::Generic("pending request".into()));
}
*state = Some(PushState {});
*state = Some(PushState {
request: params.clone(),
});
Ok(())
}
Ok(())
}
fn clear_request(
state: &Arc<Mutex<Option<PushState>>>,
) -> Result<()> {
let mut state = state.lock()?;
fn clear_request(
state: &Arc<Mutex<Option<PushState>>>,
) -> Result<()> {
let mut state = state.lock()?;
*state = None;
*state = None;
Ok(())
}
Ok(())
}
fn set_result(
arc_result: &Arc<Mutex<Option<String>>>,
res: Result<()>,
) -> Result<()> {
let mut last_res = arc_result.lock()?;
fn set_result(
arc_result: &Arc<Mutex<Option<String>>>,
res: Result<()>,
) -> Result<()> {
let mut last_res = arc_result.lock()?;
*last_res = match res {
Ok(()) => None,
Err(e) => {
log::error!("push error: {e}");
Some(e.to_string())
}
};
*last_res = match res {
Ok(_) => None,
Err(e) => {
log::error!("push error: {}", e);
Some(e.to_string())
}
};
Ok(())
}
Ok(())
}
}

View file

@ -1,148 +1,151 @@
//!
use crate::{
error::Result,
progress::ProgressPercent,
sync::remotes::push::{AsyncProgress, ProgressNotification},
AsyncGitNotification,
error::Result,
progress::ProgressPercent,
sync::remotes::push::{AsyncProgress, ProgressNotification},
AsyncGitNotification,
};
use crossbeam_channel::{Receiver, Sender};
use git2::PackBuilderStage;
use std::{
sync::{Arc, Mutex},
thread::{self, JoinHandle},
sync::{Arc, Mutex},
thread::{self, JoinHandle},
time::Duration,
};
/// used for push/pull
#[derive(Clone, Debug)]
pub enum RemoteProgressState {
///
PackingAddingObject,
///
PackingDeltafiction,
///
Pushing,
/// fetch progress
Transfer,
/// remote progress done
Done,
///
PackingAddingObject,
///
PackingDeltafiction,
///
Pushing,
/// fetch progress
Transfer,
/// remote progress done
Done,
}
///
#[derive(Clone, Debug)]
pub struct RemoteProgress {
///
pub state: RemoteProgressState,
///
pub progress: ProgressPercent,
///
pub state: RemoteProgressState,
///
pub progress: ProgressPercent,
}
impl RemoteProgress {
///
pub fn new(
state: RemoteProgressState,
current: usize,
total: usize,
) -> Self {
Self {
state,
progress: ProgressPercent::new(current, total),
}
}
///
pub fn new(
state: RemoteProgressState,
current: usize,
total: usize,
) -> Self {
Self {
state,
progress: ProgressPercent::new(current, total),
}
}
///
pub const fn get_progress_percent(&self) -> u8 {
self.progress.progress
}
///
pub const fn get_progress_percent(&self) -> u8 {
self.progress.progress
}
pub(crate) fn set_progress<T>(
progress: &Arc<Mutex<Option<T>>>,
state: Option<T>,
) -> Result<()> {
let mut progress = progress.lock()?;
pub(crate) fn set_progress<T>(
progress: &Arc<Mutex<Option<T>>>,
state: Option<T>,
) -> Result<()> {
let mut progress = progress.lock()?;
*progress = state;
*progress = state;
Ok(())
}
Ok(())
}
/// spawn thread to listen to progress notifications coming in from blocking remote git method (fetch/push)
pub(crate) fn spawn_receiver_thread<
T: 'static + AsyncProgress,
>(
notification_type: AsyncGitNotification,
sender: Sender<AsyncGitNotification>,
receiver: Receiver<T>,
progress: Arc<Mutex<Option<T>>>,
) -> JoinHandle<()> {
thread::spawn(move || loop {
let incoming = receiver.recv();
match incoming {
Ok(update) => {
Self::set_progress(
&progress,
Some(update.clone()),
)
.expect("set progress failed");
sender
.send(notification_type)
.expect("Notification error");
/// spawn thread to listen to progress notifcations coming in from blocking remote git method (fetch/push)
pub(crate) fn spawn_receiver_thread<
T: 'static + AsyncProgress,
>(
notification_type: AsyncGitNotification,
sender: Sender<AsyncGitNotification>,
receiver: Receiver<T>,
progress: Arc<Mutex<Option<T>>>,
) -> JoinHandle<()> {
thread::spawn(move || loop {
let incoming = receiver.recv();
match incoming {
Ok(update) => {
Self::set_progress(
&progress,
Some(update.clone()),
)
.expect("set progress failed");
sender
.send(notification_type)
.expect("Notification error");
thread::yield_now();
//NOTE: for better debugging
thread::sleep(Duration::from_millis(1));
if update.is_done() {
break;
}
}
Err(e) => {
log::error!(
"remote progress receiver error: {e}",
);
break;
}
}
})
}
if update.is_done() {
break;
}
}
Err(e) => {
log::error!(
"remote progress receiver error: {}",
e
);
break;
}
}
})
}
}
impl From<ProgressNotification> for RemoteProgress {
fn from(progress: ProgressNotification) -> Self {
match progress {
ProgressNotification::Packing {
stage,
current,
total,
} => match stage {
PackBuilderStage::AddingObjects => Self::new(
RemoteProgressState::PackingAddingObject,
current,
total,
),
PackBuilderStage::Deltafication => Self::new(
RemoteProgressState::PackingDeltafiction,
current,
total,
),
},
ProgressNotification::PushTransfer {
current,
total,
..
} => Self::new(
RemoteProgressState::Pushing,
current,
total,
),
ProgressNotification::Transfer {
objects,
total_objects,
..
} => Self::new(
RemoteProgressState::Transfer,
objects,
total_objects,
),
_ => Self::new(RemoteProgressState::Done, 1, 1),
}
}
fn from(progress: ProgressNotification) -> Self {
match progress {
ProgressNotification::Packing {
stage,
current,
total,
} => match stage {
PackBuilderStage::AddingObjects => Self::new(
RemoteProgressState::PackingAddingObject,
current,
total,
),
PackBuilderStage::Deltafication => Self::new(
RemoteProgressState::PackingDeltafiction,
current,
total,
),
},
ProgressNotification::PushTransfer {
current,
total,
..
} => Self::new(
RemoteProgressState::Pushing,
current,
total,
),
ProgressNotification::Transfer {
objects,
total_objects,
..
} => Self::new(
RemoteProgressState::Transfer,
objects,
total_objects,
),
_ => Self::new(RemoteProgressState::Done, 1, 1),
}
}
}

View file

@ -1,88 +1,74 @@
//!
use crate::{
asyncjob::{AsyncJob, RunParams},
error::Result,
sync::cred::BasicAuthCredential,
sync::{
remotes::{get_default_remote, tags_missing_remote},
RepoPath,
},
AsyncGitNotification,
asyncjob::AsyncJob,
error::Result,
sync::cred::BasicAuthCredential,
sync::remotes::{get_default_remote, tags_missing_remote},
CWD,
};
use std::sync::{Arc, Mutex};
enum JobState {
Request(Option<BasicAuthCredential>),
Response(Result<Vec<String>>),
Request(Option<BasicAuthCredential>),
Response(Result<Vec<String>>),
}
///
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct AsyncRemoteTagsJob {
state: Arc<Mutex<Option<JobState>>>,
repo: RepoPath,
state: Arc<Mutex<Option<JobState>>>,
}
///
impl AsyncRemoteTagsJob {
///
pub fn new(
repo: RepoPath,
basic_credential: Option<BasicAuthCredential>,
) -> Self {
Self {
repo,
state: Arc::new(Mutex::new(Some(JobState::Request(
basic_credential,
)))),
}
}
///
pub fn new(
basic_credential: Option<BasicAuthCredential>,
) -> Self {
Self {
state: Arc::new(Mutex::new(Some(JobState::Request(
basic_credential,
)))),
}
}
///
pub fn result(&self) -> Option<Result<Vec<String>>> {
if let Ok(mut state) = self.state.lock() {
if let Some(state) = state.take() {
return match state {
JobState::Request(_) => None,
JobState::Response(result) => Some(result),
};
}
}
///
pub fn result(&self) -> Option<Result<Vec<String>>> {
if let Ok(mut state) = self.state.lock() {
if let Some(state) = state.take() {
return match state {
JobState::Request(_) => None,
JobState::Response(result) => Some(result),
};
}
}
None
}
None
}
}
impl AsyncJob for AsyncRemoteTagsJob {
type Notification = AsyncGitNotification;
type Progress = ();
fn run(&mut self) {
if let Ok(mut state) = self.state.lock() {
*state = state.take().map(|state| match state {
JobState::Request(basic_credential) => {
let result =
get_default_remote(CWD).and_then(|remote| {
tags_missing_remote(
CWD,
&remote,
basic_credential,
)
});
fn run(
&mut self,
_params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
if let Ok(mut state) = self.state.lock() {
*state = state.take().map(|state| match state {
JobState::Request(basic_credential) => {
let result = get_default_remote(&self.repo)
.and_then(|remote| {
tags_missing_remote(
&self.repo,
&remote,
basic_credential,
)
});
JobState::Response(result)
}
JobState::Response(result) => {
JobState::Response(result)
}
});
}
Ok(AsyncGitNotification::RemoteTags)
}
JobState::Response(result)
}
JobState::Response(result) => {
JobState::Response(result)
}
});
}
}
}

View file

@ -1,405 +1,201 @@
use crate::{
error::Result,
sync::{
gix_repo, repo, CommitId, LogWalker, LogWalkerWithoutFilter,
RepoPath, SharedCommitFilterFn,
},
AsyncGitNotification, Error,
error::Result,
sync::{utils::repo, CommitId, LogWalker, LogWalkerFilter},
AsyncGitNotification, CWD,
};
use crossbeam_channel::Sender;
use git2::Oid;
use scopetime::scope_time;
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread,
time::{Duration, Instant},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread,
time::Duration,
};
///
#[derive(PartialEq, Eq, Debug)]
#[derive(PartialEq)]
pub enum FetchStatus {
/// previous fetch still running
Pending,
/// no change expected
NoChange,
/// new walk was started
Started,
/// previous fetch still running
Pending,
/// no change expected
NoChange,
/// new walk was started
Started,
}
///
pub struct AsyncLogResult {
///
pub commits: Vec<CommitId>,
///
pub duration: Duration,
}
///
pub struct AsyncLog {
current: Arc<Mutex<AsyncLogResult>>,
current_head: Arc<Mutex<Option<CommitId>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicBool>,
background: Arc<AtomicBool>,
filter: Option<SharedCommitFilterFn>,
partial_extract: AtomicBool,
repo: RepoPath,
current: Arc<Mutex<Vec<CommitId>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicBool>,
background: Arc<AtomicBool>,
filter: Option<LogWalkerFilter>,
}
static LIMIT_COUNT: usize = 3000;
static SLEEP_FOREGROUND: Duration = Duration::from_millis(2);
static SLEEP_BACKGROUND: Duration = Duration::from_secs(1);
static SLEEP_BACKGROUND: Duration = Duration::from_millis(1000);
impl AsyncLog {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
filter: Option<SharedCommitFilterFn>,
) -> Self {
Self {
repo,
current: Arc::new(Mutex::new(AsyncLogResult {
commits: Vec::new(),
duration: Duration::default(),
})),
current_head: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicBool::new(false)),
background: Arc::new(AtomicBool::new(false)),
filter,
partial_extract: AtomicBool::new(false),
}
}
///
pub fn new(
sender: &Sender<AsyncGitNotification>,
filter: Option<LogWalkerFilter>,
) -> Self {
Self {
current: Arc::new(Mutex::new(Vec::new())),
sender: sender.clone(),
pending: Arc::new(AtomicBool::new(false)),
background: Arc::new(AtomicBool::new(false)),
filter,
}
}
///
pub fn count(&self) -> Result<usize> {
Ok(self.current.lock()?.commits.len())
}
///
pub fn count(&mut self) -> Result<usize> {
Ok(self.current.lock()?.len())
}
///
pub fn get_slice(
&self,
start_index: usize,
amount: usize,
) -> Result<Vec<CommitId>> {
if self.partial_extract.load(Ordering::Relaxed) {
return Err(Error::Generic(String::from("Faulty usage of AsyncLog: Cannot partially extract items and rely on get_items slice to still work!")));
}
///
pub fn get_slice(
&self,
start_index: usize,
amount: usize,
) -> Result<Vec<CommitId>> {
let list = self.current.lock()?;
let list_len = list.len();
let min = start_index.min(list_len);
let max = min + amount;
let max = max.min(list_len);
Ok(list[min..max].to_vec())
}
let list = &self.current.lock()?.commits;
let list_len = list.len();
let min = start_index.min(list_len);
let max = min + amount;
let max = max.min(list_len);
Ok(list[min..max].to_vec())
}
///
pub fn position(&self, id: CommitId) -> Result<Option<usize>> {
let list = self.current.lock()?;
let position = list.iter().position(|&x| x == id);
///
pub fn get_items(&self) -> Result<Vec<CommitId>> {
if self.partial_extract.load(Ordering::Relaxed) {
return Err(Error::Generic(String::from("Faulty usage of AsyncLog: Cannot partially extract items and rely on get_items slice to still work!")));
}
Ok(position)
}
let list = &self.current.lock()?.commits;
Ok(list.clone())
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed)
}
///
pub fn extract_items(&self) -> Result<Vec<CommitId>> {
self.partial_extract.store(true, Ordering::Relaxed);
let list = &mut self.current.lock()?.commits;
let result = list.clone();
list.clear();
Ok(result)
}
///
pub fn set_background(&mut self) {
self.background.store(true, Ordering::Relaxed);
}
///
pub fn get_last_duration(&self) -> Result<Duration> {
Ok(self.current.lock()?.duration)
}
///
fn current_head(&self) -> Result<CommitId> {
Ok(self
.current
.lock()?
.first()
.map_or(Oid::zero().into(), |f| *f))
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed)
}
///
fn head_changed(&self) -> Result<bool> {
if let Ok(head) = repo(CWD)?.head() {
if let Some(head) = head.target() {
return Ok(head != self.current_head()?.into());
}
}
Ok(false)
}
///
pub fn set_background(&self) {
self.background.store(true, Ordering::Relaxed);
}
///
pub fn fetch(&mut self) -> Result<FetchStatus> {
self.background.store(false, Ordering::Relaxed);
///
fn current_head(&self) -> Result<Option<CommitId>> {
Ok(*self.current_head.lock()?)
}
if self.is_pending() {
return Ok(FetchStatus::Pending);
}
///
fn head_changed(&self) -> Result<bool> {
if let Ok(head) = repo(&self.repo)?.head() {
return Ok(
head.target() != self.current_head()?.map(Into::into)
);
}
Ok(false)
}
if !self.head_changed()? {
return Ok(FetchStatus::NoChange);
}
///
pub fn fetch(&self) -> Result<FetchStatus> {
self.background.store(false, Ordering::Relaxed);
self.clear()?;
if self.is_pending() {
return Ok(FetchStatus::Pending);
}
let arc_current = Arc::clone(&self.current);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let arc_background = Arc::clone(&self.background);
if !self.head_changed()? {
return Ok(FetchStatus::NoChange);
}
self.pending.store(true, Ordering::Relaxed);
self.pending.store(true, Ordering::Relaxed);
let filter = self.filter.clone();
self.clear()?;
rayon_core::spawn(move || {
scope_time!("async::revlog");
let arc_current = Arc::clone(&self.current);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let arc_background = Arc::clone(&self.background);
let filter = self.filter.clone();
let repo_path = self.repo.clone();
Self::fetch_helper(
&arc_current,
&arc_background,
&sender,
filter,
)
.expect("failed to fetch");
if let Ok(head) = repo(&self.repo)?.head() {
*self.current_head.lock()? =
head.target().map(CommitId::new);
}
arc_pending.store(false, Ordering::Relaxed);
rayon_core::spawn(move || {
scope_time!("async::revlog");
Self::notify(&sender);
});
Self::fetch_helper(
&repo_path,
&arc_current,
&arc_background,
&sender,
filter,
)
.expect("failed to fetch");
Ok(FetchStatus::Started)
}
arc_pending.store(false, Ordering::Relaxed);
fn fetch_helper(
arc_current: &Arc<Mutex<Vec<CommitId>>>,
arc_background: &Arc<AtomicBool>,
sender: &Sender<AsyncGitNotification>,
filter: Option<LogWalkerFilter>,
) -> Result<()> {
let mut entries = Vec::with_capacity(LIMIT_COUNT);
let r = repo(CWD)?;
let mut walker =
LogWalker::new(&r, LIMIT_COUNT)?.filter(filter);
loop {
entries.clear();
let res_is_err = walker.read(&mut entries).is_err();
Self::notify(&sender);
});
if !res_is_err {
let mut current = arc_current.lock()?;
current.extend(entries.iter());
}
Ok(FetchStatus::Started)
}
if res_is_err || entries.len() <= 1 {
break;
}
Self::notify(sender);
fn fetch_helper(
repo_path: &RepoPath,
arc_current: &Arc<Mutex<AsyncLogResult>>,
arc_background: &Arc<AtomicBool>,
sender: &Sender<AsyncGitNotification>,
filter: Option<SharedCommitFilterFn>,
) -> Result<()> {
filter.map_or_else(
|| {
Self::fetch_helper_without_filter(
repo_path,
arc_current,
arc_background,
sender,
)
},
|filter| {
Self::fetch_helper_with_filter(
repo_path,
arc_current,
arc_background,
sender,
filter,
)
},
)
}
let sleep_duration =
if arc_background.load(Ordering::Relaxed) {
SLEEP_BACKGROUND
} else {
SLEEP_FOREGROUND
};
thread::sleep(sleep_duration);
}
fn fetch_helper_with_filter(
repo_path: &RepoPath,
arc_current: &Arc<Mutex<AsyncLogResult>>,
arc_background: &Arc<AtomicBool>,
sender: &Sender<AsyncGitNotification>,
filter: SharedCommitFilterFn,
) -> Result<()> {
let start_time = Instant::now();
Ok(())
}
let mut entries = vec![CommitId::default(); LIMIT_COUNT];
entries.resize(0, CommitId::default());
fn clear(&mut self) -> Result<()> {
self.current.lock()?.clear();
Ok(())
}
let r = repo(repo_path)?;
let mut walker =
LogWalker::new(&r, LIMIT_COUNT)?.filter(Some(filter));
loop {
entries.clear();
let read = walker.read(&mut entries)?;
let mut current = arc_current.lock()?;
current.commits.extend(entries.iter());
current.duration = start_time.elapsed();
if read == 0 {
break;
}
Self::notify(sender);
let sleep_duration =
if arc_background.load(Ordering::Relaxed) {
SLEEP_BACKGROUND
} else {
SLEEP_FOREGROUND
};
thread::sleep(sleep_duration);
}
log::trace!("revlog visited: {}", walker.visited());
Ok(())
}
fn fetch_helper_without_filter(
repo_path: &RepoPath,
arc_current: &Arc<Mutex<AsyncLogResult>>,
arc_background: &Arc<AtomicBool>,
sender: &Sender<AsyncGitNotification>,
) -> Result<()> {
let start_time = Instant::now();
let mut entries = vec![CommitId::default(); LIMIT_COUNT];
entries.resize(0, CommitId::default());
let mut repo: gix::Repository = gix_repo(repo_path)?;
let mut walker =
LogWalkerWithoutFilter::new(&mut repo, LIMIT_COUNT)?;
loop {
entries.clear();
let read = walker.read(&mut entries)?;
let mut current = arc_current.lock()?;
current.commits.extend(entries.iter());
current.duration = start_time.elapsed();
if read == 0 {
break;
}
Self::notify(sender);
let sleep_duration =
if arc_background.load(Ordering::Relaxed) {
SLEEP_BACKGROUND
} else {
SLEEP_FOREGROUND
};
thread::sleep(sleep_duration);
}
log::trace!("revlog visited: {}", walker.visited());
Ok(())
}
fn clear(&self) -> Result<()> {
self.current.lock()?.commits.clear();
*self.current_head.lock()? = None;
self.partial_extract.store(false, Ordering::Relaxed);
Ok(())
}
fn notify(sender: &Sender<AsyncGitNotification>) {
sender
.send(AsyncGitNotification::Log)
.expect("error sending");
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crossbeam_channel::unbounded;
use serial_test::serial;
use tempfile::TempDir;
use crate::sync::tests::{debug_cmd_print, repo_init};
use crate::sync::RepoPath;
use crate::AsyncLog;
use super::AsyncLogResult;
#[test]
#[serial]
fn test_smoke_in_subdir() {
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 (tx_git, _rx_git) = unbounded();
debug_cmd_print(&repo_path, "mkdir subdir");
let subdir = repo.path().parent().unwrap().join("subdir");
let subdir_path: RepoPath =
subdir.as_os_str().to_str().unwrap().into();
let arc_current = Arc::new(Mutex::new(AsyncLogResult {
commits: Vec::new(),
duration: Duration::default(),
}));
let arc_background = Arc::new(AtomicBool::new(false));
let result = AsyncLog::fetch_helper_without_filter(
&subdir_path,
&arc_current,
&arc_background,
&tx_git,
);
assert_eq!(result.unwrap(), ());
}
#[test]
#[serial]
fn test_env_variables() {
let (_td, repo) = repo_init().unwrap();
let git_dir = repo.path();
let (tx_git, _rx_git) = unbounded();
let empty_dir = TempDir::new().unwrap();
let empty_path: RepoPath =
empty_dir.path().to_str().unwrap().into();
let arc_current = Arc::new(Mutex::new(AsyncLogResult {
commits: Vec::new(),
duration: Duration::default(),
}));
let arc_background = Arc::new(AtomicBool::new(false));
std::env::set_var("GIT_DIR", git_dir);
let result = AsyncLog::fetch_helper_without_filter(
// We pass an empty path, thus testing whether `GIT_DIR`, set above, is taken into account.
&empty_path,
&arc_current,
&arc_background,
&tx_git,
);
std::env::remove_var("GIT_DIR");
assert_eq!(result.unwrap(), ());
}
fn notify(sender: &Sender<AsyncGitNotification>) {
sender
.send(AsyncGitNotification::Log)
.expect("error sending");
}
}

View file

@ -1,191 +1,169 @@
use crate::{
error::Result,
hash,
sync::{
self, status::StatusType, RepoPath, ShowUntrackedFilesConfig,
},
AsyncGitNotification, StatusItem,
error::Result,
hash,
sync::{self, status::StatusType},
AsyncGitNotification, StatusItem, CWD,
};
use crossbeam_channel::Sender;
use std::{
hash::Hash,
sync::{
atomic::{AtomicU64, AtomicUsize, Ordering},
Arc, Mutex,
},
hash::Hash,
sync::{
atomic::{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>,
pub items: Vec<StatusItem>,
}
///
#[derive(Default, Hash, Copy, Clone, PartialEq, Eq)]
#[derive(Default, Hash, Copy, Clone, PartialEq)]
pub struct StatusParams {
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
tick: u128,
status_type: StatusType,
}
impl StatusParams {
///
pub const fn new(
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
) -> Self {
Self {
status_type,
config,
}
}
///
pub fn new(status_type: StatusType) -> Self {
Self {
tick: current_tick(),
status_type,
}
}
}
struct Request<R, A>(R, Option<A>);
///
pub struct AsyncStatus {
current: Arc<Mutex<Request<u64, Status>>>,
last: Arc<Mutex<Status>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
repo: RepoPath,
/// Counter that increments after each completed fetch.
generation: Arc<AtomicU64>,
current: Arc<Mutex<Request<u64, Status>>>,
last: Arc<Mutex<Status>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
}
impl AsyncStatus {
///
pub fn new(
repo: RepoPath,
sender: Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(Status::default())),
sender,
pending: Arc::new(AtomicUsize::new(0)),
generation: Arc::new(AtomicU64::new(0)),
}
}
///
pub fn new(sender: Sender<AsyncGitNotification>) -> Self {
Self {
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(Status::default())),
sender,
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn last(&self) -> Result<Status> {
let last = self.last.lock()?;
Ok(last.clone())
}
///
pub fn last(&mut self) -> Result<Status> {
let last = self.last.lock()?;
Ok(last.clone())
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn fetch(
&self,
params: &StatusParams,
) -> Result<Option<Status>> {
if self.is_pending() {
log::trace!("request blocked, still pending");
return Ok(None);
}
///
pub fn fetch(
&mut self,
params: &StatusParams,
) -> Result<Option<Status>> {
if self.is_pending() {
log::trace!("request blocked, still pending");
return Ok(None);
}
let generation = self.generation.load(Ordering::Relaxed);
let hash_request = hash(&(params, generation));
let hash_request = hash(&params);
log::trace!(
"request: [hash: {}] (type: {:?}, gen: {})",
hash_request,
params.status_type,
generation,
);
log::trace!(
"request: [hash: {}] (type: {:?})",
hash_request,
params.status_type,
);
{
let mut current = self.current.lock()?;
{
let mut current = self.current.lock()?;
if current.0 == hash_request {
return Ok(current.1.clone());
}
if current.0 == hash_request {
return Ok(current.1.clone());
}
current.0 = hash_request;
current.1 = None;
}
current.0 = hash_request;
current.1 = None;
}
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;
let config = params.config;
let repo = self.repo.clone();
let arc_current = Arc::clone(&self.current);
let arc_last = Arc::clone(&self.last);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let status_type = params.status_type;
self.pending.fetch_add(1, Ordering::Relaxed);
self.pending.fetch_add(1, Ordering::Relaxed);
rayon_core::spawn(move || {
if let Err(e) = Self::fetch_helper(
&repo,
status_type,
config,
hash_request,
&arc_current,
&arc_last,
) {
log::error!("fetch_helper: {e}");
}
rayon_core::spawn(move || {
let ok = Self::fetch_helper(
status_type,
hash_request,
&arc_current,
&arc_last,
)
.is_ok();
// Increment generation to invalidate cache for next request
arc_generation.fetch_add(1, Ordering::Relaxed);
arc_pending.fetch_sub(1, Ordering::Relaxed);
arc_pending.fetch_sub(1, Ordering::Relaxed);
if let Err(e) = sender.send(AsyncGitNotification::Status)
{
log::error!("send status error: {e}");
}
});
if ok {
sender
.send(AsyncGitNotification::Status)
.expect("error sending status");
}
});
Ok(None)
}
Ok(None)
}
fn fetch_helper(
repo: &RepoPath,
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
hash_request: u64,
arc_current: &Arc<Mutex<Request<u64, Status>>>,
arc_last: &Arc<Mutex<Status>>,
) -> Result<()> {
let res = Self::get_status(repo, status_type, config)?;
log::trace!(
"status fetched: {hash_request} (type: {status_type:?})",
);
fn fetch_helper(
status_type: StatusType,
hash_request: u64,
arc_current: &Arc<Mutex<Request<u64, Status>>>,
arc_last: &Arc<Mutex<Status>>,
) -> Result<()> {
let res = Self::get_status(status_type)?;
log::trace!(
"status fetched: {} (type: {:?})",
hash_request,
status_type,
);
{
let mut current = arc_current.lock()?;
if current.0 == hash_request {
current.1 = Some(res.clone());
}
}
{
let mut current = arc_current.lock()?;
if current.0 == hash_request {
current.1 = Some(res.clone());
}
}
{
let mut last = arc_last.lock()?;
*last = res;
}
{
let mut last = arc_last.lock()?;
*last = res;
}
Ok(())
}
Ok(())
}
fn get_status(
repo: &RepoPath,
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
) -> Result<Status> {
Ok(Status {
items: sync::status::get_status(
repo,
status_type,
config,
)?,
})
}
fn get_status(status_type: StatusType) -> Result<Status> {
Ok(Status {
items: sync::status::get_status(CWD, status_type)?,
})
}
}

View file

@ -1,11 +1,10 @@
//! Sync git API for fetching a file blame
use super::{utils, CommitId, RepoPath};
use super::{utils, CommitId};
use crate::{
error::{Error, Result},
sync::{get_commits_info, repository::repo},
error::{Error, Result},
sync::get_commits_info,
};
use git2::BlameOptions;
use scopetime::scope_time;
use std::collections::{HashMap, HashSet};
use std::io::{BufRead, BufReader};
@ -14,249 +13,205 @@ use std::path::Path;
/// A `BlameHunk` contains all the information that will be shown to the user.
#[derive(Clone, Hash, Debug, PartialEq, Eq)]
pub struct BlameHunk {
///
pub commit_id: CommitId,
///
pub author: String,
///
pub time: i64,
/// `git2::BlameHunk::final_start_line` returns 1-based indices, but
/// `start_line` is 0-based because the `Vec` storing the lines starts at
/// index 0.
pub start_line: usize,
///
pub end_line: usize,
///
pub commit_id: CommitId,
///
pub author: String,
///
pub time: i64,
/// `git2::BlameHunk::final_start_line` returns 1-based indices, but
/// `start_line` is 0-based because the `Vec` storing the lines starts at
/// index 0.
pub start_line: usize,
///
pub end_line: usize,
}
/// A `BlameFile` represents a collection of lines. This is targeted at how the
/// data will be used by the UI.
#[derive(Clone, Debug)]
pub struct FileBlame {
///
pub commit_id: CommitId,
///
pub path: String,
///
pub lines: Vec<(Option<BlameHunk>, String)>,
}
/// fixup `\` windows path separators to git compatible `/`
fn fixup_windows_path(path: &str) -> String {
#[cfg(windows)]
{
path.replace('\\', "/")
}
#[cfg(not(windows))]
{
path.to_string()
}
///
pub commit_id: CommitId,
///
pub path: String,
///
pub lines: Vec<(Option<BlameHunk>, String)>,
}
///
pub fn blame_file(
repo_path: &RepoPath,
file_path: &str,
commit_id: Option<CommitId>,
repo_path: &str,
file_path: &str,
) -> Result<FileBlame> {
scope_time!("blame_file");
scope_time!("blame_file");
let repo = repo(repo_path)?;
let repo = utils::repo(repo_path)?;
let commit_id = if let Some(commit_id) = commit_id {
commit_id
} else {
utils::get_head_repo(&repo)?
};
let commit_id = utils::get_head_repo(&repo)?;
let spec =
format!("{}:{}", commit_id, fixup_windows_path(file_path));
let spec = format!("{}:{}", commit_id.to_string(), file_path);
let object = repo.revparse_single(&spec)?;
let blob = repo.find_blob(object.id())?;
let object = repo.revparse_single(&spec)?;
let blob = repo.find_blob(object.id())?;
if blob.is_binary() {
return Err(Error::NoBlameOnBinaryFile);
}
if blob.is_binary() {
return Err(Error::NoBlameOnBinaryFile);
}
let mut opts = BlameOptions::new();
opts.newest_commit(commit_id.into());
let blame = repo.blame_file(Path::new(file_path), None)?;
let blame =
repo.blame_file(Path::new(file_path), Some(&mut opts))?;
let reader = BufReader::new(blob.content());
let reader = BufReader::new(blob.content());
let unique_commit_ids: HashSet<_> = blame
.iter()
.map(|hunk| CommitId::new(hunk.final_commit_id()))
.collect();
let mut commit_ids = Vec::with_capacity(unique_commit_ids.len());
commit_ids.extend(unique_commit_ids);
let unique_commit_ids: HashSet<_> = blame
.iter()
.map(|hunk| CommitId::new(hunk.final_commit_id()))
.collect();
let mut commit_ids = Vec::with_capacity(unique_commit_ids.len());
commit_ids.extend(unique_commit_ids);
let commit_infos = get_commits_info(repo_path, &commit_ids, 0)?;
let unique_commit_infos: HashMap<_, _> = commit_infos
.iter()
.map(|commit_info| (commit_info.id, commit_info))
.collect();
let commit_infos = get_commits_info(repo_path, &commit_ids, 0)?;
let unique_commit_infos: HashMap<_, _> = commit_infos
.iter()
.map(|commit_info| (commit_info.id, commit_info))
.collect();
let lines: Vec<(Option<BlameHunk>, String)> = reader
.lines()
.enumerate()
.map(|(i, line)| {
// Line indices in a `FileBlame` are 1-based.
let corresponding_hunk = blame.get_line(i + 1);
let lines: Vec<(Option<BlameHunk>, String)> = reader
.lines()
.enumerate()
.map(|(i, line)| {
// Line indices in a `FileBlame` are 1-based.
let corresponding_hunk = blame.get_line(i + 1);
if let Some(hunk) = corresponding_hunk {
let commit_id = CommitId::new(hunk.final_commit_id());
// Line indices in a `BlameHunk` are 1-based.
let start_line =
hunk.final_start_line().saturating_sub(1);
let end_line =
start_line.saturating_add(hunk.lines_in_hunk());
if let Some(hunk) = corresponding_hunk {
let commit_id = CommitId::new(hunk.final_commit_id());
// Line indices in a `BlameHunk` are 1-based.
let start_line =
hunk.final_start_line().saturating_sub(1);
let end_line =
start_line.saturating_add(hunk.lines_in_hunk());
if let Some(commit_info) =
unique_commit_infos.get(&commit_id)
{
let hunk = BlameHunk {
commit_id,
author: commit_info.author.clone(),
time: commit_info.time,
start_line,
end_line,
};
if let Some(commit_info) =
unique_commit_infos.get(&commit_id)
{
let hunk = BlameHunk {
commit_id,
author: commit_info.author.clone(),
time: commit_info.time,
start_line,
end_line,
};
return (
Some(hunk),
line.unwrap_or_else(|_| "".into()),
);
}
}
return (
Some(hunk),
line.unwrap_or_else(|_| String::new()),
);
}
}
(None, line.unwrap_or_else(|_| "".into()))
})
.collect();
(None, line.unwrap_or_else(|_| String::new()))
})
.collect();
let file_blame = FileBlame {
commit_id,
path: file_path.into(),
lines,
};
let file_blame = FileBlame {
commit_id,
path: file_path.into(),
lines,
};
Ok(file_blame)
Ok(file_blame)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
error::Result,
sync::{commit, stage_add_file, tests::repo_init_empty},
};
use std::{
fs::{File, OpenOptions},
io::Write,
path::Path,
};
use super::*;
use crate::error::Result;
use crate::sync::{
commit, stage_add_file, tests::repo_init_empty,
};
use std::{
fs::{File, OpenOptions},
io::Write,
path::Path,
};
#[test]
fn test_blame() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_blame() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
assert!(blame_file(repo_path, "foo", None).is_err());
assert!(matches!(blame_file(&repo_path, "foo"), Err(_)));
File::create(root.join(file_path))?.write_all(b"line 1\n")?;
File::create(&root.join(file_path))?
.write_all(b"line 1\n")?;
stage_add_file(repo_path, file_path)?;
commit(repo_path, "first commit")?;
stage_add_file(repo_path, file_path)?;
commit(repo_path, "first commit")?;
let blame = blame_file(repo_path, "foo", None)?;
let blame = blame_file(&repo_path, "foo")?;
assert!(matches!(
blame.lines.as_slice(),
[(
Some(BlameHunk {
author,
start_line: 0,
end_line: 1,
..
}),
line
)] if author == "name" && line == "line 1"
));
assert!(matches!(
blame.lines.as_slice(),
[(
Some(BlameHunk {
author,
start_line: 0,
end_line: 1,
..
}),
line
)] if author == "name" && line == "line 1"
));
let mut file = OpenOptions::new()
.append(true)
.open(root.join(file_path))?;
let mut file = OpenOptions::new()
.append(true)
.open(&root.join(file_path))?;
file.write(b"line 2\n")?;
file.write(b"line 2\n")?;
stage_add_file(repo_path, file_path)?;
commit(repo_path, "second commit")?;
stage_add_file(repo_path, file_path)?;
commit(repo_path, "second commit")?;
let blame = blame_file(repo_path, "foo", None)?;
let blame = blame_file(&repo_path, "foo")?;
assert!(matches!(
blame.lines.as_slice(),
[
(
Some(BlameHunk {
start_line: 0,
end_line: 1,
..
}),
first_line
),
(
Some(BlameHunk {
author,
start_line: 1,
end_line: 2,
..
}),
second_line
)
] if author == "name" && first_line == "line 1" && second_line == "line 2"
));
assert!(matches!(
blame.lines.as_slice(),
[
(
Some(BlameHunk {
start_line: 0,
end_line: 1,
..
}),
first_line
),
(
Some(BlameHunk {
author,
start_line: 1,
end_line: 2,
..
}),
second_line
)
] if author == "name" && first_line == "line 1" && second_line == "line 2"
));
file.write(b"line 3\n")?;
file.write(b"line 3\n")?;
let blame = blame_file(repo_path, "foo", None)?;
let blame = blame_file(&repo_path, "foo")?;
assert_eq!(blame.lines.len(), 2);
assert_eq!(blame.lines.len(), 2);
stage_add_file(repo_path, file_path)?;
commit(repo_path, "third commit")?;
stage_add_file(repo_path, file_path)?;
commit(repo_path, "third commit")?;
let blame = blame_file(repo_path, "foo", None)?;
let blame = blame_file(&repo_path, "foo")?;
assert_eq!(blame.lines.len(), 3);
assert_eq!(blame.lines.len(), 3);
Ok(())
}
#[test]
fn test_blame_windows_path_dividers() {
let file_path = Path::new("bar\\foo");
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();
std::fs::create_dir(root.join("bar")).unwrap();
File::create(root.join(file_path))
.unwrap()
.write_all(b"line 1\n")
.unwrap();
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "first commit").unwrap();
assert!(blame_file(repo_path, "bar\\foo", None).is_ok());
}
Ok(())
}
}

View file

@ -2,8 +2,8 @@
use super::BranchType;
use crate::{
error::{Error, Result},
sync::{merge_msg, repository::repo, CommitId, RepoPath},
error::{Error, Result},
sync::{merge_msg, utils, CommitId},
};
use git2::Commit;
use scopetime::scope_time;
@ -12,270 +12,258 @@ use scopetime::scope_time;
/// if we did not create conflicts we create a merge commit and return the commit id.
/// Otherwise we return `None`
pub fn merge_upstream_commit(
repo_path: &RepoPath,
branch_name: &str,
repo_path: &str,
branch_name: &str,
) -> Result<Option<CommitId>> {
scope_time!("merge_upstream_commit");
scope_time!("merge_upstream_commit");
let repo = repo(repo_path)?;
let repo = utils::repo(repo_path)?;
let branch = repo.find_branch(branch_name, BranchType::Local)?;
let upstream = branch.upstream()?;
let branch = repo.find_branch(branch_name, BranchType::Local)?;
let upstream = branch.upstream()?;
let upstream_commit = upstream.get().peel_to_commit()?;
let upstream_commit = upstream.get().peel_to_commit()?;
let annotated_upstream = repo
.reference_to_annotated_commit(&upstream.into_reference())?;
let annotated_upstream = repo
.reference_to_annotated_commit(&upstream.into_reference())?;
let (analysis, pref) =
repo.merge_analysis(&[&annotated_upstream])?;
let (analysis, pref) =
repo.merge_analysis(&[&annotated_upstream])?;
if !analysis.is_normal() {
return Err(Error::Generic(
"normal merge not possible".into(),
));
}
if !analysis.is_normal() {
return Err(Error::Generic(
"normal merge not possible".into(),
));
}
if analysis.is_fast_forward() && pref.is_fastforward_only() {
return Err(Error::Generic(
"ff merge would be possible".into(),
));
}
if analysis.is_fast_forward() && pref.is_fastforward_only() {
return Err(Error::Generic(
"ff merge would be possible".into(),
));
}
//TODO: support merge on unborn?
if analysis.is_unborn() {
return Err(Error::Generic("head is unborn".into()));
}
//TODO: support merge on unborn?
if analysis.is_unborn() {
return Err(Error::Generic("head is unborn".into()));
}
repo.merge(&[&annotated_upstream], None, None)?;
repo.merge(&[&annotated_upstream], None, None)?;
if !repo.index()?.has_conflicts() {
let msg = merge_msg(repo_path)?;
if !repo.index()?.has_conflicts() {
let msg = merge_msg(repo_path)?;
let commit_id =
commit_merge_with_head(&repo, &[upstream_commit], &msg)?;
let commit_id =
commit_merge_with_head(&repo, &[upstream_commit], &msg)?;
return Ok(Some(commit_id));
}
return Ok(Some(commit_id));
}
Ok(None)
Ok(None)
}
pub(crate) fn commit_merge_with_head(
repo: &git2::Repository,
commits: &[Commit],
msg: &str,
repo: &git2::Repository,
commits: &[Commit],
msg: &str,
) -> Result<CommitId> {
let signature =
crate::sync::commit::signature_allow_undefined_name(repo)?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let head_commit = repo.find_commit(
crate::sync::utils::get_head_repo(repo)?.into(),
)?;
let signature =
crate::sync::commit::signature_allow_undefined_name(repo)?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let head_commit = repo.find_commit(
crate::sync::utils::get_head_repo(repo)?.into(),
)?;
let mut parents = vec![&head_commit];
parents.extend(commits);
let mut parents = vec![&head_commit];
parents.extend(commits);
let commit_id = repo
.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?
.into();
repo.cleanup_state()?;
Ok(commit_id)
let commit_id = repo
.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?
.into();
repo.cleanup_state()?;
Ok(commit_id)
}
#[cfg(test)]
mod test {
use git2::Time;
use git2::Time;
use super::*;
use crate::sync::{
branch_compare_upstream,
remotes::{fetch, push::push_branch},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare, write_commit_file, write_commit_file_at,
},
RepoState,
};
use super::*;
use crate::sync::{
branch_compare_upstream,
remotes::{fetch, push::push},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare, write_commit_file, write_commit_file_at,
},
RepoState,
};
#[test]
fn test_merge_normal() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
#[test]
fn test_merge_normal() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
// clone1
// clone1
let commit1 = write_commit_file_at(
&clone1,
"test.txt",
"test",
"commit1",
Time::new(1, 0),
);
let commit1 = write_commit_file_at(
&clone1,
"test.txt",
"test",
"commit1",
Time::new(1, 0),
);
push_branch(
&clone1_dir.path().to_str().unwrap().into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
push(
clone1_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.unwrap();
// clone2
// clone2
let commit2 = write_commit_file_at(
&clone2,
"test2.txt",
"test",
"commit2",
Time::new(2, 0),
);
let commit2 = write_commit_file_at(
&clone2,
"test2.txt",
"test",
"commit2",
Time::new(2, 0),
);
//push should fail since origin diverged
assert!(push_branch(
&clone2_dir.into(),
"origin",
"master",
false,
false,
None,
None,
)
.is_err());
//push should fail since origin diverged
assert!(push(
clone2_dir, "origin", "master", false, None, None,
)
.is_err());
//lets fetch from origin
let bytes =
fetch(&clone2_dir.into(), "master", None, None).unwrap();
assert!(bytes > 0);
//lets fetch from origin
let bytes = fetch(clone2_dir, "master", None, None).unwrap();
assert!(bytes > 0);
//we should be one commit behind
assert_eq!(
branch_compare_upstream(&clone2_dir.into(), "master")
.unwrap()
.behind,
1
);
//we should be one commit behind
assert_eq!(
branch_compare_upstream(clone2_dir, "master")
.unwrap()
.behind,
1
);
let merge_commit =
merge_upstream_commit(&clone2_dir.into(), "master")
.unwrap()
.unwrap();
let merge_commit =
merge_upstream_commit(clone2_dir, "master")
.unwrap()
.unwrap();
let state =
crate::sync::repo_state(&clone2_dir.into()).unwrap();
assert_eq!(state, RepoState::Clean);
let state = crate::sync::repo_state(clone2_dir).unwrap();
assert_eq!(state, RepoState::Clean);
assert!(!clone2.head_detached().unwrap());
assert!(!clone2.head_detached().unwrap());
let commits = get_commit_ids(&clone2, 10);
assert_eq!(commits.len(), 3);
assert_eq!(commits[0], merge_commit);
assert_eq!(commits[1], commit2);
assert_eq!(commits[2], commit1);
let commits = get_commit_ids(&clone2, 10);
assert_eq!(commits.len(), 3);
assert_eq!(commits[0], merge_commit);
assert_eq!(commits[1], commit2);
assert_eq!(commits[2], commit1);
//verify commit msg
let details = crate::sync::get_commit_details(
&clone2_dir.into(),
merge_commit,
)
.unwrap();
assert_eq!(
//verify commit msg
let details =
crate::sync::get_commit_details(clone2_dir, merge_commit)
.unwrap();
assert_eq!(
details.message.unwrap().combine(),
String::from("Merge remote-tracking branch 'refs/remotes/origin/master'")
);
}
}
#[test]
fn test_merge_normal_non_ff() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
#[test]
fn test_merge_normal_non_ff() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
// clone1
// clone1
write_commit_file(
&clone1,
"test.bin",
"test\nfooo",
"commit1",
);
write_commit_file(
&clone1,
"test.bin",
"test\nfooo",
"commit1",
);
debug_cmd_print(
&clone2_dir.path().to_str().unwrap().into(),
"git status",
);
debug_cmd_print(
clone2_dir.path().to_str().unwrap(),
"git status",
);
push_branch(
&clone1_dir.path().to_str().unwrap().into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
push(
clone1_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.unwrap();
// clone2
// clone2
write_commit_file(
&clone2,
"test.bin",
"foobar\ntest",
"commit2",
);
write_commit_file(
&clone2,
"test.bin",
"foobar\ntest",
"commit2",
);
let bytes = fetch(
&clone2_dir.path().to_str().unwrap().into(),
"master",
None,
None,
)
.unwrap();
assert!(bytes > 0);
let bytes = fetch(
clone2_dir.path().to_str().unwrap(),
"master",
None,
None,
)
.unwrap();
assert!(bytes > 0);
let res = merge_upstream_commit(
&clone2_dir.path().to_str().unwrap().into(),
"master",
)
.unwrap();
let res = merge_upstream_commit(
clone2_dir.path().to_str().unwrap(),
"master",
)
.unwrap();
//this should not have committed cause we left conflicts behind
assert_eq!(res, None);
//this should not have commited cause we left conflicts behind
assert_eq!(res, None);
let state = crate::sync::repo_state(
&clone2_dir.path().to_str().unwrap().into(),
)
.unwrap();
let state = crate::sync::repo_state(
clone2_dir.path().to_str().unwrap(),
)
.unwrap();
//validate the repo is in a merge state now
assert_eq!(state, RepoState::Merge);
//validate the repo is in a merge state now
assert_eq!(state, RepoState::Merge);
//check that we still only have the first commit
let commits = get_commit_ids(&clone1, 10);
assert_eq!(commits.len(), 1);
}
//check that we still only have the first commit
let commits = get_commit_ids(&clone1, 10);
assert_eq!(commits.len(), 1);
}
}

View file

@ -2,143 +2,141 @@
use super::BranchType;
use crate::{
error::{Error, Result},
sync::{repository::repo, RepoPath},
error::{Error, Result},
sync::utils,
};
use scopetime::scope_time;
///
pub fn branch_merge_upstream_fastforward(
repo_path: &RepoPath,
branch: &str,
repo_path: &str,
branch: &str,
) -> Result<()> {
scope_time!("branch_merge_upstream");
scope_time!("branch_merge_upstream");
let repo = repo(repo_path)?;
let repo = utils::repo(repo_path)?;
let branch = repo.find_branch(branch, BranchType::Local)?;
let upstream = branch.upstream()?;
let branch = repo.find_branch(branch, BranchType::Local)?;
let upstream = branch.upstream()?;
let upstream_commit =
upstream.into_reference().peel_to_commit()?;
let upstream_commit =
upstream.into_reference().peel_to_commit()?;
let annotated =
repo.find_annotated_commit(upstream_commit.id())?;
let annotated =
repo.find_annotated_commit(upstream_commit.id())?;
let (analysis, pref) = repo.merge_analysis(&[&annotated])?;
let (analysis, pref) = repo.merge_analysis(&[&annotated])?;
if !analysis.is_fast_forward() {
return Err(Error::Generic(
"fast forward merge not possible".into(),
));
}
if !analysis.is_fast_forward() {
return Err(Error::Generic(
"fast forward merge not possible".into(),
));
}
if pref.is_no_fast_forward() {
return Err(Error::Generic("fast forward not wanted".into()));
}
if pref.is_no_fast_forward() {
return Err(Error::Generic("fast forward not wanted".into()));
}
//TODO: support merge on unborn
if analysis.is_unborn() {
return Err(Error::Generic("head is unborn".into()));
}
//TODO: support merge on unborn
if analysis.is_unborn() {
return Err(Error::Generic("head is unborn".into()));
}
repo.checkout_tree(upstream_commit.as_object(), None)?;
repo.checkout_tree(upstream_commit.as_object(), None)?;
repo.head()?.set_target(annotated.id(), "")?;
repo.head()?.set_target(annotated.id(), "")?;
Ok(())
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::sync::{
remotes::{fetch, push::push_branch},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare, write_commit_file,
},
};
pub mod test {
use super::*;
use crate::sync::{
remotes::{fetch, push::push},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare, write_commit_file,
},
};
#[test]
fn test_merge_fastforward() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
#[test]
fn test_merge_fastforward() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
// clone1
// clone1
let commit1 =
write_commit_file(&clone1, "test.txt", "test", "commit1");
let commit1 =
write_commit_file(&clone1, "test.txt", "test", "commit1");
push_branch(
&clone1_dir.path().to_str().unwrap().into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
push(
clone1_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.unwrap();
// clone2
debug_cmd_print(
&clone2_dir.path().to_str().unwrap().into(),
"git pull --ff",
);
// clone2
debug_cmd_print(
clone2_dir.path().to_str().unwrap(),
"git pull --ff",
);
let commit2 = write_commit_file(
&clone2,
"test2.txt",
"test",
"commit2",
);
let commit2 = write_commit_file(
&clone2,
"test2.txt",
"test",
"commit2",
);
push_branch(
&clone2_dir.path().to_str().unwrap().into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
push(
clone2_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.unwrap();
// clone1 again
// clone1 again
let bytes = fetch(
&clone1_dir.path().to_str().unwrap().into(),
"master",
None,
None,
)
.unwrap();
assert!(bytes > 0);
let bytes = fetch(
clone1_dir.path().to_str().unwrap(),
"master",
None,
None,
)
.unwrap();
assert!(bytes > 0);
let bytes = fetch(
&clone1_dir.path().to_str().unwrap().into(),
"master",
None,
None,
)
.unwrap();
assert_eq!(bytes, 0);
let bytes = fetch(
clone1_dir.path().to_str().unwrap(),
"master",
None,
None,
)
.unwrap();
assert_eq!(bytes, 0);
branch_merge_upstream_fastforward(
&clone1_dir.path().to_str().unwrap().into(),
"master",
)
.unwrap();
branch_merge_upstream_fastforward(
clone1_dir.path().to_str().unwrap(),
"master",
)
.unwrap();
let commits = get_commit_ids(&clone1, 10);
assert_eq!(commits.len(), 2);
assert_eq!(commits[1], commit1);
assert_eq!(commits[0], commit2);
}
let commits = get_commit_ids(&clone1, 10);
assert_eq!(commits.len(), 2);
assert_eq!(commits[1], commit1);
assert_eq!(commits[0], commit2);
}
}

View file

@ -1,356 +1,329 @@
//! merging from upstream (rebase)
use crate::{
error::{Error, Result},
sync::{
rebase::conflict_free_rebase, repository::repo, CommitId,
RepoPath,
},
error::{Error, Result},
sync::utils,
};
use git2::BranchType;
use scopetime::scope_time;
/// tries merging current branch with its upstream using rebase
/// trys merging current branch with its upstrema using rebase
pub fn merge_upstream_rebase(
repo_path: &RepoPath,
branch_name: &str,
) -> Result<CommitId> {
scope_time!("merge_upstream_rebase");
repo_path: &str,
branch_name: &str,
) -> Result<()> {
scope_time!("merge_upstream_rebase");
let repo = repo(repo_path)?;
if super::get_branch_name_repo(&repo)? != branch_name {
return Err(Error::Generic(String::from(
"can only rebase in head branch",
)));
}
let repo = utils::repo(repo_path)?;
if super::get_branch_name_repo(&repo)? != branch_name {
return Err(Error::Generic(String::from(
"can only rebase in head branch",
)));
}
let branch = repo.find_branch(branch_name, BranchType::Local)?;
let upstream = branch.upstream()?;
let upstream_commit = upstream.get().peel_to_commit()?;
let annotated_upstream =
repo.find_annotated_commit(upstream_commit.id())?;
let branch = repo.find_branch(branch_name, BranchType::Local)?;
let upstream = branch.upstream()?;
let upstream_commit = upstream.get().peel_to_commit()?;
let annotated_upstream =
repo.find_annotated_commit(upstream_commit.id())?;
conflict_free_rebase(&repo, &annotated_upstream)
let mut rebase =
repo.rebase(None, Some(&annotated_upstream), None, None)?;
let signature =
crate::sync::commit::signature_allow_undefined_name(&repo)?;
while let Some(op) = rebase.next() {
let _op = op?;
// dbg!(op.id());
if repo.index()?.has_conflicts() {
rebase.abort()?;
return Err(Error::Generic(String::from(
"conflicts while merging",
)));
}
rebase.commit(None, &signature, None)?;
}
if repo.index()?.has_conflicts() {
rebase.abort()?;
return Err(Error::Generic(String::from(
"conflicts while merging",
)));
}
rebase.finish(Some(&signature))?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::sync::{
branch_compare_upstream, get_commits_info,
remotes::{fetch, push::push_branch},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare, write_commit_file, write_commit_file_at,
},
RepoState,
};
use git2::{Repository, Time};
fn get_commit_msgs(r: &Repository) -> Vec<String> {
let commits = get_commit_ids(r, 10);
get_commits_info(
&r.workdir().unwrap().to_str().unwrap().into(),
&commits,
10,
)
.unwrap()
.into_iter()
.map(|c| c.message)
.collect()
}
#[test]
fn test_merge_normal() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone1_dir = clone1_dir.path().to_str().unwrap();
// clone1
let _commit1 = write_commit_file_at(
&clone1,
"test.txt",
"test",
"commit1",
git2::Time::new(0, 0),
);
assert!(!clone1.head_detached().unwrap());
push_branch(
&clone1_dir.into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
assert!(!clone1.head_detached().unwrap());
// clone2
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
let _commit2 = write_commit_file_at(
&clone2,
"test2.txt",
"test",
"commit2",
git2::Time::new(1, 0),
);
assert!(!clone2.head_detached().unwrap());
push_branch(
&clone2_dir.into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
assert!(!clone2.head_detached().unwrap());
// clone1
let _commit3 = write_commit_file_at(
&clone1,
"test3.txt",
"test",
"commit3",
git2::Time::new(2, 0),
);
assert!(!clone1.head_detached().unwrap());
//lets fetch from origin
let bytes =
fetch(&clone1_dir.into(), "master", None, None).unwrap();
assert!(bytes > 0);
//we should be one commit behind
assert_eq!(
branch_compare_upstream(&clone1_dir.into(), "master")
.unwrap()
.behind,
1
);
// debug_cmd_print(clone1_dir, "git status");
assert!(!clone1.head_detached().unwrap());
merge_upstream_rebase(&clone1_dir.into(), "master").unwrap();
debug_cmd_print(&clone1_dir.into(), "git log");
let state =
crate::sync::repo_state(&clone1_dir.into()).unwrap();
assert_eq!(state, RepoState::Clean);
let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![
String::from("commit3"),
String::from("commit2"),
String::from("commit1")
]
);
assert!(!clone1.head_detached().unwrap());
}
#[test]
fn test_merge_multiple() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone1_dir = clone1_dir.path().to_str().unwrap();
// clone1
write_commit_file_at(
&clone1,
"test.txt",
"test",
"commit1",
Time::new(0, 0),
);
push_branch(
&clone1_dir.into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
// clone2
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
write_commit_file_at(
&clone2,
"test2.txt",
"test",
"commit2",
Time::new(1, 0),
);
push_branch(
&clone2_dir.into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
// clone1
write_commit_file_at(
&clone1,
"test3.txt",
"test",
"commit3",
Time::new(2, 0),
);
write_commit_file_at(
&clone1,
"test4.txt",
"test",
"commit4",
Time::new(3, 0),
);
//lets fetch from origin
fetch(&clone1_dir.into(), "master", None, None).unwrap();
merge_upstream_rebase(&clone1_dir.into(), "master").unwrap();
debug_cmd_print(&clone1_dir.into(), "git log");
let state =
crate::sync::repo_state(&clone1_dir.into()).unwrap();
assert_eq!(state, RepoState::Clean);
let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![
String::from("commit4"),
String::from("commit3"),
String::from("commit2"),
String::from("commit1")
]
);
assert!(!clone1.head_detached().unwrap());
}
#[test]
fn test_merge_conflict() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone1_dir = clone1_dir.path().to_str().unwrap();
// clone1
let _commit1 =
write_commit_file(&clone1, "test.txt", "test", "commit1");
push_branch(
&clone1_dir.into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
// clone2
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
let _commit2 = write_commit_file(
&clone2,
"test2.txt",
"test",
"commit2",
);
push_branch(
&clone2_dir.into(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
// clone1
let _commit3 =
write_commit_file(&clone1, "test2.txt", "foo", "commit3");
let bytes =
fetch(&clone1_dir.into(), "master", None, None).unwrap();
assert!(bytes > 0);
assert_eq!(
branch_compare_upstream(&clone1_dir.into(), "master")
.unwrap()
.behind,
1
);
let res = merge_upstream_rebase(&clone1_dir.into(), "master");
assert!(res.is_err());
let state =
crate::sync::repo_state(&clone1_dir.into()).unwrap();
assert_eq!(state, RepoState::Clean);
use super::*;
use crate::sync::{
branch_compare_upstream, get_commits_info,
remotes::{fetch, push::push},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare, write_commit_file, write_commit_file_at,
},
RepoState,
};
use git2::{Repository, Time};
fn get_commit_msgs(r: &Repository) -> Vec<String> {
let commits = get_commit_ids(r, 10);
get_commits_info(
r.workdir().unwrap().to_str().unwrap(),
&commits,
10,
)
.unwrap()
.into_iter()
.map(|c| c.message)
.collect()
}
#[test]
fn test_merge_normal() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![String::from("commit3"), String::from("commit1")]
);
}
let clone1_dir = clone1_dir.path().to_str().unwrap();
// clone1
let _commit1 = write_commit_file_at(
&clone1,
"test.txt",
"test",
"commit1",
git2::Time::new(0, 0),
);
assert_eq!(clone1.head_detached().unwrap(), false);
push(clone1_dir, "origin", "master", false, None, None)
.unwrap();
assert_eq!(clone1.head_detached().unwrap(), false);
// clone2
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
let _commit2 = write_commit_file_at(
&clone2,
"test2.txt",
"test",
"commit2",
git2::Time::new(1, 0),
);
assert_eq!(clone2.head_detached().unwrap(), false);
push(clone2_dir, "origin", "master", false, None, None)
.unwrap();
assert_eq!(clone2.head_detached().unwrap(), false);
// clone1
let _commit3 = write_commit_file_at(
&clone1,
"test3.txt",
"test",
"commit3",
git2::Time::new(2, 0),
);
assert_eq!(clone1.head_detached().unwrap(), false);
//lets fetch from origin
let bytes = fetch(clone1_dir, "master", None, None).unwrap();
assert!(bytes > 0);
//we should be one commit behind
assert_eq!(
branch_compare_upstream(clone1_dir, "master")
.unwrap()
.behind,
1
);
// debug_cmd_print(clone1_dir, "git status");
assert_eq!(clone1.head_detached().unwrap(), false);
merge_upstream_rebase(clone1_dir, "master").unwrap();
debug_cmd_print(clone1_dir, "git log");
let state = crate::sync::repo_state(clone1_dir).unwrap();
assert_eq!(state, RepoState::Clean);
let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![
String::from("commit3"),
String::from("commit2"),
String::from("commit1")
]
);
assert_eq!(clone1.head_detached().unwrap(), false);
}
#[test]
fn test_merge_multiple() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone1_dir = clone1_dir.path().to_str().unwrap();
// clone1
write_commit_file_at(
&clone1,
"test.txt",
"test",
"commit1",
Time::new(0, 0),
);
push(clone1_dir, "origin", "master", false, None, None)
.unwrap();
// clone2
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
write_commit_file_at(
&clone2,
"test2.txt",
"test",
"commit2",
Time::new(1, 0),
);
push(clone2_dir, "origin", "master", false, None, None)
.unwrap();
// clone1
write_commit_file_at(
&clone1,
"test3.txt",
"test",
"commit3",
Time::new(2, 0),
);
write_commit_file_at(
&clone1,
"test4.txt",
"test",
"commit4",
Time::new(3, 0),
);
//lets fetch from origin
fetch(clone1_dir, "master", None, None).unwrap();
merge_upstream_rebase(clone1_dir, "master").unwrap();
debug_cmd_print(clone1_dir, "git log");
let state = crate::sync::repo_state(clone1_dir).unwrap();
assert_eq!(state, RepoState::Clean);
let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![
String::from("commit4"),
String::from("commit3"),
String::from("commit2"),
String::from("commit1")
]
);
assert_eq!(clone1.head_detached().unwrap(), false);
}
#[test]
fn test_merge_conflict() {
let (r1_dir, _repo) = repo_init_bare().unwrap();
let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone1_dir = clone1_dir.path().to_str().unwrap();
// clone1
let _commit1 =
write_commit_file(&clone1, "test.txt", "test", "commit1");
push(clone1_dir, "origin", "master", false, None, None)
.unwrap();
// clone2
let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
let clone2_dir = clone2_dir.path().to_str().unwrap();
let _commit2 = write_commit_file(
&clone2,
"test2.txt",
"test",
"commit2",
);
push(clone2_dir, "origin", "master", false, None, None)
.unwrap();
// clone1
let _commit3 =
write_commit_file(&clone1, "test2.txt", "foo", "commit3");
let bytes = fetch(clone1_dir, "master", None, None).unwrap();
assert!(bytes > 0);
assert_eq!(
branch_compare_upstream(clone1_dir, "master")
.unwrap()
.behind,
1
);
let res = merge_upstream_rebase(clone1_dir, "master");
assert!(res.is_err());
let state = crate::sync::repo_state(clone1_dir).unwrap();
assert_eq!(state, RepoState::Clean);
let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![String::from("commit3"), String::from("commit1")]
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,71 +1,67 @@
//! renaming of branches
use crate::{
error::Result,
sync::{repository::repo, RepoPath},
};
use crate::{error::Result, sync::utils};
use scopetime::scope_time;
/// Rename the branch reference
pub fn rename_branch(
repo_path: &RepoPath,
branch_ref: &str,
new_name: &str,
repo_path: &str,
branch_ref: &str,
new_name: &str,
) -> Result<()> {
scope_time!("rename_branch");
scope_time!("delete_branch");
let repo = repo(repo_path)?;
let branch_as_ref = repo.find_reference(branch_ref)?;
let mut branch = git2::Branch::wrap(branch_as_ref);
branch.rename(new_name, true)?;
let repo = utils::repo(repo_path)?;
let branch_as_ref = repo.find_reference(branch_ref)?;
let mut branch = git2::Branch::wrap(branch_as_ref);
branch.rename(new_name, true)?;
Ok(())
Ok(())
}
#[cfg(test)]
mod test {
use super::super::{checkout_branch, create_branch, RepoPath};
use super::rename_branch;
use crate::sync::tests::repo_init;
use super::super::*;
use super::rename_branch;
use crate::sync::tests::repo_init;
#[test]
fn test_rename_branch() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_rename_branch() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
create_branch(repo_path, "branch1").unwrap();
create_branch(repo_path, "branch1").unwrap();
checkout_branch(repo_path, "branch1").unwrap();
checkout_branch(repo_path, "refs/heads/branch1").unwrap();
assert_eq!(
repo.branches(None)
.unwrap()
.next()
.unwrap()
.unwrap()
.0
.name()
.unwrap()
.unwrap(),
"branch1"
);
assert_eq!(
repo.branches(None)
.unwrap()
.nth(0)
.unwrap()
.unwrap()
.0
.name()
.unwrap()
.unwrap(),
"branch1"
);
rename_branch(repo_path, "refs/heads/branch1", "AnotherName")
.unwrap();
rename_branch(repo_path, "refs/heads/branch1", "AnotherName")
.unwrap();
assert_eq!(
repo.branches(None)
.unwrap()
.next()
.unwrap()
.unwrap()
.0
.name()
.unwrap()
.unwrap(),
"AnotherName"
);
}
assert_eq!(
repo.branches(None)
.unwrap()
.nth(0)
.unwrap()
.unwrap()
.0
.name()
.unwrap()
.unwrap(),
"AnotherName"
);
}
}

View file

@ -1,548 +1,338 @@
//! Git Api for Commits
use super::{CommitId, RepoPath};
use crate::sync::sign::{SignBuilder, SignError};
use crate::{
error::{Error, Result},
sync::{repository::repo, utils::get_head_repo},
};
use git2::{
message_prettify, ErrorCode, ObjectType, Repository, Signature,
};
use super::{utils::repo, CommitId};
use crate::{error::Result, sync::utils::get_head_repo};
use git2::{ErrorCode, ObjectType, Repository, Signature};
use scopetime::scope_time;
///
pub fn amend(
repo_path: &RepoPath,
id: CommitId,
msg: &str,
repo_path: &str,
id: CommitId,
msg: &str,
) -> Result<CommitId> {
scope_time!("amend");
scope_time!("amend");
let repo = repo(repo_path)?;
let config = repo.config()?;
let repo = repo(repo_path)?;
let commit = repo.find_commit(id.into())?;
let commit = repo.find_commit(id.into())?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let new_id = commit.amend(
Some("HEAD"),
None,
None,
None,
Some(msg),
Some(&tree),
)?;
if config.get_bool("commit.gpgsign").unwrap_or(false) {
// HACK: we undo the last commit and create a new one
use crate::sync::utils::undo_last_commit;
let head = get_head_repo(&repo)?;
if head == commit.id().into() {
undo_last_commit(repo_path)?;
return self::commit(repo_path, msg);
}
return Err(Error::SignAmendNonLastCommit);
}
let committer = signature_allow_undefined_name(&repo)?;
let new_id = commit.amend(
Some("HEAD"),
None,
Some(&committer), // Passing a value will overwrite the committer.
None,
Some(msg),
Some(&tree),
)?;
Ok(CommitId::new(new_id))
Ok(CommitId::new(new_id))
}
/// Wrap `Repository::signature` to allow unknown user.name.
///
/// See <https://github.com/gitui-org/gitui/issues/79>.
/// See <https://github.com/extrawurst/gitui/issues/79>.
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn signature_allow_undefined_name(
repo: &Repository,
repo: &Repository,
) -> std::result::Result<Signature<'_>, git2::Error> {
let signature = repo.signature();
let signature = repo.signature();
if let Err(ref e) = signature {
if e.code() == ErrorCode::NotFound {
let config = repo.config()?;
if let Err(ref e) = signature {
if e.code() == ErrorCode::NotFound {
let config = repo.config()?;
if let (Err(_), Ok(email_entry)) = (
config.get_entry("user.name"),
config.get_entry("user.email"),
) {
if let Some(email) = email_entry.value() {
return Signature::now("unknown", email);
}
};
}
}
if let (Err(_), Ok(email_entry)) = (
config.get_entry("user.name"),
config.get_entry("user.email"),
) {
if let Some(email) = email_entry.value() {
return Signature::now("unknown", email);
}
};
}
}
signature
signature
}
/// this does not run any git hooks, git-hooks have to be executed manually, checkout `hooks_commit_msg` for example
pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
scope_time!("commit");
/// this does not run any git hooks
pub fn commit(repo_path: &str, msg: &str) -> Result<CommitId> {
scope_time!("commit");
let repo = repo(repo_path)?;
let config = repo.config()?;
let signature = signature_allow_undefined_name(&repo)?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let repo = repo(repo_path)?;
let parents = if let Ok(id) = get_head_repo(&repo) {
vec![repo.find_commit(id.into())?]
} else {
Vec::new()
};
let signature = signature_allow_undefined_name(&repo)?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let parents = parents.iter().collect::<Vec<_>>();
let parents = if let Ok(id) = get_head_repo(&repo) {
vec![repo.find_commit(id.into())?]
} else {
Vec::new()
};
let commit_id = if config
.get_bool("commit.gpgsign")
.unwrap_or(false)
{
let buffer = repo.commit_create_buffer(
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?;
let parents = parents.iter().collect::<Vec<_>>();
let commit = std::str::from_utf8(&buffer).map_err(|_e| {
SignError::Shellout("utf8 conversion error".to_string())
})?;
let signer = SignBuilder::from_gitconfig(&repo, &config)?;
let (signature, signature_field) = signer.sign(&buffer)?;
let commit_id = repo.commit_signed(
commit,
&signature,
signature_field.as_deref(),
)?;
// manually advance to the new commit ID
// repo.commit does that on its own, repo.commit_signed does not
// if there is no head, read default branch or default to "master"
if let Ok(mut head) = repo.head() {
head.set_target(commit_id, msg)?;
} else {
let default_branch_name = config
.get_str("init.defaultBranch")
.unwrap_or("master");
repo.reference(
&format!("refs/heads/{default_branch_name}"),
commit_id,
true,
msg,
)?;
}
commit_id
} else {
repo.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?
};
Ok(commit_id.into())
Ok(repo
.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?
.into())
}
/// Tag a commit.
///
/// This function will return an `Err(…)` variant if the tags name is refused
/// by git or if the tag already exists.
pub fn tag_commit(
repo_path: &RepoPath,
commit_id: &CommitId,
tag: &str,
message: Option<&str>,
pub fn tag(
repo_path: &str,
commit_id: &CommitId,
tag: &str,
) -> Result<CommitId> {
scope_time!("tag_commit");
scope_time!("tag");
let repo = repo(repo_path)?;
let repo = repo(repo_path)?;
let object_id = commit_id.get_oid();
let target =
repo.find_object(object_id, Some(ObjectType::Commit))?;
let signature = signature_allow_undefined_name(&repo)?;
let object_id = commit_id.get_oid();
let target =
repo.find_object(object_id, Some(ObjectType::Commit))?;
let c = if let Some(message) = message {
let signature = signature_allow_undefined_name(&repo)?;
repo.tag(tag, &target, &signature, message, false)?.into()
} else {
repo.tag_lightweight(tag, &target, false)?.into()
};
Ok(c)
}
/// Loads the comment prefix from config & uses it to prettify commit messages
pub fn commit_message_prettify(
repo_path: &RepoPath,
message: String,
) -> Result<String> {
let comment_char = repo(repo_path)?
.config()?
.get_string("core.commentChar")
.ok()
.and_then(|char_string| char_string.chars().next())
.unwrap_or('#') as u8;
Ok(message_prettify(message, Some(comment_char))?)
Ok(repo.tag(tag, &target, &signature, "", false)?.into())
}
#[cfg(test)]
mod tests {
use crate::error::Result;
use crate::sync::tags::Tag;
use crate::sync::RepoPath;
use crate::sync::{
commit, get_commit_details, get_commit_files, stage_add_file,
tags::get_tags,
tests::{get_statuses, repo_init, repo_init_empty},
utils::get_head,
LogWalker,
};
use commit::{amend, commit_message_prettify, tag_commit};
use git2::Repository;
use std::{fs::File, io::Write, path::Path};
fn count_commits(repo: &Repository, max: usize) -> usize {
let mut items = Vec::new();
let mut walk = LogWalker::new(repo, max).unwrap();
walk.read(&mut items).unwrap();
items.len()
}
use crate::error::Result;
use crate::sync::{
commit, get_commit_details, get_commit_files, stage_add_file,
tags::get_tags,
tests::{get_statuses, repo_init, repo_init_empty},
utils::get_head,
LogWalker,
};
use commit::{amend, tag};
use git2::Repository;
use std::{fs::File, io::Write, path::Path};
#[test]
fn test_commit() {
let file_path = Path::new("foo");
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
fn count_commits(repo: &Repository, max: usize) -> usize {
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo, max).unwrap();
walk.read(&mut items).unwrap();
items.len()
}
File::create(root.join(file_path))
.unwrap()
.write_all(b"test\nfoo")
.unwrap();
#[test]
fn test_commit() {
let file_path = Path::new("foo");
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
assert_eq!(get_statuses(repo_path), (1, 0));
File::create(&root.join(file_path))
.unwrap()
.write_all(b"test\nfoo")
.unwrap();
stage_add_file(repo_path, file_path).unwrap();
assert_eq!(get_statuses(repo_path), (1, 0));
assert_eq!(get_statuses(repo_path), (0, 1));
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "commit msg").unwrap();
assert_eq!(get_statuses(repo_path), (0, 1));
assert_eq!(get_statuses(repo_path), (0, 0));
}
commit(repo_path, "commit msg").unwrap();
#[test]
fn test_commit_in_empty_repo() {
let file_path = Path::new("foo");
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();
assert_eq!(get_statuses(repo_path), (0, 0));
}
assert_eq!(get_statuses(repo_path), (0, 0));
#[test]
fn test_commit_in_empty_repo() {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))
.unwrap()
.write_all(b"test\nfoo")
.unwrap();
assert_eq!(get_statuses(repo_path), (0, 0));
assert_eq!(get_statuses(repo_path), (1, 0));
File::create(&root.join(file_path))
.unwrap()
.write_all(b"test\nfoo")
.unwrap();
stage_add_file(repo_path, file_path).unwrap();
assert_eq!(get_statuses(repo_path), (1, 0));
assert_eq!(get_statuses(repo_path), (0, 1));
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "commit msg").unwrap();
assert_eq!(get_statuses(repo_path), (0, 1));
assert_eq!(get_statuses(repo_path), (0, 0));
}
commit(repo_path, "commit msg").unwrap();
#[test]
fn test_amend() -> Result<()> {
let file_path1 = Path::new("foo");
let file_path2 = Path::new("foo2");
let (_td, repo) = repo_init_empty()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
assert_eq!(get_statuses(repo_path), (0, 0));
}
File::create(root.join(file_path1))?.write_all(b"test1")?;
#[test]
fn test_amend() -> Result<()> {
let file_path1 = Path::new("foo");
let file_path2 = Path::new("foo2");
let (_td, repo) = repo_init_empty()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
stage_add_file(repo_path, file_path1)?;
let id = commit(repo_path, "commit msg")?;
File::create(&root.join(file_path1))?.write_all(b"test1")?;
assert_eq!(count_commits(&repo, 10), 1);
stage_add_file(repo_path, file_path1)?;
let id = commit(repo_path, "commit msg")?;
File::create(root.join(file_path2))?.write_all(b"test2")?;
assert_eq!(count_commits(&repo, 10), 1);
stage_add_file(repo_path, file_path2)?;
File::create(&root.join(file_path2))?.write_all(b"test2")?;
let new_id = amend(repo_path, id, "amended")?;
stage_add_file(repo_path, file_path2)?;
assert_eq!(count_commits(&repo, 10), 1);
let new_id = amend(repo_path, id, "amended")?;
let details = get_commit_details(repo_path, new_id)?;
assert_eq!(details.message.unwrap().subject, "amended");
assert_eq!(count_commits(&repo, 10), 1);
let files = get_commit_files(repo_path, new_id, None)?;
let details = get_commit_details(repo_path, new_id)?;
assert_eq!(details.message.unwrap().subject, "amended");
assert_eq!(files.len(), 2);
let files = get_commit_files(repo_path, new_id)?;
let head = get_head(repo_path)?;
assert_eq!(files.len(), 2);
assert_eq!(head, new_id);
let head = get_head(repo_path)?;
Ok(())
}
assert_eq!(head, new_id);
#[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();
Ok(())
}
File::create(root.join(file_path1))
.unwrap()
.write_all(b"test1")
.unwrap();
#[test]
fn test_tag() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
stage_add_file(repo_path, file_path1).unwrap();
let id = commit(repo_path, "commit msg").unwrap();
File::create(&root.join(file_path))?
.write_all(b"test\nfoo")?;
let amended_details =
get_commit_details(repo_path, id).unwrap();
stage_add_file(repo_path, file_path)?;
assert_eq!(amended_details.committer, None);
let new_id = commit(repo_path, "commit msg")?;
File::create(root.join(file_path2))
.unwrap()
.write_all(b"test2")
.unwrap();
tag(repo_path, &new_id, "tag")?;
stage_add_file(repo_path, file_path2).unwrap();
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec!["tag"]
);
repo.config()
.unwrap()
.set_str("user.name", "changed name")
.unwrap();
repo.config()
.unwrap()
.set_str("user.email", "changed@example.com")
.unwrap();
assert!(matches!(tag(repo_path, &new_id, "tag"), Err(_)));
let new_id = amend(repo_path, id, "amended").unwrap();
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec!["tag"]
);
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");
}
tag(repo_path, &new_id, "second-tag")?;
#[test]
fn test_tag() -> Result<()> {
let file_path = Path::new("foo");
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();
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec!["second-tag", "tag"]
);
File::create(root.join(file_path))?
.write_all(b"test\nfoo")?;
Ok(())
}
stage_add_file(repo_path, file_path)?;
/// Beware: this test has to be run with a `$HOME/.gitconfig` that has
/// `user.email` not set. Otherwise, git falls back to the value of
/// `user.email` in `$HOME/.gitconfig` and this test fails.
///
/// As of February 2021, `repo_init_empty` sets all git config locations
/// to an empty temporary directory, so this constraint is met.
#[test]
fn test_empty_email() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let new_id = commit(repo_path, "commit msg")?;
File::create(&root.join(file_path))?
.write_all(b"test\nfoo")?;
tag_commit(repo_path, &new_id, "tag", None)?;
stage_add_file(repo_path, file_path)?;
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec![Tag::new("tag")]
);
repo.config()?.remove("user.email")?;
assert!(tag_commit(repo_path, &new_id, "tag", None).is_err());
let error = commit(repo_path, "commit msg");
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec![Tag::new("tag")]
);
assert!(matches!(error, Err(_)));
tag_commit(repo_path, &new_id, "second-tag", None)?;
repo.config()?.set_str("user.email", "email")?;
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec![Tag::new("second-tag"), Tag::new("tag")]
);
let success = commit(repo_path, "commit msg");
Ok(())
}
assert!(matches!(success, Ok(_)));
assert_eq!(count_commits(&repo, 10), 1);
#[test]
fn test_tag_with_message() -> Result<()> {
let file_path = Path::new("foo");
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();
let details =
get_commit_details(repo_path, success.unwrap()).unwrap();
File::create(root.join(file_path))?
.write_all(b"test\nfoo")?;
assert_eq!(details.author.name, "name");
assert_eq!(details.author.email, "email");
stage_add_file(repo_path, file_path)?;
Ok(())
}
let new_id = commit(repo_path, "commit msg")?;
/// See comment to `test_empty_email`.
#[test]
fn test_empty_name() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
tag_commit(repo_path, &new_id, "tag", Some("tag-message"))?;
File::create(&root.join(file_path))?
.write_all(b"test\nfoo")?;
assert_eq!(
get_tags(repo_path).unwrap()[&new_id][0]
.annotation
.as_ref()
.unwrap(),
"tag-message"
);
stage_add_file(repo_path, file_path)?;
Ok(())
}
repo.config()?.remove("user.name")?;
/// Beware: this test has to be run with a `$HOME/.gitconfig` that has
/// `user.email` not set. Otherwise, git falls back to the value of
/// `user.email` in `$HOME/.gitconfig` and this test fails.
///
/// As of February 2021, `repo_init_empty` sets all git config locations
/// to an empty temporary directory, so this constraint is met.
#[test]
fn test_empty_email() -> Result<()> {
let file_path = Path::new("foo");
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();
let mut success = commit(repo_path, "commit msg");
File::create(root.join(file_path))?
.write_all(b"test\nfoo")?;
assert!(matches!(success, Ok(_)));
assert_eq!(count_commits(&repo, 10), 1);
stage_add_file(repo_path, file_path)?;
let mut details =
get_commit_details(repo_path, success.unwrap()).unwrap();
repo.config()?.remove("user.email")?;
assert_eq!(details.author.name, "unknown");
assert_eq!(details.author.email, "email");
let error = commit(repo_path, "commit msg");
repo.config()?.set_str("user.name", "name")?;
assert!(error.is_err());
success = commit(repo_path, "commit msg");
repo.config()?.set_str("user.email", "email")?;
assert!(matches!(success, Ok(_)));
assert_eq!(count_commits(&repo, 10), 2);
let success = commit(repo_path, "commit msg");
details =
get_commit_details(repo_path, success.unwrap()).unwrap();
assert!(success.is_ok());
assert_eq!(count_commits(&repo, 10), 1);
assert_eq!(details.author.name, "name");
assert_eq!(details.author.email, "email");
let details =
get_commit_details(repo_path, success.unwrap()).unwrap();
assert_eq!(details.author.name, "name");
assert_eq!(details.author.email, "email");
Ok(())
}
/// See comment to `test_empty_email`.
#[test]
fn test_empty_name() -> Result<()> {
let file_path = Path::new("foo");
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_path))?
.write_all(b"test\nfoo")?;
stage_add_file(repo_path, file_path)?;
repo.config()?.remove("user.name")?;
let mut success = commit(repo_path, "commit msg");
assert!(success.is_ok());
assert_eq!(count_commits(&repo, 10), 1);
let mut details =
get_commit_details(repo_path, success.unwrap()).unwrap();
assert_eq!(details.author.name, "unknown");
assert_eq!(details.author.email, "email");
repo.config()?.set_str("user.name", "name")?;
success = commit(repo_path, "commit msg");
assert!(success.is_ok());
assert_eq!(count_commits(&repo, 10), 2);
details =
get_commit_details(repo_path, success.unwrap()).unwrap();
assert_eq!(details.author.name, "name");
assert_eq!(details.author.email, "email");
Ok(())
}
#[test]
fn test_empty_comment_char() -> Result<()> {
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();
let message = commit_message_prettify(
repo_path,
"#This is a test message\nTest".to_owned(),
)?;
assert_eq!(message, "Test\n");
Ok(())
}
#[test]
fn test_with_comment_char() -> Result<()> {
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();
repo.config()?.set_str("core.commentChar", ";")?;
let message = commit_message_prettify(
repo_path,
";This is a test message\nTest".to_owned(),
)?;
assert_eq!(message, "Test\n");
Ok(())
}
Ok(())
}
}

View file

@ -1,219 +1,171 @@
use super::{commits_info::get_message, CommitId, RepoPath};
use crate::{error::Result, sync::repository::repo};
use super::{commits_info::get_message, utils::repo, CommitId};
use crate::error::Result;
use git2::Signature;
use scopetime::scope_time;
///
#[derive(Debug, PartialEq, Eq, Default, Clone)]
#[derive(Debug, PartialEq, Default, Clone)]
pub struct CommitSignature {
///
pub name: String,
///
pub email: String,
/// time in secs since Unix epoch
pub time: i64,
///
pub name: String,
///
pub email: String,
/// time in secs since Unix epoch
pub time: i64,
}
impl CommitSignature {
/// convert from git2-rs `Signature`
pub fn from(s: &Signature<'_>) -> Self {
Self {
name: s.name().unwrap_or("").to_string(),
email: s.email().unwrap_or("").to_string(),
/// convert from git2-rs `Signature`
pub fn from(s: &Signature<'_>) -> Self {
Self {
name: s.name().unwrap_or("").to_string(),
email: s.email().unwrap_or("").to_string(),
time: s.when().seconds(),
}
}
time: s.when().seconds(),
}
}
}
///
#[derive(Default, Clone)]
pub struct CommitMessage {
/// first line
pub subject: String,
/// remaining lines if more than one
pub body: Option<String>,
/// first line
pub subject: String,
/// remaining lines if more than one
pub body: Option<String>,
}
impl CommitMessage {
///
pub fn from(s: &str) -> Self {
let mut lines = s.lines();
let subject = lines.next().map_or_else(
String::new,
std::string::ToString::to_string,
);
///
pub fn from(s: &str) -> Self {
let mut lines = s.lines();
let subject = lines.next().map_or_else(
String::new,
std::string::ToString::to_string,
);
let body: Vec<String> =
lines.map(std::string::ToString::to_string).collect();
let body: Vec<String> =
lines.map(std::string::ToString::to_string).collect();
Self {
subject,
body: if body.is_empty() {
None
} else {
Some(body.join("\n"))
},
}
}
Self {
subject,
body: if body.is_empty() {
None
} else {
Some(body.join("\n"))
},
}
}
///
pub fn combine(self) -> String {
if let Some(body) = self.body {
format!("{}\n{body}", self.subject)
} else {
self.subject
}
}
///
pub fn combine(self) -> String {
if let Some(body) = self.body {
format!("{}\n{}", self.subject, body)
} else {
self.subject
}
}
}
///
#[derive(Default, Clone)]
pub struct CommitDetails {
///
pub author: CommitSignature,
/// committer when differs to `author` otherwise None
pub committer: Option<CommitSignature>,
///
pub message: Option<CommitMessage>,
///
pub hash: String,
}
impl CommitDetails {
///
pub fn short_hash(&self) -> &str {
&self.hash[0..7]
}
}
/// Get the author of a commit.
pub fn get_author_of_commit<'a>(
commit: &'a git2::Commit<'a>,
mailmap: &git2::Mailmap,
) -> git2::Signature<'a> {
match commit.author_with_mailmap(mailmap) {
Ok(author) => author,
Err(e) => {
log::error!(
"Couldn't get author with mailmap for {} (message: {:?}): {e}",
commit.id(),
commit.message(),
);
commit.author()
}
}
}
/// Get the committer of a commit.
pub fn get_committer_of_commit<'a>(
commit: &'a git2::Commit<'a>,
mailmap: &git2::Mailmap,
) -> git2::Signature<'a> {
match commit.committer_with_mailmap(mailmap) {
Ok(committer) => committer,
Err(e) => {
log::error!(
"Couldn't get committer with mailmap for {} (message: {:?}): {e}",
commit.id(),
commit.message(),
);
commit.committer()
}
}
///
pub author: CommitSignature,
/// committer when differs to `author` otherwise None
pub committer: Option<CommitSignature>,
///
pub message: Option<CommitMessage>,
///
pub hash: String,
}
///
pub fn get_commit_details(
repo_path: &RepoPath,
id: CommitId,
repo_path: &str,
id: CommitId,
) -> Result<CommitDetails> {
scope_time!("get_commit_details");
scope_time!("get_commit_details");
let repo = repo(repo_path)?;
let mailmap = repo.mailmap()?;
let repo = repo(repo_path)?;
let commit = repo.find_commit(id.into())?;
let commit = repo.find_commit(id.into())?;
let author = CommitSignature::from(&get_author_of_commit(
&commit, &mailmap,
));
let committer = CommitSignature::from(&get_committer_of_commit(
&commit, &mailmap,
));
let author = CommitSignature::from(&commit.author());
let committer = CommitSignature::from(&commit.committer());
let committer = if author == committer {
None
} else {
Some(committer)
};
let committer = if author == committer {
None
} else {
Some(committer)
};
let msg =
CommitMessage::from(get_message(&commit, None).as_str());
let msg =
CommitMessage::from(get_message(&commit, None).as_str());
let details = CommitDetails {
author,
committer,
message: Some(msg),
hash: id.to_string(),
};
let details = CommitDetails {
author,
committer,
message: Some(msg),
hash: id.to_string(),
};
Ok(details)
Ok(details)
}
#[cfg(test)]
mod tests {
use super::{get_commit_details, CommitMessage};
use crate::{
error::Result,
sync::{
commit, stage_add_file, tests::repo_init_empty, RepoPath,
},
};
use std::{fs::File, io::Write, path::Path};
#[test]
fn test_msg_invalid_utf8() -> Result<()> {
let file_path = Path::new("foo");
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();
use super::{get_commit_details, CommitMessage};
use crate::error::Result;
use crate::sync::{
commit, stage_add_file, tests::repo_init_empty,
};
use std::{fs::File, io::Write, path::Path};
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
#[test]
fn test_msg_invalid_utf8() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let msg = invalidstring::invalid_utf8("test msg");
let id = commit(repo_path, msg.as_str()).unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let res = get_commit_details(repo_path, id).unwrap();
let msg = invalidstring::invalid_utf8("test msg");
let id = commit(repo_path, msg.as_str()).unwrap();
assert!(res
.message
.as_ref()
.unwrap()
.subject
.starts_with("test msg"));
let res = get_commit_details(repo_path, id).unwrap();
Ok(())
}
dbg!(&res.message.as_ref().unwrap().subject);
assert_eq!(
res.message
.as_ref()
.unwrap()
.subject
.starts_with("test msg"),
true
);
#[test]
fn test_msg_linefeeds() -> Result<()> {
let msg = CommitMessage::from("foo\nbar\r\ntest");
Ok(())
}
assert_eq!(msg.subject, String::from("foo"),);
assert_eq!(msg.body, Some(String::from("bar\ntest")),);
#[test]
fn test_msg_linefeeds() -> Result<()> {
let msg = CommitMessage::from("foo\nbar\r\ntest");
Ok(())
}
assert_eq!(msg.subject, String::from("foo"),);
assert_eq!(msg.body, Some(String::from("bar\ntest")),);
#[test]
fn test_commit_message_combine() -> Result<()> {
let msg = CommitMessage::from("foo\nbar\r\ntest");
Ok(())
}
assert_eq!(msg.combine(), String::from("foo\nbar\ntest"));
#[test]
fn test_commit_message_combine() -> Result<()> {
let msg = CommitMessage::from("foo\nbar\r\ntest");
Ok(())
}
assert_eq!(msg.combine(), String::from("foo\nbar\ntest"));
Ok(())
}
}

View file

@ -1,269 +1,173 @@
//! Functions for getting infos about files in commits
use super::{diff::DiffOptions, CommitId, RepoPath};
use super::{stash::is_stash_commit, utils::repo, CommitId};
use crate::{
error::Result,
sync::{get_stashes, repository::repo},
StatusItem, StatusItemType,
error::Error, error::Result, StatusItem, StatusItemType,
};
use git2::{Diff, Repository};
use git2::{Diff, DiffDelta, DiffOptions, Repository};
use scopetime::scope_time;
use std::collections::HashSet;
/// struct containing a new and an old version
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct OldNew<T> {
/// The old version
pub old: T,
/// The new version
pub new: T,
}
/// Sort two commits.
pub fn sort_commits(
repo: &Repository,
commits: (CommitId, CommitId),
) -> Result<OldNew<CommitId>> {
if repo.graph_descendant_of(
commits.0.get_oid(),
commits.1.get_oid(),
)? {
Ok(OldNew {
old: commits.1,
new: commits.0,
})
} else {
Ok(OldNew {
old: commits.0,
new: commits.1,
})
}
}
/// get all files that are part of a commit
pub fn get_commit_files(
repo_path: &RepoPath,
id: CommitId,
other: Option<CommitId>,
repo_path: &str,
id: CommitId,
) -> Result<Vec<StatusItem>> {
scope_time!("get_commit_files");
scope_time!("get_commit_files");
let repo = repo(repo_path)?;
let repo = repo(repo_path)?;
let diff = if let Some(other) = other {
get_compare_commits_diff(
&repo,
sort_commits(&repo, (id, other))?,
None,
None,
)?
} else {
get_commit_diff(
&repo,
id,
None,
None,
Some(&get_stashes(repo_path)?.into_iter().collect()),
)?
};
let diff = get_commit_diff(&repo, id, None)?;
let res = diff
.deltas()
.map(|delta| {
let status = StatusItemType::from(delta.status());
let mut res = Vec::new();
StatusItem {
path: delta
.new_file()
.path()
.map(|p| p.to_str().unwrap_or("").to_string())
.unwrap_or_default(),
status,
}
})
.collect::<Vec<_>>();
diff.foreach(
&mut |delta: DiffDelta<'_>, _progress| {
res.push(StatusItem {
path: delta
.new_file()
.path()
.map(|p| p.to_str().unwrap_or("").to_string())
.unwrap_or_default(),
status: StatusItemType::from(delta.status()),
});
true
},
None,
None,
None,
)?;
Ok(res)
Ok(res)
}
/// get diff of two arbitrary commits
#[allow(clippy::needless_pass_by_value)]
pub fn get_compare_commits_diff(
repo: &Repository,
ids: OldNew<CommitId>,
pathspec: Option<String>,
options: Option<DiffOptions>,
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn get_commit_diff(
repo: &Repository,
id: CommitId,
pathspec: Option<String>,
) -> Result<Diff<'_>> {
// scope_time!("get_compare_commits_diff");
let commits = OldNew {
old: repo.find_commit(ids.old.into())?,
new: repo.find_commit(ids.new.into())?,
};
// scope_time!("get_commit_diff");
let trees = OldNew {
old: commits.old.tree()?,
new: commits.new.tree()?,
};
let commit = repo.find_commit(id.into())?;
let commit_tree = commit.tree()?;
let parent = if commit.parent_count() > 0 {
Some(repo.find_commit(commit.parent_id(0)?)?.tree()?)
} else {
None
};
let mut opts = git2::DiffOptions::new();
if let Some(options) = options {
opts.context_lines(options.context);
opts.ignore_whitespace(options.ignore_whitespace);
opts.interhunk_lines(options.interhunk_lines);
}
if let Some(p) = &pathspec {
opts.pathspec(p.clone());
}
let mut opts = DiffOptions::new();
if let Some(p) = &pathspec {
opts.pathspec(p.clone());
}
opts.show_binary(true);
let diff: Diff<'_> = repo.diff_tree_to_tree(
Some(&trees.old),
Some(&trees.new),
Some(&mut opts),
)?;
let mut diff = repo.diff_tree_to_tree(
parent.as_ref(),
Some(&commit_tree),
Some(&mut opts),
)?;
Ok(diff)
}
if is_stash_commit(
repo.path().to_str().map_or_else(
|| Err(Error::Generic("repo path utf8 err".to_owned())),
Ok,
)?,
&id,
)? {
if let Ok(untracked_commit) = commit.parent_id(2) {
let untracked_diff = get_commit_diff(
repo,
CommitId::new(untracked_commit),
pathspec,
)?;
/// get diff of a commit to its first parent
pub(crate) fn get_commit_diff<'a>(
repo: &'a Repository,
id: CommitId,
pathspec: Option<String>,
options: Option<DiffOptions>,
stashes: Option<&HashSet<CommitId>>,
) -> Result<Diff<'a>> {
// scope_time!("get_commit_diff");
diff.merge(&untracked_diff)?;
}
}
let commit = repo.find_commit(id.into())?;
let commit_tree = commit.tree()?;
let parent = if commit.parent_count() > 0 {
repo.find_commit(commit.parent_id(0)?)
.ok()
.and_then(|c| c.tree().ok())
} else {
None
};
let mut opts = git2::DiffOptions::new();
if let Some(options) = options {
opts.context_lines(options.context);
opts.ignore_whitespace(options.ignore_whitespace);
opts.interhunk_lines(options.interhunk_lines);
}
if let Some(p) = &pathspec {
opts.pathspec(p.clone());
}
opts.show_binary(true);
let mut diff = repo.diff_tree_to_tree(
parent.as_ref(),
Some(&commit_tree),
Some(&mut opts),
)?;
if stashes.is_some_and(|stashes| stashes.contains(&id)) {
if let Ok(untracked_commit) = commit.parent_id(2) {
let untracked_diff = get_commit_diff(
repo,
CommitId::new(untracked_commit),
pathspec,
options,
stashes,
)?;
diff.merge(&untracked_diff)?;
}
}
Ok(diff)
Ok(diff)
}
#[cfg(test)]
mod tests {
use super::get_commit_files;
use crate::{
error::Result,
sync::{
commit, stage_add_file, stash_save,
tests::{get_statuses, repo_init},
RepoPath,
},
StatusItemType,
};
use std::{fs::File, io::Write, path::Path};
use super::get_commit_files;
use crate::{
error::Result,
sync::{
commit, stage_add_file, stash_save,
tests::{get_statuses, repo_init},
},
StatusItemType,
};
use std::{fs::File, io::Write, path::Path};
#[test]
fn test_smoke() -> Result<()> {
let file_path = Path::new("file1.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_smoke() -> Result<()> {
let file_path = Path::new("file1.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?
.write_all(b"test file1 content")?;
File::create(&root.join(file_path))?
.write_all(b"test file1 content")?;
stage_add_file(repo_path, file_path)?;
stage_add_file(repo_path, file_path)?;
let id = commit(repo_path, "commit msg")?;
let id = commit(repo_path, "commit msg")?;
let diff = get_commit_files(repo_path, id, None)?;
let diff = get_commit_files(repo_path, id)?;
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].status, StatusItemType::New);
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].status, StatusItemType::New);
Ok(())
}
Ok(())
}
#[test]
fn test_stashed_untracked() -> Result<()> {
let file_path = Path::new("file1.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_stashed_untracked() -> Result<()> {
let file_path = Path::new("file1.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?
.write_all(b"test file1 content")?;
File::create(&root.join(file_path))?
.write_all(b"test file1 content")?;
let id = stash_save(repo_path, None, true, false)?;
let id = stash_save(repo_path, None, true, false)?;
let diff = get_commit_files(repo_path, id, None)?;
let diff = get_commit_files(repo_path, id)?;
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].status, StatusItemType::New);
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].status, StatusItemType::New);
Ok(())
}
Ok(())
}
#[test]
fn test_stashed_untracked_and_modified() -> Result<()> {
let file_path1 = Path::new("file1.txt");
let file_path2 = Path::new("file2.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_stashed_untracked_and_modified() -> Result<()> {
let file_path1 = Path::new("file1.txt");
let file_path2 = Path::new("file2.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path1))?.write_all(b"test")?;
stage_add_file(repo_path, file_path1)?;
commit(repo_path, "c1")?;
File::create(&root.join(file_path1))?.write_all(b"test")?;
stage_add_file(repo_path, file_path1)?;
commit(repo_path, "c1")?;
File::create(root.join(file_path1))?
.write_all(b"modified")?;
File::create(root.join(file_path2))?.write_all(b"new")?;
File::create(&root.join(file_path1))?
.write_all(b"modified")?;
File::create(&root.join(file_path2))?.write_all(b"new")?;
assert_eq!(get_statuses(repo_path), (2, 0));
assert_eq!(get_statuses(repo_path), (2, 0));
let id = stash_save(repo_path, None, true, false)?;
let id = stash_save(repo_path, None, true, false)?;
let diff = get_commit_files(repo_path, id, None)?;
let diff = get_commit_files(repo_path, id)?;
assert_eq!(diff.len(), 2);
assert_eq!(diff[0].status, StatusItemType::Modified);
assert_eq!(diff[1].status, StatusItemType::New);
assert_eq!(diff.len(), 2);
assert_eq!(diff[0].status, StatusItemType::Modified);
assert_eq!(diff[1].status, StatusItemType::New);
Ok(())
}
Ok(())
}
}

View file

@ -1,224 +0,0 @@
use super::{
commit_details::get_author_of_commit,
commit_files::get_commit_diff, CommitId,
};
use crate::error::Result;
use bitflags::bitflags;
use fuzzy_matcher::FuzzyMatcher;
use git2::{Diff, Repository};
use std::sync::Arc;
///
pub type SharedCommitFilterFn = Arc<
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
>;
///
pub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn {
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let diff = get_commit_diff(
repo,
*commit_id,
Some(file_path.clone()),
None,
None,
)?;
let contains_file = diff.deltas().len() > 0;
Ok(contains_file)
},
))
}
bitflags! {
///
#[derive(Debug, Clone, Copy)]
pub struct SearchFields: u32 {
///
const MESSAGE_SUMMARY = 1 << 0;
///
const MESSAGE_BODY = 1 << 1;
///
const FILENAMES = 1 << 2;
///
const AUTHORS = 1 << 3;
//TODO:
// const COMMIT_HASHES = 1 << 3;
// ///
// const DATES = 1 << 4;
// ///
// const DIFFS = 1 << 5;
}
}
impl Default for SearchFields {
fn default() -> Self {
Self::MESSAGE_SUMMARY
}
}
bitflags! {
///
#[derive(Debug, Clone, Copy)]
pub struct SearchOptions: u32 {
///
const CASE_SENSITIVE = 1 << 0;
///
const FUZZY_SEARCH = 1 << 1;
}
}
impl Default for SearchOptions {
fn default() -> Self {
Self::empty()
}
}
///
#[derive(Default, Debug, Clone)]
pub struct LogFilterSearchOptions {
///
pub search_pattern: String,
///
pub fields: SearchFields,
///
pub options: SearchOptions,
}
///
#[derive(Default)]
pub struct LogFilterSearch {
///
pub matcher: fuzzy_matcher::skim::SkimMatcherV2,
///
pub options: LogFilterSearchOptions,
}
impl LogFilterSearch {
///
pub fn new(options: LogFilterSearchOptions) -> Self {
let mut options = options;
if !options.options.contains(SearchOptions::CASE_SENSITIVE) {
options.search_pattern =
options.search_pattern.to_lowercase();
}
Self {
matcher: fuzzy_matcher::skim::SkimMatcherV2::default(),
options,
}
}
fn match_diff(&self, diff: &Diff<'_>) -> bool {
diff.deltas().any(|delta| {
if delta
.new_file()
.path()
.and_then(|file| file.as_os_str().to_str())
.is_some_and(|file| self.match_text(file))
{
return true;
}
delta
.old_file()
.path()
.and_then(|file| file.as_os_str().to_str())
.is_some_and(|file| self.match_text(file))
})
}
///
pub fn match_text(&self, text: &str) -> bool {
if self.options.options.contains(SearchOptions::FUZZY_SEARCH)
{
self.matcher
.fuzzy_match(
text,
self.options.search_pattern.as_str(),
)
.is_some()
} else if self
.options
.options
.contains(SearchOptions::CASE_SENSITIVE)
{
text.contains(self.options.search_pattern.as_str())
} else {
text.to_lowercase()
.contains(self.options.search_pattern.as_str())
}
}
}
///
pub fn filter_commit_by_search(
filter: LogFilterSearch,
) -> SharedCommitFilterFn {
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let mailmap = repo.mailmap()?;
let commit = repo.find_commit((*commit_id).into())?;
let msg_summary_match = filter
.options
.fields
.contains(SearchFields::MESSAGE_SUMMARY)
.then(|| {
commit.summary().map(|msg| filter.match_text(msg))
})
.flatten()
.unwrap_or_default();
let msg_body_match = filter
.options
.fields
.contains(SearchFields::MESSAGE_BODY)
.then(|| {
commit.body().map(|msg| filter.match_text(msg))
})
.flatten()
.unwrap_or_default();
let file_match = filter
.options
.fields
.contains(SearchFields::FILENAMES)
.then(|| {
get_commit_diff(
repo, *commit_id, None, None, None,
)
.ok()
})
.flatten()
.is_some_and(|diff| filter.match_diff(&diff));
let authors_match = if filter
.options
.fields
.contains(SearchFields::AUTHORS)
{
let author = get_author_of_commit(&commit, &mailmap);
[author.email(), author.name()].iter().any(
|opt_haystack| {
opt_haystack.is_some_and(|haystack| {
filter.match_text(haystack)
})
},
)
} else {
false
};
Ok(msg_summary_match
|| msg_body_match
|| file_match
|| authors_match)
},
))
}

View file

@ -1,51 +0,0 @@
use super::{CommitId, RepoPath};
use crate::{
error::Result,
sync::{repository::repo, utils::read_file},
};
use scopetime::scope_time;
const GIT_REVERT_HEAD_FILE: &str = "REVERT_HEAD";
///
pub fn revert_commit(
repo_path: &RepoPath,
commit: CommitId,
) -> Result<()> {
scope_time!("revert");
let repo = repo(repo_path)?;
let commit = repo.find_commit(commit.into())?;
repo.revert(&commit, None)?;
Ok(())
}
///
pub fn revert_head(repo_path: &RepoPath) -> Result<CommitId> {
scope_time!("revert_head");
let path = repo(repo_path)?.path().join(GIT_REVERT_HEAD_FILE);
let file_content = read_file(&path)?;
let id = git2::Oid::from_str(file_content.trim())?;
Ok(id.into())
}
///
pub fn commit_revert(
repo_path: &RepoPath,
msg: &str,
) -> Result<CommitId> {
scope_time!("commit_revert");
let id = crate::sync::commit(repo_path, msg)?;
repo(repo_path)?.cleanup_state()?;
Ok(id)
}

View file

@ -1,349 +1,216 @@
use std::fmt::Display;
use super::RepoPath;
use crate::{
error::Result,
sync::{
commit_details::get_author_of_commit,
repository::{gix_repo, repo},
},
};
use super::utils::repo;
use crate::error::Result;
use git2::{Commit, Error, Oid};
use scopetime::scope_time;
use unicode_truncate::UnicodeTruncateStr;
/// identifies a single commit
#[derive(
Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd,
Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd,
)]
pub struct CommitId(Oid);
impl Default for CommitId {
fn default() -> Self {
Self(Oid::zero())
}
}
impl CommitId {
/// create new `CommitId`
pub const fn new(id: Oid) -> Self {
Self(id)
}
/// create new `CommitId`
pub const fn new(id: Oid) -> Self {
Self(id)
}
///
pub(crate) const fn get_oid(self) -> Oid {
self.0
}
///
pub(crate) const fn get_oid(self) -> Oid {
self.0
}
/// 7 chars short hash
pub fn get_short_string(&self) -> String {
self.to_string().chars().take(7).collect()
}
/// Tries to retrieve the `CommitId` form the revision if exists in the given repository
pub fn from_revision(
repo_path: &RepoPath,
revision: &str,
) -> Result<Self> {
scope_time!("CommitId::from_revision");
let repo = repo(repo_path)?;
let commit_obj = repo.revparse_single(revision)?;
Ok(commit_obj.id().into())
}
/// Tries to convert a &str representation of a commit id into
/// a `CommitId`
pub fn from_str_unchecked(commit_id_str: &str) -> Result<Self> {
match Oid::from_str(commit_id_str) {
Err(e) => Err(crate::Error::Generic(format!(
"Could not convert {}",
e.message()
))),
Ok(v) => Ok(Self::new(v)),
}
}
/// 7 chars short hash
pub fn get_short_string(&self) -> String {
self.to_string().chars().take(7).collect()
}
}
impl Display for CommitId {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "{}", self.0)
}
//TODO: remove once clippy fixed: https://github.com/rust-lang/rust-clippy/issues/6983
#[allow(clippy::wrong_self_convention)]
impl ToString for CommitId {
fn to_string(&self) -> String {
self.0.to_string()
}
}
impl From<CommitId> for Oid {
fn from(id: CommitId) -> Self {
id.0
}
fn from(id: CommitId) -> Self {
id.0
}
}
impl From<Oid> for CommitId {
fn from(id: Oid) -> Self {
Self::new(id)
}
}
impl From<gix::ObjectId> for CommitId {
fn from(object_id: gix::ObjectId) -> Self {
#[allow(clippy::expect_used)]
let oid = Oid::from_bytes(object_id.as_bytes()).expect("`Oid::from_bytes(object_id.as_bytes())` is expected to never fail");
Self::new(oid)
}
}
impl From<gix::Commit<'_>> for CommitId {
fn from(commit: gix::Commit<'_>) -> Self {
#[allow(clippy::expect_used)]
let oid = Oid::from_bytes(commit.id().as_bytes()).expect("`Oid::from_bytes(commit.id().as_bytes())` is expected to never fail");
Self::new(oid)
}
}
impl From<CommitId> for gix::ObjectId {
fn from(id: CommitId) -> Self {
Self::from_bytes_or_panic(id.0.as_bytes())
}
fn from(id: Oid) -> Self {
Self::new(id)
}
}
///
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct CommitInfo {
///
pub message: String,
///
pub time: i64,
///
pub author: String,
///
pub id: CommitId,
///
pub message: String,
///
pub time: i64,
///
pub author: String,
///
pub id: CommitId,
}
///
pub fn get_commits_info(
repo_path: &RepoPath,
ids: &[CommitId],
message_length_limit: usize,
repo_path: &str,
ids: &[CommitId],
message_length_limit: usize,
) -> Result<Vec<CommitInfo>> {
scope_time!("get_commits_info");
scope_time!("get_commits_info");
let repo = repo(repo_path)?;
let mailmap = repo.mailmap()?;
let repo = repo(repo_path)?;
let commits = ids
.iter()
.map(|id| repo.find_commit((*id).into()))
.collect::<std::result::Result<Vec<Commit>, Error>>()?
.into_iter();
let commits = ids
.iter()
.map(|id| repo.find_commit((*id).into()))
.collect::<std::result::Result<Vec<Commit>, Error>>()?
.into_iter();
let res = commits
.map(|c: Commit| {
let message = get_message(&c, Some(message_length_limit));
let author = get_author_of_commit(&c, &mailmap)
.name()
.map_or_else(
|| String::from("<unknown>"),
String::from,
);
CommitInfo {
message,
author,
time: c.time().seconds(),
id: CommitId(c.id()),
}
})
.collect::<Vec<_>>();
let res = commits
.map(|c: Commit| {
let message = get_message(&c, Some(message_length_limit));
let author = c.author().name().map_or_else(
|| String::from("<unknown>"),
String::from,
);
CommitInfo {
message,
author,
time: c.time().seconds(),
id: CommitId(c.id()),
}
})
.collect::<Vec<_>>();
Ok(res)
Ok(res)
}
///
pub fn get_commit_info(
repo_path: &RepoPath,
commit_id: &CommitId,
repo_path: &str,
commit_id: &CommitId,
) -> Result<CommitInfo> {
scope_time!("get_commit_info");
scope_time!("get_commit_info");
let repo: gix::Repository = gix_repo(repo_path)?;
let mailmap = repo.open_mailmap();
let repo = repo(repo_path)?;
let commit = repo.find_commit(*commit_id)?;
let commit_ref = commit.decode()?;
let commit = repo.find_commit((*commit_id).into())?;
let author = commit.author();
let message = gix_get_message(&commit_ref, None);
let author = commit_ref.author()?;
let author = mailmap.try_resolve(author).map_or_else(
|| author.name.into(),
|signature| signature.name,
);
Ok(CommitInfo {
message,
author: author.to_string(),
time: commit_ref.time()?.seconds,
id: commit.id().detach().into(),
})
Ok(CommitInfo {
message: commit.message().unwrap_or("").into(),
author: author.name().unwrap_or("<unknown>").into(),
time: commit.time().seconds(),
id: CommitId(commit.id()),
})
}
/// if `message_limit` is set the message will be
/// limited to the first line and truncated to fit
pub fn get_message(
c: &git2::Commit,
message_limit: Option<usize>,
c: &Commit,
message_limit: Option<usize>,
) -> String {
let msg = String::from_utf8_lossy(c.message_bytes());
let msg = msg.trim();
let msg = String::from_utf8_lossy(c.message_bytes());
let msg = msg.trim();
message_limit.map_or_else(
|| msg.to_string(),
|limit| {
let msg = msg.lines().next().unwrap_or_default();
msg.unicode_truncate(limit).0.to_string()
},
)
}
/// if `message_limit` is set the message will be
/// limited to the first line and truncated to fit
pub fn gix_get_message(
commit_ref: &gix::objs::CommitRef,
message_limit: Option<usize>,
) -> String {
let message = commit_ref.message.to_string();
let message = message.trim();
message_limit.map_or_else(
|| message.to_string(),
|limit| {
let message = message.lines().next().unwrap_or_default();
message.unicode_truncate(limit).0.to_string()
},
)
message_limit.map_or_else(
|| msg.to_string(),
|limit| {
let msg = msg.lines().next().unwrap_or_default();
msg.unicode_truncate(limit).0.to_string()
},
)
}
#[cfg(test)]
mod tests {
use super::get_commits_info;
use crate::{
error::Result,
sync::{
commit, stage_add_file, tests::repo_init_empty,
utils::get_head_repo, CommitId, RepoPath,
},
};
use std::{fs::File, io::Write, path::Path};
use super::get_commits_info;
use crate::error::Result;
use crate::sync::{
commit, stage_add_file, tests::repo_init_empty,
utils::get_head_repo,
};
use std::{fs::File, io::Write, path::Path};
#[test]
fn test_log() -> Result<()> {
let file_path = Path::new("foo");
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();
#[test]
fn test_log() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let c1 = commit(repo_path, "commit1").unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let c2 = commit(repo_path, "commit2").unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let c1 = commit(repo_path, "commit1").unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let c2 = commit(repo_path, "commit2").unwrap();
let res = get_commits_info(repo_path, &[c2, c1], 50).unwrap();
let res =
get_commits_info(repo_path, &vec![c2, c1], 50).unwrap();
assert_eq!(res.len(), 2);
assert_eq!(res[0].message.as_str(), "commit2");
assert_eq!(res[0].author.as_str(), "name");
assert_eq!(res[1].message.as_str(), "commit1");
assert_eq!(res.len(), 2);
assert_eq!(res[0].message.as_str(), "commit2");
assert_eq!(res[0].author.as_str(), "name");
assert_eq!(res[1].message.as_str(), "commit1");
File::create(root.join(".mailmap"))?
.write_all(b"new name <newemail> <email>")?;
let res = get_commits_info(repo_path, &[c2], 50).unwrap();
Ok(())
}
assert_eq!(res[0].author.as_str(), "new name");
#[test]
fn test_log_first_msg_line() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
Ok(())
}
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let c1 = commit(repo_path, "subject\nbody").unwrap();
#[test]
fn test_log_first_msg_line() -> Result<()> {
let file_path = Path::new("foo");
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();
let res = get_commits_info(repo_path, &vec![c1], 50).unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let c1 = commit(repo_path, "subject\nbody").unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].message.as_str(), "subject");
let res = get_commits_info(repo_path, &[c1], 50).unwrap();
Ok(())
}
assert_eq!(res.len(), 1);
assert_eq!(res[0].message.as_str(), "subject");
#[test]
fn test_invalid_utf8() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
Ok(())
}
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
#[test]
fn test_invalid_utf8() -> Result<()> {
let file_path = Path::new("foo");
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();
let msg = invalidstring::invalid_utf8("test msg");
commit(repo_path, msg.as_str()).unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let res = get_commits_info(
repo_path,
&vec![get_head_repo(&repo).unwrap().into()],
50,
)
.unwrap();
let msg = invalidstring::invalid_utf8("test msg");
commit(repo_path, msg.as_str()).unwrap();
assert_eq!(res.len(), 1);
dbg!(&res[0].message);
assert_eq!(res[0].message.starts_with("test msg"), true);
let res = get_commits_info(
repo_path,
&[get_head_repo(&repo).unwrap()],
50,
)
.unwrap();
assert_eq!(res.len(), 1);
dbg!(&res[0].message);
assert!(res[0].message.starts_with("test msg"));
Ok(())
}
#[test]
fn test_get_commit_from_revision() -> Result<()> {
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();
let foo_file = Path::new("foo");
File::create(root.join(foo_file))?.write_all(b"a")?;
stage_add_file(repo_path, foo_file).unwrap();
let c1 = commit(repo_path, "subject: foo\nbody").unwrap();
let c1_rev = c1.get_short_string();
assert_eq!(
CommitId::from_revision(repo_path, c1_rev.as_str())
.unwrap(),
c1
);
const FOREIGN_HASH: &str =
"d6d7d55cb6e4ba7301d6a11a657aab4211e5777e";
assert!(
CommitId::from_revision(repo_path, FOREIGN_HASH).is_err()
);
Ok(())
}
Ok(())
}
}

View file

@ -1,170 +1,114 @@
use super::utils::repo;
use crate::error::Result;
use git2::Repository;
use scopetime::scope_time;
use serde::{Deserialize, Serialize};
use super::{repository::repo, RepoPath};
// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles
/// represents the `status.showUntrackedFiles` git config state
#[derive(
Hash, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum ShowUntrackedFilesConfig {
///
#[default]
No,
///
Normal,
///
All,
///
No,
///
Normal,
///
All,
}
impl ShowUntrackedFilesConfig {
///
pub const fn include_none(self) -> bool {
matches!(self, Self::No)
}
///
pub const fn include_none(&self) -> bool {
matches!(self, Self::No)
}
///
pub const fn include_untracked(self) -> bool {
matches!(self, Self::Normal | Self::All)
}
///
pub const fn include_untracked(&self) -> bool {
matches!(self, Self::Normal | Self::All)
}
///
pub const fn recurse_untracked_dirs(self) -> bool {
matches!(self, Self::All)
}
///
pub const fn recurse_untracked_dirs(&self) -> bool {
matches!(self, Self::All)
}
}
pub fn untracked_files_config_repo(
repo: &Repository,
repo: &Repository,
) -> Result<ShowUntrackedFilesConfig> {
let show_untracked_files =
get_config_string_repo(repo, "status.showUntrackedFiles")?;
let show_untracked_files =
get_config_string_repo(repo, "status.showUntrackedFiles")?;
if let Some(show_untracked_files) = show_untracked_files {
if &show_untracked_files == "no" {
return Ok(ShowUntrackedFilesConfig::No);
} else if &show_untracked_files == "normal" {
return Ok(ShowUntrackedFilesConfig::Normal);
}
}
if let Some(show_untracked_files) = show_untracked_files {
if &show_untracked_files == "no" {
return Ok(ShowUntrackedFilesConfig::No);
} else if &show_untracked_files == "normal" {
return Ok(ShowUntrackedFilesConfig::Normal);
}
}
// This does not reflect how git works according to its docs that say: "If this variable is not
// specified, it defaults to `normal`."
//
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles
//
// Note that this might become less relevant over time as more code gets migrated to `gitoxide`
// because `gitoxide` respects `status.showUntrackedFiles` by default.
Ok(ShowUntrackedFilesConfig::All)
}
// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushdefault
/// represents `push.default` git config
#[derive(PartialEq, Default, Eq)]
pub enum PushDefaultStrategyConfig {
Nothing,
Current,
Upstream,
#[default]
Simple,
Matching,
}
impl<'a> TryFrom<&'a str> for PushDefaultStrategyConfig {
type Error = crate::Error;
fn try_from(
value: &'a str,
) -> std::result::Result<Self, Self::Error> {
match value {
"nothing" => Ok(Self::Nothing),
"current" => Ok(Self::Current),
"upstream" | "tracking" => Ok(Self::Upstream),
"simple" => Ok(Self::Simple),
"matching" => Ok(Self::Matching),
_ => Err(crate::Error::GitConfig(format!(
"malformed value for push.default: {value}, must be one of nothing, matching, simple, upstream or current"
))),
}
}
}
pub fn push_default_strategy_config_repo(
repo: &Repository,
) -> Result<PushDefaultStrategyConfig> {
(get_config_string_repo(repo, "push.default")?).map_or_else(
|| Ok(PushDefaultStrategyConfig::default()),
|entry_str| {
PushDefaultStrategyConfig::try_from(entry_str.as_str())
},
)
Ok(ShowUntrackedFilesConfig::All)
}
///
pub fn untracked_files_config(
repo_path: &RepoPath,
repo_path: &str,
) -> Result<ShowUntrackedFilesConfig> {
let repo = repo(repo_path)?;
untracked_files_config_repo(&repo)
let repo = repo(repo_path)?;
untracked_files_config_repo(&repo)
}
/// get string from config
pub fn get_config_string(
repo_path: &RepoPath,
key: &str,
repo_path: &str,
key: &str,
) -> Result<Option<String>> {
let repo = repo(repo_path)?;
get_config_string_repo(&repo, key)
let repo = repo(repo_path)?;
get_config_string_repo(&repo, key)
}
pub fn get_config_string_repo(
repo: &Repository,
key: &str,
repo: &Repository,
key: &str,
) -> Result<Option<String>> {
scope_time!("get_config_string_repo");
scope_time!("get_config_string_repo");
let cfg = repo.config()?;
let cfg = repo.config()?;
// this code doesn't match what the doc says regarding what
// gets returned when but it actually works
let entry_res = cfg.get_entry(key);
// this code doesnt match what the doc says regarding what
// gets returned when but it actually works
let entry_res = cfg.get_entry(key);
let Ok(entry) = entry_res else {
return Ok(None);
};
let entry = match entry_res {
Ok(ent) => ent,
Err(_) => return Ok(None),
};
if entry.has_value() {
Ok(entry.value().map(std::string::ToString::to_string))
} else {
Ok(None)
}
if entry.has_value() {
Ok(entry.value().map(std::string::ToString::to_string))
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::tests::repo_init;
use super::*;
use crate::sync::tests::repo_init;
#[test]
fn test_get_config() {
let bad_dir_cfg = get_config_string(
&"oodly_noodly".into(),
"this.doesnt.exist",
);
assert!(bad_dir_cfg.is_err());
#[test]
fn test_get_config() {
let bad_dir_cfg =
get_config_string("oodly_noodly", "this.doesnt.exist");
assert!(bad_dir_cfg.is_err());
let (_td, repo) = repo_init().unwrap();
let path = repo.path();
let rpath = path.as_os_str().to_str().unwrap();
let bad_cfg =
get_config_string(&rpath.into(), "this.doesnt.exist");
assert!(bad_cfg.is_ok());
assert!(bad_cfg.unwrap().is_none());
// repo init sets user.name
let good_cfg = get_config_string(&rpath.into(), "user.name");
assert!(good_cfg.is_ok());
assert!(good_cfg.unwrap().is_some());
}
let (_td, repo) = repo_init().unwrap();
let path = repo.path();
let rpath = path.as_os_str().to_str().unwrap();
let bad_cfg = get_config_string(rpath, "this.doesnt.exist");
assert!(bad_cfg.is_ok());
assert!(bad_cfg.unwrap().is_none());
// repo init sets user.name
let good_cfg = get_config_string(rpath, "user.name");
assert!(good_cfg.is_ok());
assert!(good_cfg.unwrap().is_some());
}
}

View file

@ -1,367 +1,253 @@
//! credentials git helper
use super::{
remotes::{
get_default_remote_for_fetch_in_repo,
get_default_remote_for_push_in_repo,
get_default_remote_in_repo,
},
repository::repo,
RepoPath,
use super::remotes::get_default_remote_in_repo;
use crate::{
error::{Error, Result},
CWD,
};
use crate::error::{Error, Result};
use git2::CredentialHelper;
use git2::{Config, CredentialHelper};
/// basic Authentication Credentials
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BasicAuthCredential {
///
pub username: Option<String>,
///
pub password: Option<String>,
///
pub username: Option<String>,
///
pub password: Option<String>,
}
impl BasicAuthCredential {
///
pub const fn is_complete(&self) -> bool {
self.username.is_some() && self.password.is_some()
}
///
pub const fn new(
username: Option<String>,
password: Option<String>,
) -> Self {
Self { username, password }
}
///
pub const fn is_complete(&self) -> bool {
self.username.is_some() && self.password.is_some()
}
///
pub const fn new(
username: Option<String>,
password: Option<String>,
) -> Self {
Self { username, password }
}
}
/// know if username and password are needed for this url
pub fn need_username_password(repo_path: &RepoPath) -> Result<bool> {
let repo = repo(repo_path)?;
let remote =
repo.find_remote(&get_default_remote_in_repo(&repo)?)?;
let url = remote
.pushurl()
.or_else(|| remote.url())
.ok_or(Error::UnknownRemote)?
.to_owned();
let is_http = url.starts_with("http");
Ok(is_http)
}
/// know if username and password are needed for this url
/// TODO: Very similar to `need_username_password_for_fetch`. Can be refactored. See also
/// `need_username_password`.
pub fn need_username_password_for_fetch(
repo_path: &RepoPath,
) -> Result<bool> {
let repo = repo(repo_path)?;
let remote = repo
.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?;
let url = remote
.url()
.or_else(|| remote.url())
.ok_or(Error::UnknownRemote)?
.to_owned();
let is_http = url.starts_with("http");
Ok(is_http)
}
/// know if username and password are needed for this url
/// TODO: Very similar to `need_username_password_for_fetch`. Can be refactored. See also
/// `need_username_password`.
pub fn need_username_password_for_push(
repo_path: &RepoPath,
) -> Result<bool> {
let repo = repo(repo_path)?;
let remote = repo
.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?;
let url = remote
.pushurl()
.or_else(|| remote.url())
.ok_or(Error::UnknownRemote)?
.to_owned();
let is_http = url.starts_with("http");
Ok(is_http)
pub fn need_username_password() -> Result<bool> {
let repo = crate::sync::utils::repo(CWD)?;
let url = repo
.find_remote(&get_default_remote_in_repo(&repo)?)?
.url()
.ok_or(Error::UnknownRemote)?
.to_owned();
let is_http = url.starts_with("http");
Ok(is_http)
}
/// extract username and password
pub fn extract_username_password(
repo_path: &RepoPath,
) -> Result<BasicAuthCredential> {
let repo = repo(repo_path)?;
let url = repo
.find_remote(&get_default_remote_in_repo(&repo)?)?
.url()
.ok_or(Error::UnknownRemote)?
.to_owned();
let mut helper = CredentialHelper::new(&url);
pub fn extract_username_password() -> Result<BasicAuthCredential> {
let repo = crate::sync::utils::repo(CWD)?;
let url = repo
.find_remote(&get_default_remote_in_repo(&repo)?)?
.url()
.ok_or(Error::UnknownRemote)?
.to_owned();
let mut helper = CredentialHelper::new(&url);
//TODO: look at Cred::credential_helper,
//if the username is in the url we need to set it here,
//I dont think `config` will pick it up
if let Ok(config) = repo.config() {
helper.config(&config);
}
Ok(match helper.execute() {
Some((username, password)) => {
BasicAuthCredential::new(Some(username), Some(password))
}
None => extract_cred_from_url(&url),
})
}
/// extract username and password
/// TODO: Very similar to `extract_username_password_for_fetch`. Can be refactored.
pub fn extract_username_password_for_fetch(
repo_path: &RepoPath,
) -> Result<BasicAuthCredential> {
let repo = repo(repo_path)?;
let url = repo
.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?
.url()
.ok_or(Error::UnknownRemote)?
.to_owned();
let mut helper = CredentialHelper::new(&url);
//TODO: look at Cred::credential_helper,
//if the username is in the url we need to set it here,
//I dont think `config` will pick it up
if let Ok(config) = repo.config() {
helper.config(&config);
}
Ok(match helper.execute() {
Some((username, password)) => {
BasicAuthCredential::new(Some(username), Some(password))
}
None => extract_cred_from_url(&url),
})
}
/// extract username and password
/// TODO: Very similar to `extract_username_password_for_fetch`. Can be refactored.
pub fn extract_username_password_for_push(
repo_path: &RepoPath,
) -> Result<BasicAuthCredential> {
let repo = repo(repo_path)?;
let url = repo
.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?
.url()
.ok_or(Error::UnknownRemote)?
.to_owned();
let mut helper = CredentialHelper::new(&url);
//TODO: look at Cred::credential_helper,
//if the username is in the url we need to set it here,
//I dont think `config` will pick it up
if let Ok(config) = repo.config() {
helper.config(&config);
}
Ok(match helper.execute() {
Some((username, password)) => {
BasicAuthCredential::new(Some(username), Some(password))
}
None => extract_cred_from_url(&url),
})
if let Ok(config) = Config::open_default() {
helper.config(&config);
}
Ok(match helper.execute() {
Some((username, password)) => {
BasicAuthCredential::new(Some(username), Some(password))
}
None => extract_cred_from_url(&url),
})
}
/// extract credentials from url
pub fn extract_cred_from_url(url: &str) -> BasicAuthCredential {
url::Url::parse(url).map_or_else(
|_| BasicAuthCredential::new(None, None),
|url| {
BasicAuthCredential::new(
if url.username() == "" {
None
} else {
Some(url.username().to_owned())
},
url.password().map(std::borrow::ToOwned::to_owned),
)
},
)
if let Ok(url) = url::Url::parse(url) {
BasicAuthCredential::new(
if url.username() == "" {
None
} else {
Some(url.username().to_owned())
},
url.password().map(std::borrow::ToOwned::to_owned),
)
} else {
BasicAuthCredential::new(None, None)
}
}
#[cfg(test)]
mod tests {
use crate::sync::{
cred::{
extract_cred_from_url, extract_username_password,
need_username_password, BasicAuthCredential,
},
remotes::DEFAULT_REMOTE_NAME,
tests::repo_init,
RepoPath,
};
use serial_test::serial;
use crate::sync::{
cred::{
extract_cred_from_url, extract_username_password,
need_username_password, BasicAuthCredential,
},
remotes::DEFAULT_REMOTE_NAME,
tests::repo_init,
};
use serial_test::serial;
use std::env;
#[test]
fn test_credential_complete() {
assert!(BasicAuthCredential::new(
Some("username".to_owned()),
Some("password".to_owned())
)
.is_complete());
}
#[test]
fn test_credential_complete() {
assert_eq!(
BasicAuthCredential::new(
Some("username".to_owned()),
Some("password".to_owned())
)
.is_complete(),
true
);
}
#[test]
fn test_credential_not_complete() {
assert!(!BasicAuthCredential::new(
None,
Some("password".to_owned())
)
.is_complete());
assert!(!BasicAuthCredential::new(
Some("username".to_owned()),
None
)
.is_complete());
assert!(!BasicAuthCredential::new(None, None).is_complete());
}
#[test]
fn test_credential_not_complete() {
assert_eq!(
BasicAuthCredential::new(
None,
Some("password".to_owned())
)
.is_complete(),
false
);
assert_eq!(
BasicAuthCredential::new(
Some("username".to_owned()),
None
)
.is_complete(),
false
);
assert_eq!(
BasicAuthCredential::new(None, None).is_complete(),
false
);
}
#[test]
fn test_extract_username_from_url() {
assert_eq!(
extract_cred_from_url("https://user@github.com"),
BasicAuthCredential::new(Some("user".to_owned()), None)
);
}
#[test]
fn test_extract_username_from_url() {
assert_eq!(
extract_cred_from_url("https://user@github.com"),
BasicAuthCredential::new(Some("user".to_owned()), None)
);
}
#[test]
fn test_extract_username_password_from_url() {
assert_eq!(
extract_cred_from_url("https://user:pwd@github.com"),
BasicAuthCredential::new(
Some("user".to_owned()),
Some("pwd".to_owned())
)
);
}
#[test]
fn test_extract_username_password_from_url() {
assert_eq!(
extract_cred_from_url("https://user:pwd@github.com"),
BasicAuthCredential::new(
Some("user".to_owned()),
Some("pwd".to_owned())
)
);
}
#[test]
fn test_extract_nothing_from_url() {
assert_eq!(
extract_cred_from_url("https://github.com"),
BasicAuthCredential::new(None, None)
);
}
#[test]
fn test_extract_nothing_from_url() {
assert_eq!(
extract_cred_from_url("https://github.com"),
BasicAuthCredential::new(None, None)
);
}
#[test]
#[serial]
fn test_need_username_password_if_https() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
#[serial]
fn test_need_username_password_if_https() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
.unwrap();
env::set_current_dir(repo_path).unwrap();
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
.unwrap();
assert!(need_username_password(repo_path).unwrap());
}
assert_eq!(need_username_password().unwrap(), true);
}
#[test]
#[serial]
fn test_dont_need_username_password_if_ssh() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
#[serial]
fn test_dont_need_username_password_if_ssh() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
repo.remote(DEFAULT_REMOTE_NAME, "git@github.com:user/repo")
.unwrap();
env::set_current_dir(repo_path).unwrap();
repo.remote(DEFAULT_REMOTE_NAME, "git@github.com:user/repo")
.unwrap();
assert!(!need_username_password(repo_path).unwrap());
}
assert_eq!(need_username_password().unwrap(), false);
}
#[test]
#[serial]
fn test_dont_need_username_password_if_pushurl_ssh() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
#[serial]
#[should_panic]
fn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password(
) {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
.unwrap();
repo.remote_set_pushurl(
DEFAULT_REMOTE_NAME,
Some("git@github.com:user/repo"),
)
.unwrap();
env::set_current_dir(repo_path).unwrap();
assert!(!need_username_password(repo_path).unwrap());
}
need_username_password().unwrap();
}
#[test]
#[serial]
#[should_panic]
fn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password(
) {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
#[serial]
fn test_extract_username_password_from_repo() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
need_username_password(repo_path).unwrap();
}
env::set_current_dir(repo_path).unwrap();
repo.remote(
DEFAULT_REMOTE_NAME,
"http://user:pass@github.com",
)
.unwrap();
#[test]
#[serial]
fn test_extract_username_password_from_repo() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
assert_eq!(
extract_username_password().unwrap(),
BasicAuthCredential::new(
Some("user".to_owned()),
Some("pass".to_owned())
)
);
}
repo.remote(
DEFAULT_REMOTE_NAME,
"http://user:pass@github.com",
)
.unwrap();
#[test]
#[serial]
fn test_extract_username_from_repo() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
assert_eq!(
extract_username_password(repo_path).unwrap(),
BasicAuthCredential::new(
Some("user".to_owned()),
Some("pass".to_owned())
)
);
}
env::set_current_dir(repo_path).unwrap();
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
.unwrap();
#[test]
#[serial]
fn test_extract_username_from_repo() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
assert_eq!(
extract_username_password().unwrap(),
BasicAuthCredential::new(Some("user".to_owned()), None)
);
}
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
.unwrap();
#[test]
#[serial]
#[should_panic]
fn test_error_if_no_remote_when_trying_to_extract_username_password(
) {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
assert_eq!(
extract_username_password(repo_path).unwrap(),
BasicAuthCredential::new(Some("user".to_owned()), None)
);
}
env::set_current_dir(repo_path).unwrap();
#[test]
#[serial]
#[should_panic]
fn test_error_if_no_remote_when_trying_to_extract_username_password(
) {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
extract_username_password(repo_path).unwrap();
}
extract_username_password().unwrap();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,501 +1,389 @@
use super::{repository::repo, RepoPath};
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 super::utils::{repo, work_dir};
use crate::error::{Error, Result};
use scopetime::scope_time;
use std::collections::HashMap;
use std::{
fs::File,
io::{Read, Write},
path::Path,
process::Command,
};
///
#[derive(Debug, PartialEq, Eq)]
pub enum HookResult {
/// Everything went fine
Ok,
/// Hook returned error
NotOk(String),
}
const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit";
const HOOK_PRE_COMMIT: &str = ".git/hooks/pre-commit";
const HOOK_COMMIT_MSG: &str = ".git/hooks/commit-msg";
const HOOK_COMMIT_MSG_TEMP_FILE: &str = ".git/COMMIT_EDITMSG";
impl From<git2_hooks::HookResult> for HookResult {
fn from(v: git2_hooks::HookResult) -> Self {
match v {
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`
/// this hook is documented here <https://git-scm.com/docs/githooks#_commit_msg>
/// we use the same convention as other git clients to create a temp file containing
/// the commit message at `.git/COMMIT_EDITMSG` and pass it's relative path as the only
/// parameter to the hook script.
pub fn hooks_commit_msg(
repo_path: &RepoPath,
msg: &mut String,
repo_path: &str,
msg: &mut String,
) -> Result<HookResult> {
scope_time!("hooks_commit_msg");
scope_time!("hooks_commit_msg");
let repo = repo(repo_path)?;
let work_dir = work_dir_as_string(repo_path)?;
Ok(git2_hooks::hooks_commit_msg(&repo, None, msg)?.into())
if hook_runable(work_dir.as_str(), HOOK_COMMIT_MSG) {
let temp_file = Path::new(work_dir.as_str())
.join(HOOK_COMMIT_MSG_TEMP_FILE);
File::create(&temp_file)?.write_all(msg.as_bytes())?;
let res = run_hook(
work_dir.as_str(),
HOOK_COMMIT_MSG,
&[HOOK_COMMIT_MSG_TEMP_FILE],
)?;
// load possibly altered msg
msg.clear();
File::open(temp_file)?.read_to_string(msg)?;
Ok(res)
} else {
Ok(HookResult::Ok)
}
}
/// see `git2_hooks::hooks_pre_commit`
pub fn hooks_pre_commit(repo_path: &RepoPath) -> Result<HookResult> {
scope_time!("hooks_pre_commit");
/// this hook is documented here <https://git-scm.com/docs/githooks#_pre_commit>
///
pub fn hooks_pre_commit(repo_path: &str) -> Result<HookResult> {
scope_time!("hooks_pre_commit");
let repo = repo(repo_path)?;
let work_dir = work_dir_as_string(repo_path)?;
Ok(git2_hooks::hooks_pre_commit(&repo, None)?.into())
if hook_runable(work_dir.as_str(), HOOK_PRE_COMMIT) {
Ok(run_hook(work_dir.as_str(), HOOK_PRE_COMMIT, &[])?)
} else {
Ok(HookResult::Ok)
}
}
///
pub fn hooks_post_commit(repo_path: &str) -> Result<HookResult> {
scope_time!("hooks_post_commit");
let work_dir = work_dir_as_string(repo_path)?;
let work_dir_str = work_dir.as_str();
if hook_runable(work_dir_str, HOOK_POST_COMMIT) {
Ok(run_hook(work_dir_str, HOOK_POST_COMMIT, &[])?)
} else {
Ok(HookResult::Ok)
}
}
/// see `git2_hooks::hooks_post_commit`
pub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {
scope_time!("hooks_post_commit");
let repo = repo(repo_path)?;
Ok(git2_hooks::hooks_post_commit(&repo, None)?.into())
fn work_dir_as_string(repo_path: &str) -> Result<String> {
let repo = repo(repo_path)?;
work_dir(&repo)?
.to_str()
.map(std::string::ToString::to_string)
.ok_or_else(|| {
Error::Generic(
"workdir contains invalid utf8".to_string(),
)
})
}
/// see `git2_hooks::hooks_prepare_commit_msg`
pub fn hooks_prepare_commit_msg(
repo_path: &RepoPath,
source: PrepareCommitMsgSource,
msg: &mut String,
fn hook_runable(path: &str, hook: &str) -> bool {
let path = Path::new(path);
let path = path.join(hook);
path.exists() && is_executable(&path)
}
///
#[derive(Debug, PartialEq)]
pub enum HookResult {
/// Everything went fine
Ok,
/// Hook returned error
NotOk(String),
}
/// this function calls hook scripts based on conventions documented here
/// see <https://git-scm.com/docs/githooks>
fn run_hook(
path: &str,
hook_script: &str,
args: &[&str],
) -> Result<HookResult> {
scope_time!("hooks_prepare_commit_msg");
let arg_str = format!("{} {}", hook_script, args.join(" "));
let bash_args = vec!["-c".to_string(), arg_str];
let repo = repo(repo_path)?;
let output = Command::new("bash")
.args(bash_args)
.current_dir(path)
// This call forces Command to handle the Path environment correctly on windows,
// the specific env set here does not matter
// see https://github.com/rust-lang/rust/issues/37519
.env(
"DUMMY_ENV_TO_FIX_WINDOWS_CMD_RUNS",
"FixPathHandlingOnWindows",
)
.output()?;
Ok(git2_hooks::hooks_prepare_commit_msg(
&repo, None, source, msg,
)?
.into())
if output.status.success() {
Ok(HookResult::Ok)
} else {
let err = String::from_utf8_lossy(&output.stderr);
let out = String::from_utf8_lossy(&output.stdout);
let formatted = format!("{}{}", out, err);
Ok(HookResult::NotOk(formatted))
}
}
/// see `git2_hooks::hooks_pre_push`
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");
#[cfg(not(windows))]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
let metadata = match path.metadata() {
Ok(metadata) => metadata,
Err(_) => return false,
};
let repo = repo(repo_path)?;
if !git2_hooks::hook_available(
&repo,
None,
git2_hooks::HOOK_PRE_PUSH,
)? {
return Ok(HookResult::Ok);
}
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())
let permissions = metadata.permissions();
permissions.mode() & 0o111 != 0
}
/// 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(windows)]
/// windows does not consider bash scripts to be executable so we consider everything
/// to be executable (which is not far from the truth for windows platform.)
const fn is_executable(_: &Path) -> bool {
true
}
#[cfg(test)]
mod tests {
use std::{ffi::OsString, io::Write as _, path::Path};
use super::*;
use crate::sync::tests::repo_init;
use std::fs::{self, File};
use git2::Repository;
use tempfile::TempDir;
#[test]
fn test_smoke() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
use super::*;
use crate::sync::tests::repo_init_with_prefix;
let mut msg = String::from("test");
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
fn repo_init() -> Result<(TempDir, Repository)> {
let mut os_string: OsString = OsString::new();
assert_eq!(res, HookResult::Ok);
os_string.push("gitui $# ' ");
let res = hooks_post_commit(repo_path).unwrap();
#[cfg(target_os = "linux")]
{
use std::os::unix::ffi::OsStrExt;
assert_eq!(res, HookResult::Ok);
}
const INVALID_UTF8: &[u8] = b"\xED\xA0\x80";
fn create_hook(path: &Path, hook_path: &str, hook_script: &[u8]) {
File::create(&path.join(hook_path))
.unwrap()
.write_all(hook_script)
.unwrap();
os_string.push(std::ffi::OsStr::from_bytes(INVALID_UTF8));
#[cfg(not(windows))]
{
Command::new("chmod")
.args(&["+x", hook_path])
.current_dir(path)
.output()
.unwrap();
}
}
assert!(os_string.to_str().is_none());
}
#[test]
fn test_hooks_commit_msg_ok() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
os_string.push(" ");
let hook = b"#!/bin/sh
exit 0
";
repo_init_with_prefix(os_string)
}
create_hook(root, HOOK_COMMIT_MSG, hook);
fn create_hook_in_path(path: &Path, hook_script: &[u8]) {
std::fs::File::create(path)
.unwrap()
.write_all(hook_script)
.unwrap();
let mut msg = String::from("test");
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
#[cfg(unix)]
{
std::process::Command::new("chmod")
.arg("+x")
.arg(path)
// .current_dir(path)
.output()
.unwrap();
}
}
assert_eq!(res, HookResult::Ok);
#[test]
fn test_post_commit_hook_reject_in_subfolder() {
let (_td, repo) = repo_init().unwrap();
let root = repo.workdir().unwrap();
assert_eq!(msg, String::from("test"));
}
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
#[test]
fn test_pre_commit_sh() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
git2_hooks::create_hook(
&repo,
git2_hooks::HOOK_POST_COMMIT,
hook,
);
let hook = b"#!/bin/sh
exit 0
";
let subfolder = root.join("foo/");
std::fs::create_dir_all(&subfolder).unwrap();
create_hook(root, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(repo_path).unwrap();
assert_eq!(res, HookResult::Ok);
}
let res = hooks_post_commit(&subfolder.into()).unwrap();
#[test]
fn test_pre_commit_fail_sh() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
}
// make sure we run the hooks with the correct pwd.
// for non-bare repos this is the dir of the worktree
// unfortunately does not work on windows
#[test]
#[cfg(unix)]
fn test_pre_commit_workdir() {
let (_td, repo) = repo_init().unwrap();
let root = repo.workdir().unwrap();
let repo_path: &RepoPath = &root.to_path_buf().into();
let hook = b"#!/bin/sh
echo \"$(pwd)\"
exit 1
";
git2_hooks::create_hook(
&repo,
git2_hooks::HOOK_PRE_COMMIT,
hook,
);
let res = hooks_pre_commit(repo_path).unwrap();
if let HookResult::NotOk(res) = res {
assert_eq!(
res.trim_end().trim_end_matches('/'),
// TODO: fix if output isn't utf8.
root.to_string_lossy().trim_end_matches('/'),
);
} else {
assert!(false);
}
}
#[test]
fn test_hooks_commit_msg_reject_in_subfolder() {
let (_td, repo) = repo_init().unwrap();
let root = repo.workdir().unwrap();
let hook = b"#!/bin/sh
echo 'msg' > \"$1\"
echo 'rejected'
exit 1
";
git2_hooks::create_hook(
&repo,
git2_hooks::HOOK_COMMIT_MSG,
hook,
);
let subfolder = root.join("foo/");
std::fs::create_dir_all(&subfolder).unwrap();
let mut msg = String::from("test");
let res =
hooks_commit_msg(&subfolder.into(), &mut msg).unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_hooks_commit_msg_reject_in_hooks_folder_githooks_moved_absolute(
) {
let (_td, repo) = repo_init().unwrap();
let root = repo.workdir().unwrap();
let mut config = repo.config().unwrap();
const HOOKS_DIR: &str = "my_hooks";
config.set_str("core.hooksPath", HOOKS_DIR).unwrap();
let hook = b"#!/bin/sh
echo 'msg' > \"$1\"
echo 'rejected'
exit 1
";
let hooks_folder = root.join(HOOKS_DIR);
std::fs::create_dir_all(&hooks_folder).unwrap();
create_hook_in_path(&hooks_folder.join("commit-msg"), hook);
let mut msg = String::from("test");
let res =
hooks_commit_msg(&hooks_folder.into(), &mut msg).unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
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
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
git2_hooks::create_hook(
&repo,
git2_hooks::HOOK_PRE_PUSH,
hook,
);
create_hook(root, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(repo_path).unwrap();
assert!(res != HookResult::Ok);
}
let commit_id = repo.head().unwrap().target().unwrap();
let update = git2_hooks::PrePushRef::new(
"refs/heads/master",
Some(commit_id),
"refs/heads/master",
None,
);
#[test]
fn test_pre_commit_py() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let expected_stdin =
git2_hooks::PrePushRef::to_stdin(&[update.clone()]);
// mirror how python pre-commmit sets itself up
#[cfg(not(windows))]
let hook = b"#!/usr/bin/env python
import sys
sys.exit(0)
";
#[cfg(windows)]
let hook = b"#!/bin/env python.exe
import sys
sys.exit(0)
";
let res = git2_hooks::hooks_pre_push(
&repo,
None,
Some("origin"),
"https://github.com/test/repo.git",
&[update],
)
.unwrap();
create_hook(root, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(repo_path).unwrap();
assert_eq!(res, HookResult::Ok);
}
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"));
}
#[test]
fn test_pre_commit_fail_py() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
// mirror how python pre-commmit sets itself up
#[cfg(not(windows))]
let hook = b"#!/usr/bin/env python
import sys
sys.exit(1)
";
#[cfg(windows)]
let hook = b"#!/bin/env python.exe
import sys
sys.exit(1)
";
create_hook(root, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(repo_path).unwrap();
assert!(res != HookResult::Ok);
}
#[test]
fn test_hooks_commit_msg_reject() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let hook = b"#!/bin/sh
echo 'msg' > $1
echo 'rejected'
exit 1
";
create_hook(root, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_hooks_commit_msg_reject_in_subfolder() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
// let repo_path = root.as_os_str().to_str().unwrap();
let hook = b"#!/bin/sh
echo 'msg' > $1
echo 'rejected'
exit 1
";
create_hook(root, HOOK_COMMIT_MSG, hook);
let subfolder = root.join("foo/");
fs::create_dir_all(&subfolder).unwrap();
let mut msg = String::from("test");
let res =
hooks_commit_msg(subfolder.to_str().unwrap(), &mut msg)
.unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_commit_msg_no_block_but_alter() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let hook = b"#!/bin/sh
echo 'msg' > $1
exit 0
";
create_hook(root, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
assert_eq!(res, HookResult::Ok);
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_post_commit_hook_reject_in_subfolder() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
create_hook(root, HOOK_POST_COMMIT, hook);
let subfolder = root.join("foo/");
fs::create_dir_all(&subfolder).unwrap();
let res =
hooks_post_commit(subfolder.to_str().unwrap()).unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
}
}

View file

@ -1,194 +1,187 @@
use super::{
diff::{get_diff_raw, DiffOptions, HunkHeader},
RepoPath,
diff::{get_diff_raw, HunkHeader},
utils::repo,
};
use crate::{
error::{Error, Result},
hash,
sync::repository::repo,
error::{Error, Result},
hash,
};
use git2::{ApplyLocation, ApplyOptions, Diff};
use scopetime::scope_time;
///
pub fn stage_hunk(
repo_path: &RepoPath,
file_path: &str,
hunk_hash: u64,
options: Option<DiffOptions>,
repo_path: &str,
file_path: &str,
hunk_hash: u64,
) -> Result<()> {
scope_time!("stage_hunk");
scope_time!("stage_hunk");
let repo = repo(repo_path)?;
let repo = repo(repo_path)?;
let diff = get_diff_raw(&repo, file_path, false, false, options)?;
let diff = get_diff_raw(&repo, file_path, false, false, None)?;
let mut opt = ApplyOptions::new();
opt.hunk_callback(|hunk| {
hunk.is_some_and(|hunk| {
let header = HunkHeader::from(hunk);
hash(&header) == hunk_hash
})
});
let mut opt = ApplyOptions::new();
opt.hunk_callback(|hunk| {
hunk.map_or(false, |hunk| {
let header = HunkHeader::from(hunk);
hash(&header) == hunk_hash
})
});
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
Ok(())
Ok(())
}
/// this will fail for an all untracked file
pub fn reset_hunk(
repo_path: &RepoPath,
file_path: &str,
hunk_hash: u64,
options: Option<DiffOptions>,
repo_path: &str,
file_path: &str,
hunk_hash: u64,
) -> Result<()> {
scope_time!("reset_hunk");
scope_time!("reset_hunk");
let repo = repo(repo_path)?;
let repo = repo(repo_path)?;
let diff = get_diff_raw(&repo, file_path, false, false, options)?;
let diff = get_diff_raw(&repo, file_path, false, false, None)?;
let hunk_index = find_hunk_index(&diff, hunk_hash);
if let Some(hunk_index) = hunk_index {
let mut hunk_idx = 0;
let mut opt = ApplyOptions::new();
opt.hunk_callback(|_hunk| {
let res = hunk_idx == hunk_index;
hunk_idx += 1;
res
});
let hunk_index = find_hunk_index(&diff, hunk_hash);
if let Some(hunk_index) = hunk_index {
let mut hunk_idx = 0;
let mut opt = ApplyOptions::new();
opt.hunk_callback(|_hunk| {
let res = hunk_idx == hunk_index;
hunk_idx += 1;
res
});
let diff = get_diff_raw(&repo, file_path, false, true, None)?;
let diff = get_diff_raw(&repo, file_path, false, true, None)?;
repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
Ok(())
} else {
Err(Error::Generic("hunk not found".to_string()))
}
Ok(())
} else {
Err(Error::Generic("hunk not found".to_string()))
}
}
fn find_hunk_index(diff: &Diff, hunk_hash: u64) -> Option<usize> {
let mut result = None;
let mut result = None;
let mut hunk_count = 0;
let mut hunk_count = 0;
let foreach_result = diff.foreach(
&mut |_, _| true,
None,
Some(&mut |_, hunk| {
let header = HunkHeader::from(hunk);
if hash(&header) == hunk_hash {
result = Some(hunk_count);
}
hunk_count += 1;
true
}),
None,
);
let foreach_result = diff.foreach(
&mut |_, _| true,
None,
Some(&mut |_, hunk| {
let header = HunkHeader::from(hunk);
if hash(&header) == hunk_hash {
result = Some(hunk_count);
}
hunk_count += 1;
true
}),
None,
);
if foreach_result.is_ok() {
result
} else {
None
}
if foreach_result.is_ok() {
result
} else {
None
}
}
///
pub fn unstage_hunk(
repo_path: &RepoPath,
file_path: &str,
hunk_hash: u64,
options: Option<DiffOptions>,
repo_path: &str,
file_path: &str,
hunk_hash: u64,
) -> Result<bool> {
scope_time!("revert_hunk");
scope_time!("revert_hunk");
let repo = repo(repo_path)?;
let repo = repo(repo_path)?;
let diff = get_diff_raw(&repo, file_path, true, false, options)?;
let diff_count_positive = diff.deltas().len();
let diff = get_diff_raw(&repo, file_path, true, false, None)?;
let diff_count_positive = diff.deltas().len();
let hunk_index = find_hunk_index(&diff, hunk_hash);
let hunk_index = hunk_index.map_or_else(
|| Err(Error::Generic("hunk not found".to_string())),
Ok,
)?;
let hunk_index = find_hunk_index(&diff, hunk_hash);
let hunk_index = hunk_index.map_or_else(
|| Err(Error::Generic("hunk not found".to_string())),
Ok,
)?;
let diff = get_diff_raw(&repo, file_path, true, true, options)?;
let diff = get_diff_raw(&repo, file_path, true, true, None)?;
if diff.deltas().len() != diff_count_positive {
return Err(Error::Generic(format!(
"hunk error: {}!={}",
diff.deltas().len(),
diff_count_positive
)));
}
if diff.deltas().len() != diff_count_positive {
return Err(Error::Generic(format!(
"hunk error: {}!={}",
diff.deltas().len(),
diff_count_positive
)));
}
let mut count = 0;
{
let mut hunk_idx = 0;
let mut opt = ApplyOptions::new();
opt.hunk_callback(|_hunk| {
let res = if hunk_idx == hunk_index {
count += 1;
true
} else {
false
};
let mut count = 0;
{
let mut hunk_idx = 0;
let mut opt = ApplyOptions::new();
opt.hunk_callback(|_hunk| {
let res = if hunk_idx == hunk_index {
count += 1;
true
} else {
false
};
hunk_idx += 1;
hunk_idx += 1;
res
});
res
});
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
}
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
}
Ok(count == 1)
Ok(count == 1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
error::Result,
sync::{diff::get_diff, tests::repo_init_empty},
};
use std::{
fs::{self, File},
io::Write,
path::Path,
};
use super::*;
use crate::{
error::Result,
sync::{diff::get_diff, tests::repo_init_empty},
};
use std::{
fs::{self, File},
io::Write,
path::Path,
};
#[test]
fn reset_untracked_file_which_will_not_find_hunk() -> Result<()> {
let file_path = Path::new("foo/foo.txt");
let (_td, repo) = repo_init_empty()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
let sub_path = root.join("foo/");
#[test]
fn reset_untracked_file_which_will_not_find_hunk() -> Result<()> {
let file_path = Path::new("foo/foo.txt");
let (_td, repo) = repo_init_empty()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
fs::create_dir_all(&sub_path)?;
File::create(root.join(file_path))?.write_all(b"test")?;
let sub_path = root.join("foo/");
let sub_path: &RepoPath = &sub_path.to_str().unwrap().into();
let diff = get_diff(
sub_path,
file_path.to_str().unwrap(),
false,
None,
)?;
fs::create_dir_all(&sub_path)?;
File::create(&root.join(file_path))?.write_all(b"test")?;
assert!(reset_hunk(
repo_path,
file_path.to_str().unwrap(),
diff.hunks[0].header_hash,
None,
)
.is_err());
let diff = get_diff(
sub_path.to_str().unwrap(),
file_path.to_str().unwrap(),
false,
)?;
Ok(())
}
assert!(reset_hunk(
repo_path,
file_path.to_str().unwrap(),
diff.hunks[0].header_hash,
)
.is_err());
Ok(())
}
}

View file

@ -1,159 +1,127 @@
use super::{utils::work_dir, RepoPath};
use crate::{
error::{Error, Result},
sync::repository::repo,
};
use super::utils::{repo, work_dir};
use crate::error::Result;
use scopetime::scope_time;
use std::{
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
path::Path,
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
path::Path,
};
static GITIGNORE: &str = ".gitignore";
/// add file or path to root ignore file
pub fn add_to_ignore(
repo_path: &RepoPath,
path_to_ignore: &str,
repo_path: &str,
path_to_ignore: &str,
) -> Result<()> {
scope_time!("add_to_ignore");
scope_time!("add_to_ignore");
let repo = repo(repo_path)?;
let repo = repo(repo_path)?;
if Path::new(path_to_ignore).file_name()
== Path::new(GITIGNORE).file_name()
{
return Err(Error::Generic(String::from(
"cannot ignore gitignore",
)));
}
let ignore_file = work_dir(&repo)?.join(GITIGNORE);
let ignore_file = work_dir(&repo)?.join(GITIGNORE);
let optional_newline = ignore_file.exists()
&& !file_ends_with_newline(&ignore_file)?;
let optional_newline = ignore_file.exists()
&& !file_ends_with_newline(&ignore_file)?;
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(ignore_file)?;
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(ignore_file)?;
writeln!(
file,
"{}{}",
if optional_newline { "\n" } else { "" },
path_to_ignore
)?;
writeln!(
file,
"{}{}",
if optional_newline { "\n" } else { "" },
path_to_ignore
)?;
Ok(())
Ok(())
}
fn file_ends_with_newline(file: &Path) -> Result<bool> {
let mut file = File::open(file)?;
let size = file.metadata()?.len();
let mut file = File::open(file)?;
let size = file.metadata()?.len();
file.seek(SeekFrom::Start(size.saturating_sub(1)))?;
let mut last_char = String::with_capacity(1);
file.read_to_string(&mut last_char)?;
file.seek(SeekFrom::Start(size.saturating_sub(1)))?;
let mut last_char = String::with_capacity(1);
file.read_to_string(&mut last_char)?;
Ok(last_char == "\n")
Ok(last_char == "\n")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::{tests::repo_init, utils::repo_write_file};
use io::BufRead;
use pretty_assertions::assert_eq;
use std::{fs::File, io, path::Path};
use super::*;
use crate::sync::tests::repo_init;
use io::BufRead;
use std::{fs::File, io, path::Path};
#[test]
fn test_empty() -> Result<()> {
let ignore_file_path = Path::new(".gitignore");
let file_path = Path::new("foo.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_empty() -> Result<()> {
let ignore_file_path = Path::new(".gitignore");
let file_path = Path::new("foo.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?.write_all(b"test")?;
File::create(&root.join(file_path))?.write_all(b"test")?;
assert_eq!(root.join(ignore_file_path).exists(), false);
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
assert_eq!(root.join(ignore_file_path).exists(), true);
assert_eq!(root.join(ignore_file_path).exists(), false);
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
assert_eq!(root.join(ignore_file_path).exists(), true);
Ok(())
}
Ok(())
}
fn read_lines<P>(
filename: P,
) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
fn read_lines<P>(
filename: P,
) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
#[test]
fn test_append() -> Result<()> {
let ignore_file_path = Path::new(".gitignore");
let file_path = Path::new("foo.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_append() -> Result<()> {
let ignore_file_path = Path::new(".gitignore");
let file_path = Path::new("foo.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?.write_all(b"test")?;
File::create(root.join(ignore_file_path))?
.write_all(b"foo\n")?;
File::create(&root.join(file_path))?.write_all(b"test")?;
File::create(&root.join(ignore_file_path))?
.write_all(b"foo\n")?;
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
let mut lines =
read_lines(root.join(ignore_file_path)).unwrap();
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
let mut lines =
read_lines(&root.join(ignore_file_path)).unwrap();
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
Ok(())
}
Ok(())
}
#[test]
fn test_append_no_newline_at_end() -> Result<()> {
let ignore_file_path = Path::new(".gitignore");
let file_path = Path::new("foo.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_append_no_newline_at_end() -> Result<()> {
let ignore_file_path = Path::new(".gitignore");
let file_path = Path::new("foo.txt");
let (_td, repo) = repo_init()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?.write_all(b"test")?;
File::create(root.join(ignore_file_path))?
.write_all(b"foo")?;
File::create(&root.join(file_path))?.write_all(b"test")?;
File::create(&root.join(ignore_file_path))?
.write_all(b"foo")?;
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
let mut lines =
read_lines(root.join(ignore_file_path)).unwrap();
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
let mut lines =
read_lines(&root.join(ignore_file_path)).unwrap();
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
Ok(())
}
#[test]
fn test_ignore_ignore() {
let ignore_file_path = Path::new(".gitignore");
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
repo_write_file(&repo, ".gitignore", "#foo").unwrap();
let res = add_to_ignore(repo_path, ".gitignore");
assert!(res.is_err());
let lines = read_lines(root.join(ignore_file_path)).unwrap();
assert_eq!(lines.count(), 1);
}
Ok(())
}
}

View file

@ -1,386 +1,248 @@
use super::{CommitId, SharedCommitFilterFn};
use super::CommitId;
use crate::error::Result;
use git2::{Commit, Oid, Repository};
use gix::revision::Walk;
use std::{
cmp::Ordering,
collections::{BinaryHeap, HashSet},
cmp::Ordering,
collections::{BinaryHeap, HashSet},
sync::Arc,
};
struct TimeOrderedCommit<'a>(Commit<'a>);
impl Eq for TimeOrderedCommit<'_> {}
impl<'a> Eq for TimeOrderedCommit<'a> {}
impl PartialEq for TimeOrderedCommit<'_> {
fn eq(&self, other: &Self) -> bool {
self.0.time().eq(&other.0.time())
}
impl<'a> PartialEq for TimeOrderedCommit<'a> {
fn eq(&self, other: &Self) -> bool {
self.0.time().eq(&other.0.time())
}
}
impl PartialOrd for TimeOrderedCommit<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
impl<'a> PartialOrd for TimeOrderedCommit<'a> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.0.time().partial_cmp(&other.0.time())
}
}
impl Ord for TimeOrderedCommit<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.time().cmp(&other.0.time())
}
impl<'a> Ord for TimeOrderedCommit<'a> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.time().cmp(&other.0.time())
}
}
///
pub type LogWalkerFilter = Arc<
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
>;
///
pub struct LogWalker<'a> {
commits: BinaryHeap<TimeOrderedCommit<'a>>,
visited: HashSet<Oid>,
limit: usize,
repo: &'a Repository,
filter: Option<SharedCommitFilterFn>,
commits: BinaryHeap<TimeOrderedCommit<'a>>,
visited: HashSet<Oid>,
limit: usize,
repo: &'a Repository,
filter: Option<LogWalkerFilter>,
}
impl<'a> LogWalker<'a> {
///
pub fn new(repo: &'a Repository, limit: usize) -> Result<Self> {
let c = repo.head()?.peel_to_commit()?;
///
pub fn new(repo: &'a Repository, limit: usize) -> Result<Self> {
let c = repo.head()?.peel_to_commit()?;
let mut commits = BinaryHeap::with_capacity(10);
commits.push(TimeOrderedCommit(c));
let mut commits = BinaryHeap::with_capacity(10);
commits.push(TimeOrderedCommit(c));
Ok(Self {
commits,
limit,
visited: HashSet::with_capacity(1000),
repo,
filter: None,
})
}
Ok(Self {
commits,
limit,
visited: HashSet::with_capacity(1000),
repo,
filter: None,
})
}
///
pub fn visited(&self) -> usize {
self.visited.len()
}
///
pub fn filter(self, filter: Option<LogWalkerFilter>) -> Self {
Self { filter, ..self }
}
///
#[must_use]
pub fn filter(
self,
filter: Option<SharedCommitFilterFn>,
) -> Self {
Self { filter, ..self }
}
///
pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {
let mut count = 0_usize;
///
pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {
let mut count = 0_usize;
while let Some(c) = self.commits.pop() {
for p in c.0.parents() {
self.visit(p);
}
while let Some(c) = self.commits.pop() {
for p in c.0.parents() {
self.visit(p);
}
let id: CommitId = c.0.id().into();
let commit_should_be_included =
if let Some(ref filter) = self.filter {
filter(self.repo, &id)?
} else {
true
};
let id: CommitId = c.0.id().into();
let commit_should_be_included =
if let Some(ref filter) = self.filter {
filter(self.repo, &id)?
} else {
true
};
if commit_should_be_included {
out.push(id);
}
if commit_should_be_included {
out.push(id);
}
count += 1;
if count == self.limit {
break;
}
}
count += 1;
if count == self.limit {
break;
}
}
Ok(count)
}
Ok(count)
}
//
fn visit(&mut self, c: Commit<'a>) {
if self.visited.insert(c.id()) {
self.commits.push(TimeOrderedCommit(c));
}
}
}
/// This is separate from `LogWalker` because filtering currently (June 2024) works through
/// `SharedCommitFilterFn`.
///
/// `SharedCommitFilterFn` requires access to a `git2::repo::Repository` because, under the hood,
/// it calls into functions that work with a `git2::repo::Repository`. It seems unwise to open a
/// repo both through `gix::discover` and `Repository::open_ext` at the same time, so there is a
/// separate struct that works with `gix::Repository` only.
///
/// A more long-term option is to refactor filtering to work with a `gix::Repository` and to remove
/// `LogWalker` once this is done, but this is a larger effort.
pub struct LogWalkerWithoutFilter<'a> {
walk: Walk<'a>,
limit: usize,
visited: usize,
}
impl<'a> LogWalkerWithoutFilter<'a> {
///
pub fn new(
repo: &'a mut gix::Repository,
limit: usize,
) -> Result<Self> {
// This seems to be an object cache size that yields optimal performance. Theres no specific
// reason this is 2^14, so benchmarking might reveal that theres better values.
repo.object_cache_size_if_unset(2_usize.pow(14));
let commit = repo.head()?.peel_to_commit()?;
let tips = [commit.id];
let platform = repo
.rev_walk(tips)
.sorting(gix::revision::walk::Sorting::ByCommitTime(gix::traverse::commit::simple::CommitTimeOrder::NewestFirst))
.use_commit_graph(false);
let walk = platform.all()?;
Ok(Self {
walk,
limit,
visited: 0,
})
}
///
pub const fn visited(&self) -> usize {
self.visited
}
///
pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {
let mut count = 0_usize;
while let Some(Ok(info)) = self.walk.next() {
out.push(info.id.into());
count += 1;
if count == self.limit {
break;
}
}
self.visited += count;
Ok(count)
}
//
fn visit(&mut self, c: Commit<'a>) {
if !self.visited.contains(&c.id()) {
self.visited.insert(c.id());
self.commits.push(TimeOrderedCommit(c));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Result;
use crate::sync::commit_filter::{SearchFields, SearchOptions};
use crate::sync::repository::gix_repo;
use crate::sync::tests::write_commit_file;
use crate::sync::{
commit, get_commits_info, stage_add_file,
tests::repo_init_empty,
};
use crate::sync::{
diff_contains_file, filter_commit_by_search, LogFilterSearch,
LogFilterSearchOptions, RepoPath,
};
use pretty_assertions::assert_eq;
use std::{fs::File, io::Write, path::Path};
use super::*;
use crate::error::Result;
use crate::sync::{
commit, commit_files::get_commit_diff, get_commits_info,
stage_add_file, tests::repo_init_empty,
};
use pretty_assertions::assert_eq;
use std::{fs::File, io::Write, path::Path};
#[test]
fn test_limit() -> Result<()> {
let file_path = Path::new("foo");
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();
#[test]
fn test_limit() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "commit1").unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let oid2 = commit(repo_path, "commit2").unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "commit1").unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let oid2 = commit(repo_path, "commit2").unwrap();
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo, 1)?;
walk.read(&mut items).unwrap();
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo, 1)?;
walk.read(&mut items).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], oid2);
assert_eq!(items.len(), 1);
assert_eq!(items[0], oid2.into());
Ok(())
}
Ok(())
}
#[test]
fn test_logwalker() -> Result<()> {
let file_path = Path::new("foo");
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();
#[test]
fn test_logwalker() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "commit1").unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let oid2 = commit(repo_path, "commit2").unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "commit1").unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let oid2 = commit(repo_path, "commit2").unwrap();
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo, 100)?;
walk.read(&mut items).unwrap();
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo, 100)?;
walk.read(&mut items).unwrap();
let info = get_commits_info(repo_path, &items, 50).unwrap();
dbg!(&info);
let info = get_commits_info(repo_path, &items, 50).unwrap();
dbg!(&info);
assert_eq!(items.len(), 2);
assert_eq!(items[0], oid2);
assert_eq!(items.len(), 2);
assert_eq!(items[0], oid2.into());
let mut items = Vec::new();
walk.read(&mut items).unwrap();
let mut items = Vec::new();
walk.read(&mut items).unwrap();
assert_eq!(items.len(), 0);
assert_eq!(items.len(), 0);
Ok(())
}
Ok(())
}
#[test]
fn test_logwalker_without_filter() -> Result<()> {
let file_path = Path::new("foo");
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();
#[test]
fn test_logwalker_with_filter() -> Result<()> {
let file_path = Path::new("foo");
let second_file_path = Path::new("baz");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
commit(repo_path, "commit1").unwrap();
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let oid2 = commit(repo_path, "commit2").unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path).unwrap();
let mut repo: gix::Repository = gix_repo(repo_path)?;
let mut walk = LogWalkerWithoutFilter::new(&mut repo, 100)?;
let mut items = Vec::new();
assert!(matches!(walk.read(&mut items), Ok(2)));
let _first_commit_id = commit(repo_path, "commit1").unwrap();
let info = get_commits_info(repo_path, &items, 50).unwrap();
dbg!(&info);
File::create(&root.join(second_file_path))?
.write_all(b"a")?;
stage_add_file(repo_path, second_file_path).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0], oid2);
let second_commit_id = commit(repo_path, "commit2").unwrap();
let mut items = Vec::new();
assert!(matches!(walk.read(&mut items), Ok(0)));
File::create(&root.join(file_path))?.write_all(b"b")?;
stage_add_file(repo_path, file_path).unwrap();
assert_eq!(items.len(), 0);
let _third_commit_id = commit(repo_path, "commit3").unwrap();
Ok(())
}
let diff_contains_baz = |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let diff = get_commit_diff(
&repo,
*commit_id,
Some("baz".into()),
)?;
#[test]
fn test_logwalker_with_filter() -> Result<()> {
let file_path = Path::new("foo");
let second_file_path = Path::new("baz");
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();
let contains_file = diff.deltas().len() > 0;
File::create(root.join(file_path))?.write_all(b"a")?;
stage_add_file(&repo_path, file_path).unwrap();
Ok(contains_file)
};
let _first_commit_id = commit(&repo_path, "commit1").unwrap();
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)?
.filter(Some(Arc::new(Box::new(diff_contains_baz))));
walker.read(&mut items).unwrap();
File::create(root.join(second_file_path))?.write_all(b"a")?;
stage_add_file(&repo_path, second_file_path).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], second_commit_id.into());
let second_commit_id = commit(&repo_path, "commit2").unwrap();
let mut items = Vec::new();
walker.read(&mut items).unwrap();
File::create(root.join(file_path))?.write_all(b"b")?;
stage_add_file(&repo_path, file_path).unwrap();
assert_eq!(items.len(), 0);
let _third_commit_id = commit(&repo_path, "commit3").unwrap();
let diff_contains_bar = |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let diff = get_commit_diff(
&repo,
*commit_id,
Some("bar".into()),
)?;
let diff_contains_baz = diff_contains_file("baz".into());
let contains_file = diff.deltas().len() > 0;
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)?
.filter(Some(diff_contains_baz));
walker.read(&mut items).unwrap();
Ok(contains_file)
};
assert_eq!(items.len(), 1);
assert_eq!(items[0], second_commit_id);
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)?
.filter(Some(Arc::new(Box::new(diff_contains_bar))));
walker.read(&mut items).unwrap();
let mut items = Vec::new();
walker.read(&mut items).unwrap();
assert_eq!(items.len(), 0);
assert_eq!(items.len(), 0);
let diff_contains_bar = diff_contains_file("bar".into());
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)?
.filter(Some(diff_contains_bar));
walker.read(&mut items).unwrap();
assert_eq!(items.len(), 0);
Ok(())
}
#[test]
fn test_logwalker_with_filter_search() {
let (_td, repo) = repo_init_empty().unwrap();
write_commit_file(&repo, "foo", "a", "commit1");
let second_commit_id = write_commit_file(
&repo,
"baz",
"a",
"my commit msg (#2)",
);
write_commit_file(&repo, "foo", "b", "commit3");
let log_filter = filter_commit_by_search(
LogFilterSearch::new(LogFilterSearchOptions {
fields: SearchFields::MESSAGE_SUMMARY,
options: SearchOptions::FUZZY_SEARCH,
search_pattern: String::from("my msg"),
}),
);
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)
.unwrap()
.filter(Some(log_filter));
walker.read(&mut items).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], second_commit_id);
let log_filter = filter_commit_by_search(
LogFilterSearch::new(LogFilterSearchOptions {
fields: SearchFields::FILENAMES,
options: SearchOptions::FUZZY_SEARCH,
search_pattern: String::from("fo"),
}),
);
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)
.unwrap()
.filter(Some(log_filter));
walker.read(&mut items).unwrap();
assert_eq!(items.len(), 2);
}
Ok(())
}
}

View file

@ -1,188 +1,141 @@
use crate::{
error::{Error, Result},
sync::{
branch::merge_commit::commit_merge_with_head,
rebase::{
abort_rebase, continue_rebase, get_rebase_progress,
},
repository::repo,
reset_stage, reset_workdir, CommitId,
},
error::{Error, Result},
sync::{
branch::merge_commit::commit_merge_with_head, reset_stage,
reset_workdir, utils, CommitId,
},
};
use git2::{BranchType, Commit, MergeOptions, Repository};
use scopetime::scope_time;
use super::{
rebase::{RebaseProgress, RebaseState},
RepoPath,
};
///
pub fn mergehead_ids(repo_path: &RepoPath) -> Result<Vec<CommitId>> {
scope_time!("mergehead_ids");
pub fn mergehead_ids(repo_path: &str) -> Result<Vec<CommitId>> {
scope_time!("mergehead_ids");
let mut repo = repo(repo_path)?;
let mut repo = utils::repo(repo_path)?;
let mut ids: Vec<CommitId> = Vec::new();
repo.mergehead_foreach(|id| {
ids.push(CommitId::from(*id));
true
})?;
let mut ids: Vec<CommitId> = Vec::new();
repo.mergehead_foreach(|id| {
ids.push(CommitId::from(*id));
true
})?;
Ok(ids)
Ok(ids)
}
/// does these steps:
/// * reset all staged changes,
/// * revert all changes in workdir
/// * cleanup repo merge state
pub fn abort_pending_state(repo_path: &RepoPath) -> Result<()> {
scope_time!("abort_pending_state");
pub fn abort_merge(repo_path: &str) -> Result<()> {
scope_time!("cleanup_state");
let repo = repo(repo_path)?;
let repo = utils::repo(repo_path)?;
reset_stage(repo_path, "*")?;
reset_workdir(repo_path, "*")?;
reset_stage(repo_path, "*")?;
reset_workdir(repo_path, "*")?;
repo.cleanup_state()?;
repo.cleanup_state()?;
Ok(())
Ok(())
}
///
pub fn merge_branch(
repo_path: &RepoPath,
branch: &str,
branch_type: BranchType,
) -> Result<()> {
scope_time!("merge_branch");
pub fn merge_branch(repo_path: &str, branch: &str) -> Result<()> {
scope_time!("merge_branch");
let repo = repo(repo_path)?;
let repo = utils::repo(repo_path)?;
merge_branch_repo(&repo, branch, branch_type)?;
merge_branch_repo(&repo, branch)?;
Ok(())
}
///
pub fn rebase_progress(
repo_path: &RepoPath,
) -> Result<RebaseProgress> {
scope_time!("rebase_progress");
let repo = repo(repo_path)?;
get_rebase_progress(&repo)
}
///
pub fn continue_pending_rebase(
repo_path: &RepoPath,
) -> Result<RebaseState> {
scope_time!("continue_pending_rebase");
let repo = repo(repo_path)?;
continue_rebase(&repo)
}
///
pub fn abort_pending_rebase(repo_path: &RepoPath) -> Result<()> {
scope_time!("abort_pending_rebase");
let repo = repo(repo_path)?;
abort_rebase(&repo)
Ok(())
}
///
pub fn merge_branch_repo(
repo: &Repository,
branch: &str,
branch_type: BranchType,
repo: &Repository,
branch: &str,
) -> Result<()> {
let branch = repo.find_branch(branch, branch_type)?;
let branch = repo.find_branch(branch, BranchType::Local)?;
let annotated =
repo.reference_to_annotated_commit(&branch.into_reference())?;
let annotated =
repo.reference_to_annotated_commit(&branch.into_reference())?;
let (analysis, _) = repo.merge_analysis(&[&annotated])?;
let (analysis, _) = repo.merge_analysis(&[&annotated])?;
//TODO: support merge on unborn
if analysis.is_unborn() {
return Err(Error::Generic("head is unborn".into()));
}
//TODO: support merge on unborn
if analysis.is_unborn() {
return Err(Error::Generic("head is unborn".into()));
}
let mut opt = MergeOptions::default();
let mut opt = MergeOptions::default();
repo.merge(&[&annotated], Some(&mut opt), None)?;
repo.merge(&[&annotated], Some(&mut opt), None)?;
Ok(())
Ok(())
}
///
pub fn merge_msg(repo_path: &RepoPath) -> Result<String> {
scope_time!("merge_msg");
pub fn merge_msg(repo_path: &str) -> Result<String> {
scope_time!("merge_msg");
let repo = repo(repo_path)?;
let content = repo.message()?;
let repo = utils::repo(repo_path)?;
let content = repo.message()?;
Ok(content)
Ok(content)
}
///
pub fn merge_commit(
repo_path: &RepoPath,
msg: &str,
ids: &[CommitId],
repo_path: &str,
msg: &str,
ids: &[CommitId],
) -> Result<CommitId> {
scope_time!("merge_commit");
scope_time!("merge_commit");
let repo = repo(repo_path)?;
let repo = utils::repo(repo_path)?;
let mut commits: Vec<Commit> = Vec::new();
let mut commits: Vec<Commit> = Vec::new();
for id in ids {
commits.push(repo.find_commit((*id).into())?);
}
for id in ids {
commits.push(repo.find_commit((*id).into())?);
}
let id = commit_merge_with_head(&repo, &commits, msg)?;
let id = commit_merge_with_head(&repo, &commits, msg)?;
Ok(id)
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::{
create_branch,
tests::{repo_init, write_commit_file},
RepoPath,
};
use pretty_assertions::assert_eq;
use super::*;
use crate::sync::{
create_branch,
tests::{repo_init, write_commit_file},
};
use pretty_assertions::assert_eq;
#[test]
fn test_smoke() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
#[test]
fn test_smoke() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let c1 =
write_commit_file(&repo, "test.txt", "test", "commit1");
let c1 =
write_commit_file(&repo, "test.txt", "test", "commit1");
create_branch(repo_path, "foo").unwrap();
create_branch(repo_path, "foo").unwrap();
write_commit_file(&repo, "test.txt", "test2", "commit2");
write_commit_file(&repo, "test.txt", "test2", "commit2");
merge_branch(repo_path, "master", BranchType::Local).unwrap();
merge_branch(repo_path, "master").unwrap();
let msg = merge_msg(repo_path).unwrap();
let msg = merge_msg(repo_path).unwrap();
assert_eq!(&msg[0..12], "Merge branch");
assert_eq!(&msg[0..12], "Merge branch");
let mergeheads = mergehead_ids(repo_path).unwrap();
let mergeheads = mergehead_ids(repo_path).unwrap();
assert_eq!(mergeheads[0], c1);
}
assert_eq!(mergeheads[0], c1);
}
}

View file

@ -5,11 +5,9 @@
pub mod blame;
pub mod branch;
pub mod commit;
mod commit;
mod commit_details;
pub mod commit_files;
mod commit_filter;
mod commit_revert;
mod commit_files;
mod commits_info;
mod config;
pub mod cred;
@ -20,361 +18,300 @@ mod ignore;
mod logwalker;
mod merge;
mod patches;
mod rebase;
pub mod remotes;
mod repository;
mod reset;
mod reword;
pub mod sign;
mod staging;
mod stash;
mod state;
pub mod status;
mod submodules;
mod tags;
mod tree;
pub mod utils;
pub use blame::{blame_file, BlameHunk, FileBlame};
pub use branch::{
branch_compare_upstream, checkout_branch, checkout_commit,
config_is_pull_rebase, create_branch, delete_branch,
get_branch_remote, get_branch_upstream_merge, get_branches_info,
merge_commit::merge_upstream_commit,
merge_ff::branch_merge_upstream_fastforward,
merge_rebase::merge_upstream_rebase, rename::rename_branch,
validate_branch_name, BranchCompare, BranchDetails, BranchInfo,
branch_compare_upstream, checkout_branch, config_is_pull_rebase,
create_branch, delete_branch, get_branch_remote,
get_branches_info, merge_commit::merge_upstream_commit,
merge_ff::branch_merge_upstream_fastforward,
merge_rebase::merge_upstream_rebase, rename::rename_branch,
BranchCompare, BranchInfo,
};
pub use commit::{amend, commit, tag_commit};
pub use commit::{amend, commit, tag};
pub use commit_details::{
get_commit_details, CommitDetails, CommitMessage, CommitSignature,
get_commit_details, CommitDetails, CommitMessage, CommitSignature,
};
pub use commit_files::get_commit_files;
pub use commit_filter::{
diff_contains_file, filter_commit_by_search, LogFilterSearch,
LogFilterSearchOptions, SearchFields, SearchOptions,
SharedCommitFilterFn,
};
pub use commit_revert::{commit_revert, revert_commit, revert_head};
pub use commits_info::{
get_commit_info, get_commits_info, CommitId, CommitInfo,
get_commit_info, get_commits_info, CommitId, CommitInfo,
};
pub use config::{
get_config_string, untracked_files_config,
ShowUntrackedFilesConfig,
get_config_string, untracked_files_config,
ShowUntrackedFilesConfig,
};
pub use diff::get_diff_commit;
pub use git2::BranchType;
pub use hooks::{
hooks_commit_msg, hooks_post_commit, hooks_pre_commit,
hooks_pre_push, hooks_prepare_commit_msg, HookResult,
PrePushTarget, PrepareCommitMsgSource,
hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult,
};
pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
pub use ignore::add_to_ignore;
pub use logwalker::{LogWalker, LogWalkerWithoutFilter};
pub use logwalker::{LogWalker, LogWalkerFilter};
pub use merge::{
abort_pending_rebase, abort_pending_state,
continue_pending_rebase, merge_branch, merge_commit, merge_msg,
mergehead_ids, rebase_progress,
abort_merge, merge_branch, merge_commit, merge_msg, mergehead_ids,
};
pub use rebase::rebase_branch;
pub use remotes::{
add_remote, delete_remote, get_default_remote,
get_default_remote_for_fetch, get_default_remote_for_push,
get_remote_url, get_remotes, push::AsyncProgress, rename_remote,
tags::PushTagsProgress, update_remote_url, validate_remote_name,
get_default_remote, get_remotes, push::AsyncProgress,
tags::PushTagsProgress,
};
pub(crate) use repository::{gix_repo, repo};
pub use repository::{RepoPath, RepoPathRef};
pub use reset::{reset_repo, reset_stage, reset_workdir};
pub use reword::reword;
pub use reset::{reset_stage, reset_workdir};
pub use staging::{discard_lines, stage_lines};
pub use stash::{
get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
};
pub use state::{repo_state, RepoState};
pub use status::is_workdir_clean;
pub use submodules::{
get_submodules, submodule_parent_info, update_submodule,
SubmoduleInfo, SubmoduleParentInfo, SubmoduleStatus,
};
pub use tags::{
delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag,
TagWithMetadata, Tags,
delete_tag, get_tags, get_tags_with_metadata, CommitTags,
TagWithMetadata, Tags,
};
pub use tree::{tree_file_content, tree_files, TreeFile};
pub use utils::{
get_head, get_head_tuple, repo_dir, repo_open_error,
stage_add_all, stage_add_file, stage_addremoved, Head,
get_head, get_head_tuple, is_bare_repo, is_repo, repo_dir,
stage_add_all, stage_add_file, stage_addremoved, Head,
};
pub use git2::ResetType;
/// test utils
#[cfg(test)]
pub mod tests {
use super::{
commit,
repository::repo,
stage_add_file,
status::{get_status, StatusType},
utils::{get_head_repo, repo_write_file},
CommitId, LogWalker, RepoPath,
};
use crate::error::Result;
use git2::Repository;
use std::{ffi::OsStr, path::Path, process::Command};
use tempfile::TempDir;
mod tests {
use super::{
commit, stage_add_file,
status::{get_status, StatusType},
utils::{get_head_repo, repo, repo_write_file},
CommitId, LogWalker,
};
use crate::error::Result;
use git2::Repository;
use std::{path::Path, process::Command};
use tempfile::TempDir;
///
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
init_log();
/// Calling `set_search_path` with an empty directory makes sure that there
/// is no git config interfering with our tests (for example user-local
/// `.gitconfig`).
#[allow(unsafe_code)]
fn sandbox_config_files() {
use git2::{opts::set_search_path, ConfigLevel};
use std::sync::Once;
sandbox_config_files();
static INIT: Once = Once::new();
let td = TempDir::new()?;
let repo = Repository::init(td.path())?;
{
let mut config = repo.config()?;
config.set_str("user.name", "name")?;
config.set_str("user.email", "email")?;
}
Ok((td, repo))
}
// Adapted from https://github.com/rust-lang/cargo/pull/9035
INIT.call_once(|| unsafe {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path();
///
pub fn repo_init() -> Result<(TempDir, Repository)> {
repo_init_with_prefix("gitui")
}
set_search_path(ConfigLevel::System, &path).unwrap();
set_search_path(ConfigLevel::Global, &path).unwrap();
set_search_path(ConfigLevel::XDG, &path).unwrap();
set_search_path(ConfigLevel::ProgramData, &path).unwrap();
});
}
///
#[inline]
pub fn repo_init_with_prefix(
prefix: impl AsRef<OsStr>,
) -> Result<(TempDir, Repository)> {
init_log();
/// write, stage and commit a file
pub fn write_commit_file(
repo: &Repository,
file: &str,
content: &str,
commit_name: &str,
) -> CommitId {
repo_write_file(repo, file, content).unwrap();
sandbox_config_files();
stage_add_file(
repo.workdir().unwrap().to_str().unwrap(),
Path::new(file),
)
.unwrap();
let td = TempDir::with_prefix(prefix)?;
let repo = Repository::init(td.path())?;
{
let mut config = repo.config()?;
config.set_str("user.name", "name")?;
config.set_str("user.email", "email")?;
commit(repo.workdir().unwrap().to_str().unwrap(), commit_name)
.unwrap()
}
let mut index = repo.index()?;
let id = index.write_tree()?;
/// write, stage and commit a file giving the commit a specific timestamp
pub fn write_commit_file_at(
repo: &Repository,
file: &str,
content: &str,
commit_name: &str,
time: git2::Time,
) -> CommitId {
repo_write_file(repo, file, content).unwrap();
let tree = repo.find_tree(id)?;
let sig = repo.signature()?;
repo.commit(
Some("HEAD"),
&sig,
&sig,
"initial",
&tree,
&[],
)?;
}
Ok((td, repo))
}
let path = repo.workdir().unwrap().to_str().unwrap();
///
pub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> {
sandbox_config_files();
stage_add_file(path, Path::new(file)).unwrap();
let td = TempDir::new()?;
commit_at(path, commit_name, time)
}
let td_path = td.path().as_os_str().to_str().unwrap();
fn commit_at(
repo_path: &str,
msg: &str,
time: git2::Time,
) -> CommitId {
let repo = repo(repo_path).unwrap();
let repo = Repository::clone(p, td_path).unwrap();
let signature =
git2::Signature::new("name", "email", &time).unwrap();
let mut index = repo.index().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let mut config = repo.config()?;
config.set_str("user.name", "name")?;
config.set_str("user.email", "email")?;
let parents = if let Ok(id) = get_head_repo(&repo) {
vec![repo.find_commit(id.into()).unwrap()]
} else {
Vec::new()
};
Ok((td, repo))
}
let parents = parents.iter().collect::<Vec<_>>();
/// write, stage and commit a file
pub fn write_commit_file(
repo: &Repository,
file: &str,
content: &str,
commit_name: &str,
) -> CommitId {
repo_write_file(repo, file, content).unwrap();
let commit = repo
.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)
.unwrap()
.into();
stage_add_file(
&repo.workdir().unwrap().to_str().unwrap().into(),
Path::new(file),
)
.unwrap();
commit
}
commit(
&repo.workdir().unwrap().to_str().unwrap().into(),
commit_name,
)
.unwrap()
}
///
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
sandbox_config_files();
/// write, stage and commit a file giving the commit a specific timestamp
pub fn write_commit_file_at(
repo: &Repository,
file: &str,
content: &str,
commit_name: &str,
time: git2::Time,
) -> CommitId {
repo_write_file(repo, file, content).unwrap();
let td = TempDir::new()?;
let repo = Repository::init(td.path())?;
{
let mut config = repo.config()?;
config.set_str("user.name", "name")?;
config.set_str("user.email", "email")?;
}
Ok((td, repo))
}
let path: &RepoPath =
&repo.workdir().unwrap().to_str().unwrap().into();
///
pub fn repo_init() -> Result<(TempDir, Repository)> {
sandbox_config_files();
stage_add_file(path, Path::new(file)).unwrap();
let td = TempDir::new()?;
let repo = Repository::init(td.path())?;
{
let mut config = repo.config()?;
config.set_str("user.name", "name")?;
config.set_str("user.email", "email")?;
commit_at(path, commit_name, time)
}
let mut index = repo.index()?;
let id = index.write_tree()?;
/// helper returning amount of files with changes in the (wd,stage)
pub fn get_statuses(repo_path: &RepoPath) -> (usize, usize) {
(
get_status(repo_path, StatusType::WorkingDir, None)
.unwrap()
.len(),
get_status(repo_path, StatusType::Stage, None)
.unwrap()
.len(),
)
}
let tree = repo.find_tree(id)?;
let sig = repo.signature()?;
repo.commit(
Some("HEAD"),
&sig,
&sig,
"initial",
&tree,
&[],
)?;
}
Ok((td, repo))
}
///
pub fn debug_cmd_print(path: &RepoPath, cmd: &str) {
let cmd = debug_cmd(path, cmd);
eprintln!("\n----\n{cmd}");
}
///
pub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> {
sandbox_config_files();
/// helper to fetch commit details using log walker
pub fn get_commit_ids(
r: &Repository,
max_count: usize,
) -> Vec<CommitId> {
let mut commit_ids = Vec::<CommitId>::new();
LogWalker::new(r, max_count)
.unwrap()
.read(&mut commit_ids)
.unwrap();
let td = TempDir::new()?;
commit_ids
}
let td_path = td.path().as_os_str().to_str().unwrap();
/// Same as `repo_init`, but the repo is a bare repo (--bare)
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
init_log();
let repo = Repository::clone(p, td_path).unwrap();
let tmp_repo_dir = TempDir::new()?;
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
Ok((tmp_repo_dir, bare_repo))
}
let mut config = repo.config()?;
config.set_str("user.name", "name")?;
config.set_str("user.email", "email")?;
/// Calling `set_search_path` with an empty directory makes sure that there
/// is no git config interfering with our tests (for example user-local
/// `.gitconfig`).
#[allow(unsafe_code)]
fn sandbox_config_files() {
use git2::{opts::set_search_path, ConfigLevel};
use std::sync::Once;
Ok((td, repo))
}
static INIT: Once = Once::new();
/// Same as repo_init, but the repo is a bare repo (--bare)
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
let tmp_repo_dir = TempDir::new()?;
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
Ok((tmp_repo_dir, bare_repo))
}
// Adapted from https://github.com/rust-lang/cargo/pull/9035
INIT.call_once(|| unsafe {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path();
/// helper returning amount of files with changes in the (wd,stage)
pub fn get_statuses(repo_path: &str) -> (usize, usize) {
(
get_status(repo_path, StatusType::WorkingDir)
.unwrap()
.len(),
get_status(repo_path, StatusType::Stage).unwrap().len(),
)
}
set_search_path(ConfigLevel::System, path).unwrap();
set_search_path(ConfigLevel::Global, path).unwrap();
set_search_path(ConfigLevel::XDG, path).unwrap();
set_search_path(ConfigLevel::ProgramData, path).unwrap();
});
}
///
pub fn debug_cmd_print(path: &str, cmd: &str) {
let cmd = debug_cmd(path, cmd);
eprintln!("\n----\n{}", cmd);
}
fn commit_at(
repo_path: &RepoPath,
msg: &str,
time: git2::Time,
) -> CommitId {
let repo = repo(repo_path).unwrap();
/// helper to fetch commmit details using log walker
pub fn get_commit_ids(
r: &Repository,
max_count: usize,
) -> Vec<CommitId> {
let mut commit_ids = Vec::<CommitId>::new();
LogWalker::new(r, max_count)
.unwrap()
.read(&mut commit_ids)
.unwrap();
let signature =
git2::Signature::new("name", "email", &time).unwrap();
let mut index = repo.index().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
commit_ids
}
let parents = if let Ok(id) = get_head_repo(&repo) {
vec![repo.find_commit(id.into()).unwrap()]
} else {
Vec::new()
};
fn debug_cmd(path: &str, cmd: &str) -> String {
let output = if cfg!(target_os = "windows") {
Command::new("cmd")
.args(&["/C", cmd])
.current_dir(path)
.output()
.unwrap()
} else {
Command::new("sh")
.arg("-c")
.arg(cmd)
.current_dir(path)
.output()
.unwrap()
};
let parents = parents.iter().collect::<Vec<_>>();
let commit = repo
.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)
.unwrap()
.into();
commit
}
// init log
fn init_log() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
fn debug_cmd(path: &RepoPath, cmd: &str) -> String {
let output = if cfg!(target_os = "windows") {
Command::new("cmd")
.args(["/C", cmd])
.current_dir(path.gitpath())
.output()
.unwrap()
} else {
Command::new("sh")
.arg("-c")
.arg(cmd)
.current_dir(path.gitpath())
.output()
.unwrap()
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
format!(
"{}{}",
if stdout.is_empty() {
String::new()
} else {
format!("out:\n{stdout}")
},
if stderr.is_empty() {
String::new()
} else {
format!("err:\n{stderr}")
}
)
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
format!(
"{}{}",
if stdout.is_empty() {
String::new()
} else {
format!("out:\n{}", stdout)
},
if stderr.is_empty() {
String::new()
} else {
format!("err:\n{}", stderr)
}
)
}
}

View file

@ -1,78 +1,73 @@
use super::diff::{get_diff_raw, DiffOptions, HunkHeader};
use super::diff::{get_diff_raw, HunkHeader};
use crate::error::{Error, Result};
use git2::{Diff, DiffLine, Patch, Repository};
pub struct HunkLines<'a> {
pub hunk: HunkHeader,
pub lines: Vec<DiffLine<'a>>,
#[allow(clippy::redundant_pub_crate)]
pub(crate) struct HunkLines<'a> {
pub hunk: HunkHeader,
pub lines: Vec<DiffLine<'a>>,
}
pub fn get_file_diff_patch<'a>(
repo: &'a Repository,
file: &str,
is_staged: bool,
reverse: bool,
) -> Result<Patch<'a>> {
let diff = get_diff_raw(
repo,
file,
is_staged,
reverse,
Some(DiffOptions {
context: 1,
..DiffOptions::default()
}),
)?;
let patches = get_patches(&diff)?;
if patches.len() > 1 {
return Err(Error::Generic(String::from("patch error")));
}
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn get_file_diff_patch_and_hunklines<'a>(
repo: &'a Repository,
file: &str,
is_staged: bool,
reverse: bool,
) -> Result<(Patch<'a>, Vec<HunkLines<'a>>)> {
let diff = get_diff_raw(repo, file, is_staged, reverse, Some(1))?;
let patches = get_patches(&diff)?;
if patches.len() > 1 {
return Err(Error::Generic(String::from("patch error")));
}
let patch = patches.into_iter().next().ok_or_else(|| {
Error::Generic(String::from("no patch found"))
})?;
let patch = patches.into_iter().next().ok_or_else(|| {
Error::Generic(String::from("no patch found"))
})?;
Ok(patch)
let lines = patch_get_hunklines(&patch)?;
Ok((patch, lines))
}
//
pub fn patch_get_hunklines<'a>(
patch: &'a Patch<'a>,
fn patch_get_hunklines<'a>(
patch: &Patch<'a>,
) -> Result<Vec<HunkLines<'a>>> {
let count_hunks = patch.num_hunks();
let mut res = Vec::with_capacity(count_hunks);
for hunk_idx in 0..count_hunks {
let (hunk, _) = patch.hunk(hunk_idx)?;
let count_hunks = patch.num_hunks();
let mut res = Vec::with_capacity(count_hunks);
for hunk_idx in 0..count_hunks {
let (hunk, _) = patch.hunk(hunk_idx)?;
let count_lines = patch.num_lines_in_hunk(hunk_idx)?;
let count_lines = patch.num_lines_in_hunk(hunk_idx)?;
let mut hunk = HunkLines {
hunk: HunkHeader::from(hunk),
lines: Vec::with_capacity(count_lines),
};
let mut hunk = HunkLines {
hunk: HunkHeader::from(hunk),
lines: Vec::with_capacity(count_lines),
};
for line_idx in 0..count_lines {
let line = patch.line_in_hunk(hunk_idx, line_idx)?;
hunk.lines.push(line);
}
for line_idx in 0..count_lines {
let line = patch.line_in_hunk(hunk_idx, line_idx)?;
hunk.lines.push(line);
}
res.push(hunk);
}
res.push(hunk);
}
Ok(res)
Ok(res)
}
//
fn get_patches<'a>(diff: &Diff<'a>) -> Result<Vec<Patch<'a>>> {
let count = diff.deltas().len();
let count = diff.deltas().len();
let mut res = Vec::with_capacity(count);
for idx in 0..count {
let p = Patch::from_diff(diff, idx)?;
if let Some(p) = p {
res.push(p);
}
}
let mut res = Vec::with_capacity(count);
for idx in 0..count {
let p = Patch::from_diff(diff, idx)?;
if let Some(p) = p {
res.push(p);
}
}
Ok(res)
Ok(res)
}

View file

@ -1,343 +0,0 @@
use git2::{BranchType, Repository};
use scopetime::scope_time;
use crate::{
error::{Error, Result},
sync::repository::repo,
};
use super::{CommitId, RepoPath};
/// rebase current HEAD on `branch`
pub fn rebase_branch(
repo_path: &RepoPath,
branch: &str,
branch_type: BranchType,
) -> Result<RebaseState> {
scope_time!("rebase_branch");
let repo = repo(repo_path)?;
rebase_branch_repo(&repo, branch, branch_type)
}
fn rebase_branch_repo(
repo: &Repository,
branch_name: &str,
branch_type: BranchType,
) -> Result<RebaseState> {
let branch = repo.find_branch(branch_name, branch_type)?;
let annotated =
repo.reference_to_annotated_commit(&branch.into_reference())?;
rebase(repo, &annotated)
}
/// rebase attempt which aborts and undo's rebase if any conflict appears
pub fn conflict_free_rebase(
repo: &git2::Repository,
commit: &git2::AnnotatedCommit,
) -> Result<CommitId> {
let mut rebase = repo.rebase(None, Some(commit), None, None)?;
let signature =
crate::sync::commit::signature_allow_undefined_name(repo)?;
let mut last_commit = None;
while let Some(op) = rebase.next() {
let _op = op?;
if repo.index()?.has_conflicts() {
rebase.abort()?;
return Err(Error::RebaseConflict);
}
let c = rebase.commit(None, &signature, None)?;
last_commit = Some(CommitId::from(c));
}
if repo.index()?.has_conflicts() {
rebase.abort()?;
return Err(Error::RebaseConflict);
}
rebase.finish(Some(&signature))?;
last_commit.ok_or_else(|| {
Error::Generic(String::from("no commit rebased"))
})
}
///
#[derive(PartialEq, Eq, Debug)]
pub enum RebaseState {
///
Finished,
///
Conflicted,
}
/// rebase
pub fn rebase(
repo: &git2::Repository,
commit: &git2::AnnotatedCommit,
) -> Result<RebaseState> {
let mut rebase = repo.rebase(None, Some(commit), None, None)?;
let signature =
crate::sync::commit::signature_allow_undefined_name(repo)?;
while let Some(op) = rebase.next() {
let _op = op?;
// dbg!(op.id());
if repo.index()?.has_conflicts() {
return Ok(RebaseState::Conflicted);
}
rebase.commit(None, &signature, None)?;
}
if repo.index()?.has_conflicts() {
return Ok(RebaseState::Conflicted);
}
rebase.finish(Some(&signature))?;
Ok(RebaseState::Finished)
}
/// continue pending rebase
pub fn continue_rebase(
repo: &git2::Repository,
) -> Result<RebaseState> {
let mut rebase = repo.open_rebase(None)?;
let signature =
crate::sync::commit::signature_allow_undefined_name(repo)?;
if repo.index()?.has_conflicts() {
return Ok(RebaseState::Conflicted);
}
// try commit current rebase step
if !repo.index()?.is_empty() {
rebase.commit(None, &signature, None)?;
}
while let Some(op) = rebase.next() {
let _op = op?;
// dbg!(op.id());
if repo.index()?.has_conflicts() {
return Ok(RebaseState::Conflicted);
}
rebase.commit(None, &signature, None)?;
}
if repo.index()?.has_conflicts() {
return Ok(RebaseState::Conflicted);
}
rebase.finish(Some(&signature))?;
Ok(RebaseState::Finished)
}
///
#[derive(PartialEq, Eq, Debug)]
pub struct RebaseProgress {
///
pub steps: usize,
///
pub current: usize,
///
pub current_commit: Option<CommitId>,
}
///
pub fn get_rebase_progress(
repo: &git2::Repository,
) -> Result<RebaseProgress> {
let mut rebase = repo.open_rebase(None)?;
let current_commit: Option<CommitId> = rebase
.operation_current()
.and_then(|idx| rebase.nth(idx))
.map(|op| op.id().into());
let progress = RebaseProgress {
steps: rebase.len(),
current: rebase.operation_current().unwrap_or_default(),
current_commit,
};
Ok(progress)
}
///
pub fn abort_rebase(repo: &git2::Repository) -> Result<()> {
let mut rebase = repo.open_rebase(None)?;
rebase.abort()?;
Ok(())
}
#[cfg(test)]
mod test_conflict_free_rebase {
use crate::sync::{
checkout_branch, create_branch,
rebase::{rebase_branch, RebaseState},
repo_state,
repository::repo,
tests::{repo_init, write_commit_file},
CommitId, RepoPath, RepoState,
};
use git2::{BranchType, Repository};
use super::conflict_free_rebase;
fn parent_ids(repo: &Repository, c: CommitId) -> Vec<CommitId> {
let foo = repo
.find_commit(c.into())
.unwrap()
.parent_ids()
.map(CommitId::from)
.collect();
foo
}
///
fn test_rebase_branch_repo(
repo_path: &RepoPath,
branch_name: &str,
) -> CommitId {
let repo = repo(repo_path).unwrap();
let branch =
repo.find_branch(branch_name, BranchType::Local).unwrap();
let annotated = repo
.reference_to_annotated_commit(&branch.into_reference())
.unwrap();
conflict_free_rebase(&repo, &annotated).unwrap()
}
#[test]
fn test_smoke() {
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 c1 =
write_commit_file(&repo, "test1.txt", "test", "commit1");
create_branch(repo_path, "foo").unwrap();
let c2 =
write_commit_file(&repo, "test2.txt", "test", "commit2");
assert_eq!(parent_ids(&repo, c2), vec![c1]);
checkout_branch(repo_path, "master").unwrap();
let c3 =
write_commit_file(&repo, "test3.txt", "test", "commit3");
checkout_branch(repo_path, "foo").unwrap();
let r = test_rebase_branch_repo(repo_path, "master");
assert_eq!(parent_ids(&repo, r), vec![c3]);
}
#[test]
fn test_conflict() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
write_commit_file(&repo, "test.txt", "test1", "commit1");
create_branch(repo_path, "foo").unwrap();
write_commit_file(&repo, "test.txt", "test2", "commit2");
checkout_branch(repo_path, "master").unwrap();
write_commit_file(&repo, "test.txt", "test3", "commit3");
checkout_branch(repo_path, "foo").unwrap();
let res =
rebase_branch(repo_path, "master", BranchType::Local);
assert!(matches!(res.unwrap(), RebaseState::Conflicted));
assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase);
}
}
#[cfg(test)]
mod test_rebase {
use crate::sync::{
checkout_branch, create_branch,
rebase::{
abort_rebase, get_rebase_progress, RebaseProgress,
RebaseState,
},
rebase_branch, repo_state,
tests::{repo_init, write_commit_file},
RepoPath, RepoState,
};
use git2::BranchType;
#[test]
fn test_conflicted_abort() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();
write_commit_file(&repo, "test.txt", "test1", "commit1");
create_branch(repo_path, "foo").unwrap();
let c =
write_commit_file(&repo, "test.txt", "test2", "commit2");
checkout_branch(repo_path, "master").unwrap();
write_commit_file(&repo, "test.txt", "test3", "commit3");
checkout_branch(repo_path, "foo").unwrap();
assert!(get_rebase_progress(&repo).is_err());
// rebase
let r = rebase_branch(repo_path, "master", BranchType::Local)
.unwrap();
assert_eq!(r, RebaseState::Conflicted);
assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase);
assert_eq!(
get_rebase_progress(&repo).unwrap(),
RebaseProgress {
current: 0,
steps: 1,
current_commit: Some(c)
}
);
// abort
abort_rebase(&repo).unwrap();
assert_eq!(repo_state(repo_path).unwrap(), RepoState::Clean);
}
}

View file

@ -1,221 +1,223 @@
#![allow(dead_code)]
use super::push::ProgressNotification;
use crate::{error::Result, sync::cred::BasicAuthCredential};
use crossbeam_channel::Sender;
use git2::{Cred, Error as GitError, RemoteCallbacks};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
///
#[derive(Default, Clone)]
pub struct CallbackStats {
pub push_rejected_msg: Option<(String, String)>,
pub push_rejected_msg: Option<(String, String)>,
}
///
#[derive(Clone)]
pub struct Callbacks {
sender: Option<Sender<ProgressNotification>>,
basic_credential: Option<BasicAuthCredential>,
stats: Arc<Mutex<CallbackStats>>,
first_call_to_credentials: Arc<AtomicBool>,
sender: Option<Sender<ProgressNotification>>,
basic_credential: Option<BasicAuthCredential>,
stats: Arc<Mutex<CallbackStats>>,
first_call_to_credentials: Arc<AtomicBool>,
}
impl Callbacks {
///
pub fn new(
sender: Option<Sender<ProgressNotification>>,
basic_credential: Option<BasicAuthCredential>,
) -> Self {
let stats = Arc::new(Mutex::new(CallbackStats::default()));
///
pub fn new(
sender: Option<Sender<ProgressNotification>>,
basic_credential: Option<BasicAuthCredential>,
) -> Self {
let stats = Arc::new(Mutex::new(CallbackStats::default()));
Self {
sender,
basic_credential,
stats,
first_call_to_credentials: Arc::new(AtomicBool::new(
true,
)),
}
}
Self {
sender,
basic_credential,
stats,
first_call_to_credentials: Arc::new(AtomicBool::new(
true,
)),
}
}
///
pub fn get_stats(&self) -> Result<CallbackStats> {
let stats = self.stats.lock()?;
Ok(stats.clone())
}
///
pub fn get_stats(&self) -> Result<CallbackStats> {
let stats = self.stats.lock()?;
Ok(stats.clone())
}
///
pub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {
let mut callbacks = RemoteCallbacks::new();
///
pub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {
let mut callbacks = RemoteCallbacks::new();
let this = self.clone();
callbacks.push_transfer_progress(
move |current, total, bytes| {
this.push_transfer_progress(current, total, bytes);
},
);
let this = self.clone();
callbacks.push_transfer_progress(
move |current, total, bytes| {
this.push_transfer_progress(current, total, bytes);
},
);
let this = self.clone();
callbacks.update_tips(move |name, a, b| {
this.update_tips(name, a, b);
true
});
let this = self.clone();
callbacks.update_tips(move |name, a, b| {
this.update_tips(name, a, b);
true
});
let this = self.clone();
callbacks.transfer_progress(move |p| {
this.transfer_progress(&p);
true
});
let this = self.clone();
callbacks.transfer_progress(move |p| {
this.transfer_progress(&p);
true
});
let this = self.clone();
callbacks.pack_progress(move |stage, current, total| {
this.pack_progress(stage, total, current);
});
let this = self.clone();
callbacks.pack_progress(move |stage, current, total| {
this.pack_progress(stage, total, current);
});
let this = self.clone();
callbacks.push_update_reference(move |reference, msg| {
this.push_update_reference(reference, msg);
Ok(())
});
let this = self.clone();
callbacks.push_update_reference(move |reference, msg| {
this.push_update_reference(reference, msg);
Ok(())
});
let this = self.clone();
callbacks.credentials(
move |url, username_from_url, allowed_types| {
this.credentials(
url,
username_from_url,
allowed_types,
)
},
);
let this = self.clone();
callbacks.credentials(
move |url, username_from_url, allowed_types| {
this.credentials(
url,
username_from_url,
allowed_types,
)
},
);
callbacks.sideband_progress(move |data| {
log::debug!(
"sideband transfer: '{}'",
String::from_utf8_lossy(data).trim()
);
true
});
callbacks
}
callbacks
}
fn push_update_reference(
&self,
reference: &str,
msg: Option<&str>,
) {
log::debug!(
"push_update_reference: '{}' {:?}",
reference,
msg
);
fn push_update_reference(
&self,
reference: &str,
msg: Option<&str>,
) {
log::debug!("push_update_reference: '{reference}' {msg:?}");
if let Ok(mut stats) = self.stats.lock() {
stats.push_rejected_msg = msg
.map(|msg| (reference.to_string(), msg.to_string()));
}
}
if let Ok(mut stats) = self.stats.lock() {
stats.push_rejected_msg = msg
.map(|msg| (reference.to_string(), msg.to_string()));
}
}
fn pack_progress(
&self,
stage: git2::PackBuilderStage,
total: usize,
current: usize,
) {
log::debug!("packing: {:?} - {}/{}", stage, current, total);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::Packing {
stage,
total,
current,
})
});
}
fn pack_progress(
&self,
stage: git2::PackBuilderStage,
total: usize,
current: usize,
) {
log::debug!("packing: {stage:?} - {current}/{total}");
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::Packing {
stage,
total,
current,
})
});
}
fn transfer_progress(&self, p: &git2::Progress) {
log::debug!(
"transfer: {}/{}",
p.received_objects(),
p.total_objects()
);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::Transfer {
objects: p.received_objects(),
total_objects: p.total_objects(),
})
});
}
fn transfer_progress(&self, p: &git2::Progress) {
log::debug!(
"transfer: {}/{}",
p.received_objects(),
p.total_objects()
);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::Transfer {
objects: p.received_objects(),
total_objects: p.total_objects(),
})
});
}
fn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {
log::debug!("update tips: '{}' [{}] [{}]", name, a, b);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::UpdateTips {
name: name.to_string(),
a: a.into(),
b: b.into(),
})
});
}
fn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {
log::debug!("update tips: '{name}' [{a}] [{b}]");
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::UpdateTips {
name: name.to_string(),
a: a.into(),
b: b.into(),
})
});
}
fn push_transfer_progress(
&self,
current: usize,
total: usize,
bytes: usize,
) {
log::debug!("progress: {}/{} ({} B)", current, total, bytes,);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::PushTransfer {
current,
total,
bytes,
})
});
}
fn push_transfer_progress(
&self,
current: usize,
total: usize,
bytes: usize,
) {
log::debug!("progress: {current}/{total} ({bytes} B)");
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::PushTransfer {
current,
total,
bytes,
})
});
}
// If credentials are bad, we don't ask the user to re-fill their creds. We push an error and they will be able to restart their action (for example a push) and retype their creds.
// This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347
// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588
// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/
fn credentials(
&self,
url: &str,
username_from_url: Option<&str>,
allowed_types: git2::CredentialType,
) -> std::result::Result<Cred, GitError> {
log::debug!(
"creds: '{}' {:?} ({:?})",
url,
username_from_url,
allowed_types
);
// If credentials are bad, we don't ask the user to re-fill their creds. We push an error and they will be able to restart their action (for example a push) and retype their creds.
// This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347
// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588
// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/
fn credentials(
&self,
url: &str,
username_from_url: Option<&str>,
allowed_types: git2::CredentialType,
) -> std::result::Result<Cred, GitError> {
log::debug!(
"creds: '{url}' {username_from_url:?} ({allowed_types:?})",
);
// This boolean is used to avoid multiple calls to credentials callback.
if self.first_call_to_credentials.load(Ordering::Relaxed) {
self.first_call_to_credentials
.store(false, Ordering::Relaxed);
} else {
return Err(GitError::from_str("Bad credentials."));
}
// This boolean is used to avoid multiple calls to credentials callback.
if self.first_call_to_credentials.load(Ordering::Relaxed) {
self.first_call_to_credentials
.store(false, Ordering::Relaxed);
} else {
return Err(GitError::from_str("Bad credentials."));
}
match &self.basic_credential {
_ if allowed_types.is_ssh_key() => username_from_url
.map_or_else(
|| {
Err(GitError::from_str(
" Couldn't extract username from url.",
))
},
Cred::ssh_key_from_agent,
),
Some(BasicAuthCredential {
username: Some(user),
password: Some(pwd),
}) if allowed_types.is_user_pass_plaintext() => {
Cred::userpass_plaintext(user, pwd)
}
Some(BasicAuthCredential {
username: Some(user),
password: _,
}) if allowed_types.is_username() => Cred::username(user),
_ if allowed_types.is_default() => Cred::default(),
_ => Err(GitError::from_str("Couldn't find credentials")),
}
}
match &self.basic_credential {
_ if allowed_types.is_ssh_key() => {
match username_from_url {
Some(username) => {
Cred::ssh_key_from_agent(username)
}
None => Err(GitError::from_str(
" Couldn't extract username from url.",
)),
}
}
Some(BasicAuthCredential {
username: Some(user),
password: Some(pwd),
}) if allowed_types.is_user_pass_plaintext() => {
Cred::userpass_plaintext(user, pwd)
}
Some(BasicAuthCredential {
username: Some(user),
password: _,
}) if allowed_types.is_username() => Cred::username(user),
_ if allowed_types.is_default() => Cred::default(),
_ => Err(GitError::from_str("Couldn't find credentials")),
}
}
}

View file

@ -5,555 +5,212 @@ pub(crate) mod push;
pub(crate) mod tags;
use crate::{
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::push::ProgressNotification, repository::repo, utils,
},
ProgressPercent,
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::push::ProgressNotification, utils,
},
};
use crossbeam_channel::Sender;
use git2::{
BranchType, FetchOptions, ProxyOptions, Remote, Repository,
};
use git2::{BranchType, FetchOptions, Repository};
use scopetime::scope_time;
use utils::bytes2string;
pub use callbacks::Callbacks;
pub use tags::tags_missing_remote;
use super::RepoPath;
/// origin
pub const DEFAULT_REMOTE_NAME: &str = "origin";
///
pub fn proxy_auto<'a>() -> ProxyOptions<'a> {
let mut proxy = ProxyOptions::new();
proxy.auto();
proxy
}
pub fn get_remotes(repo_path: &str) -> Result<Vec<String>> {
scope_time!("get_remotes");
///
pub fn add_remote(
repo_path: &RepoPath,
name: &str,
url: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote(name, url)?;
Ok(())
}
let repo = utils::repo(repo_path)?;
let remotes = repo.remotes()?;
let remotes: Vec<String> =
remotes.iter().flatten().map(String::from).collect();
///
pub fn rename_remote(
repo_path: &RepoPath,
name: &str,
new_name: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote_rename(name, new_name)?;
Ok(())
}
///
pub fn update_remote_url(
repo_path: &RepoPath,
name: &str,
new_url: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote_set_url(name, new_url)?;
Ok(())
}
///
pub fn delete_remote(
repo_path: &RepoPath,
remote_name: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote_delete(remote_name)?;
Ok(())
}
///
pub fn validate_remote_name(name: &str) -> bool {
Remote::is_valid_name(name)
}
///
pub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {
scope_time!("get_remotes");
let repo = repo(repo_path)?;
let remotes = repo.remotes()?;
let remotes: Vec<String> =
remotes.iter().flatten().map(String::from).collect();
Ok(remotes)
}
///
pub fn get_remote_url(
repo_path: &RepoPath,
remote_name: &str,
) -> Result<Option<String>> {
let repo = repo(repo_path)?;
let remote = repo.find_remote(remote_name)?.clone();
let url = remote.url();
if let Some(u) = url {
return Ok(Some(u.to_string()));
}
Ok(None)
Ok(remotes)
}
/// tries to find origin or the only remote that is defined if any
/// in case of multiple remotes and none named *origin* we fail
pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
let repo = repo(repo_path)?;
get_default_remote_in_repo(&repo)
}
/// Gets the current branch the user is on.
/// Returns none if they are not on a branch
/// and Err if there was a problem finding the branch
fn get_current_branch(
repo: &Repository,
) -> Result<Option<git2::Branch<'_>>> {
for b in repo.branches(None)? {
let branch = b?.0;
if branch.is_head() {
return Ok(Some(branch));
}
}
Ok(None)
}
/// Tries to find the default repo to fetch from based on configuration.
///
/// > `branch.<name>.remote`
/// >
/// > When on branch `<name>`, it tells `git fetch` and `git push` which remote to fetch from or
/// > push to. [...] If no remote is configured, or if you are not on any branch and there is more
/// > than one remote defined in the repository, it defaults to `origin` for fetching [...].
///
/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote
///
/// Falls back to `get_default_remote_in_repo`.
pub fn get_default_remote_for_fetch(
repo_path: &RepoPath,
) -> Result<String> {
let repo = repo(repo_path)?;
get_default_remote_for_fetch_in_repo(&repo)
}
// TODO: Very similar to `get_default_remote_for_push_in_repo`. Can probably be refactored.
pub(crate) fn get_default_remote_for_fetch_in_repo(
repo: &Repository,
) -> Result<String> {
scope_time!("get_default_remote_for_fetch_in_repo");
let config = repo.config()?;
let branch = get_current_branch(repo)?;
if let Some(branch) = branch {
let remote_name = bytes2string(branch.name_bytes()?)?;
let entry_name = format!("branch.{}.remote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
}
get_default_remote_in_repo(repo)
}
/// Tries to find the default repo to push to based on configuration.
///
/// > `remote.pushDefault`
/// >
/// > The remote to push to by default. Overrides `branch.<name>.remote` for all branches, and is
/// > overridden by `branch.<name>.pushRemote` for specific branches.
///
/// > `branch.<name>.remote`
/// >
/// > When on branch `<name>`, it tells `git fetch` and `git push` which remote to fetch from or
/// > push to. The remote to push to may be overridden with `remote.pushDefault` (for all
/// > branches). The remote to push to, for the current branch, may be further overridden by
/// > `branch.<name>.pushRemote`. If no remote is configured, or if you are not on any branch and
/// > there is more than one remote defined in the repository, it defaults to `origin` for fetching
/// > and `remote.pushDefault` for pushing.
///
/// [git-config-remote-push-default]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-remotepushDefault
/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote
///
/// Falls back to `get_default_remote_in_repo`.
pub fn get_default_remote_for_push(
repo_path: &RepoPath,
) -> Result<String> {
let repo = repo(repo_path)?;
get_default_remote_for_push_in_repo(&repo)
}
// TODO: Very similar to `get_default_remote_for_fetch_in_repo`. Can probably be refactored.
pub(crate) fn get_default_remote_for_push_in_repo(
repo: &Repository,
) -> Result<String> {
scope_time!("get_default_remote_for_push_in_repo");
let config = repo.config()?;
let branch = get_current_branch(repo)?;
if let Some(branch) = branch {
let remote_name = bytes2string(branch.name_bytes()?)?;
let entry_name =
format!("branch.{}.pushRemote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
if let Ok(entry) = config.get_entry("remote.pushDefault") {
return bytes2string(entry.value_bytes());
}
let entry_name = format!("branch.{}.remote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
}
get_default_remote_in_repo(repo)
pub fn get_default_remote(repo_path: &str) -> Result<String> {
let repo = utils::repo(repo_path)?;
get_default_remote_in_repo(&repo)
}
/// see `get_default_remote`
pub(crate) fn get_default_remote_in_repo(
repo: &Repository,
repo: &Repository,
) -> Result<String> {
scope_time!("get_default_remote_in_repo");
scope_time!("get_default_remote_in_repo");
let remotes = repo.remotes()?;
let remotes = repo.remotes()?;
// if `origin` exists return that
let found_origin = remotes
.iter()
.any(|r| r.is_some_and(|r| r == DEFAULT_REMOTE_NAME));
if found_origin {
return Ok(DEFAULT_REMOTE_NAME.into());
}
// if `origin` exists return that
let found_origin = remotes.iter().any(|r| {
r.map(|r| r == DEFAULT_REMOTE_NAME).unwrap_or_default()
});
if found_origin {
return Ok(DEFAULT_REMOTE_NAME.into());
}
//if only one remote exists pick that
if remotes.len() == 1 {
let first_remote = remotes
.iter()
.next()
.flatten()
.map(String::from)
.ok_or_else(|| {
Error::Generic("no remote found".into())
})?;
//if only one remote exists pick that
if remotes.len() == 1 {
let first_remote = remotes
.iter()
.next()
.flatten()
.map(String::from)
.ok_or_else(|| {
Error::Generic("no remote found".into())
})?;
return Ok(first_remote);
}
return Ok(first_remote);
}
//inconclusive
Err(Error::NoDefaultRemoteFound)
//inconclusive
Err(Error::NoDefaultRemoteFound)
}
///
fn fetch_from_remote(
repo_path: &RepoPath,
remote: &str,
basic_credential: Option<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> Result<()> {
let repo = repo(repo_path)?;
let mut remote = repo.find_remote(remote)?;
let mut options = FetchOptions::new();
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.prune(git2::FetchPrune::On);
options.proxy_options(proxy_auto());
options.download_tags(git2::AutotagOption::All);
options.remote_callbacks(callbacks.callbacks());
remote.fetch(&[] as &[&str], Some(&mut options), None)?;
// fetch tags (also removing remotely deleted ones)
remote.fetch(
&["refs/tags/*:refs/tags/*"],
Some(&mut options),
None,
)?;
Ok(())
}
/// updates/prunes all branches from all remotes
pub fn fetch_all(
repo_path: &RepoPath,
basic_credential: &Option<BasicAuthCredential>,
progress_sender: &Option<Sender<ProgressPercent>>,
) -> Result<()> {
scope_time!("fetch_all");
let repo = repo(repo_path)?;
let remotes = repo
.remotes()?
.iter()
.flatten()
.map(String::from)
.collect::<Vec<_>>();
let remotes_count = remotes.len();
for (idx, remote) in remotes.into_iter().enumerate() {
fetch_from_remote(
repo_path,
&remote,
basic_credential.clone(),
None,
)?;
if let Some(sender) = progress_sender {
let progress = ProgressPercent::new(idx, remotes_count);
sender.send(progress)?;
}
}
Ok(())
}
/// fetches from upstream/remote for local `branch`
/// fetches from upstream/remote for `branch`
pub(crate) fn fetch(
repo_path: &RepoPath,
branch: &str,
basic_credential: Option<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
repo_path: &str,
branch: &str,
basic_credential: Option<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> Result<usize> {
scope_time!("fetch");
scope_time!("fetch_origin");
let repo = repo(repo_path)?;
let branch_ref = repo
.find_branch(branch, BranchType::Local)?
.into_reference();
let branch_ref = bytes2string(branch_ref.name_bytes())?;
let remote_name = repo.branch_upstream_remote(&branch_ref)?;
let remote_name = bytes2string(&remote_name)?;
let mut remote = repo.find_remote(&remote_name)?;
let repo = utils::repo(repo_path)?;
let branch_ref = repo
.find_branch(branch, BranchType::Local)?
.into_reference();
let branch_ref = bytes2string(branch_ref.name_bytes())?;
let remote_name = repo.branch_upstream_remote(&branch_ref)?;
let remote_name = bytes2string(&*remote_name)?;
let mut remote = repo.find_remote(&remote_name)?;
let mut options = FetchOptions::new();
options.download_tags(git2::AutotagOption::All);
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.remote_callbacks(callbacks.callbacks());
options.proxy_options(proxy_auto());
let mut options = FetchOptions::new();
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.remote_callbacks(callbacks.callbacks());
remote.fetch(&[branch], Some(&mut options), None)?;
remote.fetch(&[branch], Some(&mut options), None)?;
Ok(remote.stats().received_bytes())
Ok(remote.stats().received_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::tests::{
debug_cmd_print, repo_clone, repo_init,
};
use super::*;
use crate::sync::tests::{
debug_cmd_print, repo_clone, repo_init,
};
#[test]
fn test_smoke() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
#[test]
fn test_smoke() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
let remotes = get_remotes(repo_path).unwrap();
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(remotes, vec![String::from("origin")]);
assert_eq!(remotes, vec![String::from("origin")]);
fetch(repo_path, "master", None, None).unwrap();
}
fetch(repo_path, "master", None, None).unwrap();
}
#[test]
fn test_default_remote() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
#[test]
fn test_default_remote() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
debug_cmd_print(
repo_path,
&format!("git remote add second {remote_path}")[..],
);
debug_cmd_print(
repo_path,
&format!("git remote add second {}", remote_path)[..],
);
let remotes = get_remotes(repo_path).unwrap();
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(
remotes,
vec![String::from("origin"), String::from("second")]
);
assert_eq!(
remotes,
vec![String::from("origin"), String::from("second")]
);
let first =
get_default_remote_in_repo(&repo(repo_path).unwrap())
.unwrap();
assert_eq!(first, String::from("origin"));
}
let first = get_default_remote_in_repo(
&utils::repo(repo_path).unwrap(),
)
.unwrap();
assert_eq!(first, String::from("origin"));
}
#[test]
fn test_default_remote_out_of_order() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
#[test]
fn test_default_remote_out_of_order() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add origin {remote_path}")[..],
);
debug_cmd_print(
repo_path,
&format!("git remote add origin {}", remote_path)[..],
);
//NOTE: apparently remotes are not chronolically sorted but alphabetically
let remotes = get_remotes(repo_path).unwrap();
//NOTE: aparently remotes are not chronolically sorted but alphabetically
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(
remotes,
vec![String::from("alternate"), String::from("origin")]
);
assert_eq!(
remotes,
vec![String::from("alternate"), String::from("origin")]
);
let first =
get_default_remote_in_repo(&repo(repo_path).unwrap())
.unwrap();
assert_eq!(first, String::from("origin"));
}
let first = get_default_remote_in_repo(
&utils::repo(repo_path).unwrap(),
)
.unwrap();
assert_eq!(first, String::from("origin"));
}
#[test]
fn test_default_remote_inconclusive() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
#[test]
fn test_default_remote_inconclusive() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {remote_path}")[..],
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {}", remote_path)[..],
);
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(
remotes,
vec![
String::from("alternate"),
String::from("someremote")
]
);
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(
remotes,
vec![
String::from("alternate"),
String::from("someremote")
]
);
let default_remote =
get_default_remote_in_repo(&repo(repo_path).unwrap());
assert!(matches!(
default_remote,
Err(Error::NoDefaultRemoteFound)
));
}
#[test]
fn test_default_remote_for_fetch() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {remote_path}")[..],
);
let mut config = repo.config().unwrap();
config
.set_str("branch.master.remote", "branchremote")
.unwrap();
let default_fetch_remote =
get_default_remote_for_fetch_in_repo(&repo);
assert!(
matches!(default_fetch_remote, Ok(remote_name) if remote_name == "branchremote")
);
}
#[test]
fn test_default_remote_for_push() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {remote_path}")[..],
);
let mut config = repo.config().unwrap();
config
.set_str("branch.master.remote", "branchremote")
.unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchremote")
);
config.set_str("remote.pushDefault", "pushdefault").unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "pushdefault")
);
config
.set_str("branch.master.pushRemote", "branchpushremote")
.unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchpushremote")
);
}
let res = get_default_remote_in_repo(
&utils::repo(repo_path).unwrap(),
);
assert_eq!(res.is_err(), true);
assert!(matches!(res, Err(Error::NoDefaultRemoteFound)));
}
}

Some files were not shown because too many files have changed in this diff Show more